/* * Copyright (c) 1996, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package sun.security.ssl; import java.io.*; import java.nio.*; import java.util.*; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import sun.security.util.HexDumpEncoder; import static sun.security.ssl.Ciphertext.RecordType; /** * {@code OutputRecord} implementation for {@code SSLEngine}. */ final class SSLEngineOutputRecord extends OutputRecord implements SSLRecord { private HandshakeFragment fragmenter = null; private LinkedList<RecordMemo> alertMemos = new LinkedList<>(); private boolean isTalkingToV2 = false; // SSLv2Hello private ByteBuffer v2ClientHello = null; // SSLv2Hello private boolean isCloseWaiting = false; SSLEngineOutputRecord() { this.writeAuthenticator = MAC.TLS_NULL; this.packetSize = SSLRecord.maxRecordSize; this.protocolVersion = ProtocolVersion.DEFAULT_TLS; } @Override public synchronized void close() throws IOException { if (!isClosed) { if (alertMemos != null && !alertMemos.isEmpty()) { isCloseWaiting = true; } else { super.close(); } } } @Override void encodeAlert(byte level, byte description) throws IOException { RecordMemo memo = new RecordMemo(); memo.contentType = Record.ct_alert; memo.majorVersion = protocolVersion.major; memo.minorVersion = protocolVersion.minor; memo.encodeCipher = writeCipher; memo.encodeAuthenticator = writeAuthenticator; memo.fragment = new byte[2]; memo.fragment[0] = level; memo.fragment[1] = description; alertMemos.add(memo); } @Override void encodeHandshake(byte[] source, int offset, int length) throws IOException { if (fragmenter == null) { fragmenter = new HandshakeFragment(); } if (firstMessage) { firstMessage = false; if ((helloVersion == ProtocolVersion.SSL20Hello) && (source[offset] == HandshakeMessage.ht_client_hello) && // 5: recode header size (source[offset + 4 + 2 + 32] == 0)) { // V3 session ID is empty // 4: handshake header size // 2: client_version in ClientHello // 32: random in ClientHello // Double space should be big enough for the converted message. v2ClientHello = encodeV2ClientHello( source, (offset + 4), (length - 4)); v2ClientHello.position(2); // exclude the header handshakeHash.update(v2ClientHello); v2ClientHello.position(0); return; } } byte handshakeType = source[offset]; if (handshakeType != HandshakeMessage.ht_hello_request) { handshakeHash.update(source, offset, length); } fragmenter.queueUpFragment(source, offset, length); } @Override void encodeChangeCipherSpec() throws IOException { if (fragmenter == null) { fragmenter = new HandshakeFragment(); } fragmenter.queueUpChangeCipherSpec(); } @Override void encodeV2NoCipher() throws IOException { isTalkingToV2 = true; } @Override Ciphertext encode(ByteBuffer[] sources, int offset, int length, ByteBuffer destination) throws IOException { if (writeAuthenticator.seqNumOverflow()) { if (debug != null && Debug.isOn("ssl")) { System.out.println(Thread.currentThread().getName() + ", sequence number extremely close to overflow " + "(2^64-1 packets). Closing connection."); } throw new SSLHandshakeException("sequence number overflow"); } int macLen = 0; if (writeAuthenticator instanceof MAC) { macLen = ((MAC)writeAuthenticator).MAClen(); } int dstLim = destination.limit(); boolean isFirstRecordOfThePayload = true; int packetLeftSize = Math.min(maxRecordSize, packetSize); boolean needMorePayload = true; long recordSN = 0L; while (needMorePayload) { int fragLen; if (isFirstRecordOfThePayload && needToSplitPayload()) { needMorePayload = true; fragLen = 1; isFirstRecordOfThePayload = false; } else { needMorePayload = false; if (packetLeftSize > 0) { fragLen = writeCipher.calculateFragmentSize( packetLeftSize, macLen, headerSize); fragLen = Math.min(fragLen, Record.maxDataSize); } else { fragLen = Record.maxDataSize; } if (fragmentSize > 0) { fragLen = Math.min(fragLen, fragmentSize); } } int dstPos = destination.position(); int dstContent = dstPos + headerSize + writeCipher.getExplicitNonceSize(); destination.position(dstContent); int remains = Math.min(fragLen, destination.remaining()); fragLen = 0; int srcsLen = offset + length; for (int i = offset; (i < srcsLen) && (remains > 0); i++) { int amount = Math.min(sources[i].remaining(), remains); int srcLimit = sources[i].limit(); sources[i].limit(sources[i].position() + amount); destination.put(sources[i]); sources[i].limit(srcLimit); // restore the limit remains -= amount; fragLen += amount; if (remains > 0) { offset++; length--; } } destination.limit(destination.position()); destination.position(dstContent); if ((debug != null) && Debug.isOn("record")) { System.out.println(Thread.currentThread().getName() + ", WRITE: " + protocolVersion + " " + Record.contentName(Record.ct_application_data) + ", length = " + destination.remaining()); } // Encrypt the fragment and wrap up a record. recordSN = encrypt(writeAuthenticator, writeCipher, Record.ct_application_data, destination, dstPos, dstLim, headerSize, protocolVersion, false); if ((debug != null) && Debug.isOn("packet")) { ByteBuffer temporary = destination.duplicate(); temporary.limit(temporary.position()); temporary.position(dstPos); Debug.printHex( "[Raw write]: length = " + temporary.remaining(), temporary); } packetLeftSize -= destination.position() - dstPos; // remain the limit unchanged destination.limit(dstLim); if (isFirstAppOutputRecord) { isFirstAppOutputRecord = false; } } return new Ciphertext(RecordType.RECORD_APPLICATION_DATA, recordSN); } @Override Ciphertext acquireCiphertext(ByteBuffer destination) throws IOException { if (isTalkingToV2) { // SSLv2Hello // We don't support SSLv2. Send an SSLv2 error message // so that the connection can be closed gracefully. // // Please don't change the limit of the destination buffer. destination.put(SSLRecord.v2NoCipher); if (debug != null && Debug.isOn("packet")) { Debug.printHex( "[Raw write]: length = " + SSLRecord.v2NoCipher.length, SSLRecord.v2NoCipher); } isTalkingToV2 = false; return new Ciphertext(RecordType.RECORD_ALERT, -1L); } if (v2ClientHello != null) { // deliver the SSLv2 format ClientHello message // // Please don't change the limit of the destination buffer. if (debug != null) { if (Debug.isOn("record")) { System.out.println(Thread.currentThread().getName() + ", WRITE: SSLv2 ClientHello message" + ", length = " + v2ClientHello.remaining()); } if (Debug.isOn("packet")) { Debug.printHex( "[Raw write]: length = " + v2ClientHello.remaining(), v2ClientHello); } } destination.put(v2ClientHello); v2ClientHello = null; return new Ciphertext(RecordType.RECORD_CLIENT_HELLO, -1L); } if (alertMemos != null && !alertMemos.isEmpty()) { RecordMemo memo = alertMemos.pop(); int macLen = 0; if (memo.encodeAuthenticator instanceof MAC) { macLen = ((MAC)memo.encodeAuthenticator).MAClen(); } int dstPos = destination.position(); int dstLim = destination.limit(); int dstContent = dstPos + headerSize + writeCipher.getExplicitNonceSize(); destination.position(dstContent); destination.put(memo.fragment); destination.limit(destination.position()); destination.position(dstContent); if ((debug != null) && Debug.isOn("record")) { System.out.println(Thread.currentThread().getName() + ", WRITE: " + protocolVersion + " " + Record.contentName(Record.ct_alert) + ", length = " + destination.remaining()); } // Encrypt the fragment and wrap up a record. long recordSN = encrypt(memo.encodeAuthenticator, memo.encodeCipher, Record.ct_alert, destination, dstPos, dstLim, headerSize, ProtocolVersion.valueOf(memo.majorVersion, memo.minorVersion), false); if ((debug != null) && Debug.isOn("packet")) { ByteBuffer temporary = destination.duplicate(); temporary.limit(temporary.position()); temporary.position(dstPos); Debug.printHex( "[Raw write]: length = " + temporary.remaining(), temporary); } // remain the limit unchanged destination.limit(dstLim); if (isCloseWaiting && (memo.contentType == Record.ct_alert)) { isCloseWaiting = true; close(); } return new Ciphertext(RecordType.RECORD_ALERT, recordSN); } if (fragmenter != null) { return fragmenter.acquireCiphertext(destination); } return null; } @Override boolean isEmpty() { return (!isTalkingToV2) && (v2ClientHello == null) && ((fragmenter == null) || fragmenter.isEmpty()) && ((alertMemos == null) || alertMemos.isEmpty()); } // buffered record fragment private static class RecordMemo { byte contentType; byte majorVersion; byte minorVersion; CipherBox encodeCipher; Authenticator encodeAuthenticator; byte[] fragment; } private static class HandshakeMemo extends RecordMemo { byte handshakeType; int acquireOffset; } final class HandshakeFragment { private LinkedList<RecordMemo> handshakeMemos = new LinkedList<>(); void queueUpFragment(byte[] source, int offset, int length) throws IOException { HandshakeMemo memo = new HandshakeMemo(); memo.contentType = Record.ct_handshake; memo.majorVersion = protocolVersion.major; // kick start version? memo.minorVersion = protocolVersion.minor; memo.encodeCipher = writeCipher; memo.encodeAuthenticator = writeAuthenticator; memo.handshakeType = source[offset]; memo.acquireOffset = 0; memo.fragment = new byte[length - 4]; // 4: header size // 1: HandshakeType // 3: message length System.arraycopy(source, offset + 4, memo.fragment, 0, length - 4); handshakeMemos.add(memo); } void queueUpChangeCipherSpec() { RecordMemo memo = new RecordMemo(); memo.contentType = Record.ct_change_cipher_spec; memo.majorVersion = protocolVersion.major; memo.minorVersion = protocolVersion.minor; memo.encodeCipher = writeCipher; memo.encodeAuthenticator = writeAuthenticator; memo.fragment = new byte[1]; memo.fragment[0] = 1; handshakeMemos.add(memo); } Ciphertext acquireCiphertext(ByteBuffer dstBuf) throws IOException { if (isEmpty()) { return null; } RecordMemo memo = handshakeMemos.getFirst(); HandshakeMemo hsMemo = null; if (memo.contentType == Record.ct_handshake) { hsMemo = (HandshakeMemo)memo; } int macLen = 0; if (memo.encodeAuthenticator instanceof MAC) { macLen = ((MAC)memo.encodeAuthenticator).MAClen(); } // ChangeCipherSpec message is pretty small. Don't worry about // the fragmentation of ChangeCipherSpec record. int fragLen; if (packetSize > 0) { fragLen = Math.min(maxRecordSize, packetSize); fragLen = memo.encodeCipher.calculateFragmentSize( fragLen, macLen, headerSize); } else { fragLen = Record.maxDataSize; } if (fragmentSize > 0) { fragLen = Math.min(fragLen, fragmentSize); } int dstPos = dstBuf.position(); int dstLim = dstBuf.limit(); int dstContent = dstPos + headerSize + memo.encodeCipher.getExplicitNonceSize(); dstBuf.position(dstContent); if (hsMemo != null) { int remainingFragLen = fragLen; while ((remainingFragLen > 0) && !handshakeMemos.isEmpty()) { int memoFragLen = hsMemo.fragment.length; if (hsMemo.acquireOffset == 0) { // Don't fragment handshake message header if (remainingFragLen <= 4) { break; } dstBuf.put(hsMemo.handshakeType); dstBuf.put((byte)((memoFragLen >> 16) & 0xFF)); dstBuf.put((byte)((memoFragLen >> 8) & 0xFF)); dstBuf.put((byte)(memoFragLen & 0xFF)); remainingFragLen -= 4; } // Otherwise, handshake message is fragmented. int chipLen = Math.min(remainingFragLen, (memoFragLen - hsMemo.acquireOffset)); dstBuf.put(hsMemo.fragment, hsMemo.acquireOffset, chipLen); hsMemo.acquireOffset += chipLen; if (hsMemo.acquireOffset == memoFragLen) { handshakeMemos.removeFirst(); // still have space for more records? if ((remainingFragLen > chipLen) && !handshakeMemos.isEmpty()) { // look for the next buffered record fragment RecordMemo reMemo = handshakeMemos.getFirst(); if (reMemo.contentType == Record.ct_handshake) { hsMemo = (HandshakeMemo)reMemo; } else { // not handshake message, break the loop break; } } } remainingFragLen -= chipLen; } fragLen -= remainingFragLen; } else { fragLen = Math.min(fragLen, memo.fragment.length); dstBuf.put(memo.fragment, 0, fragLen); handshakeMemos.removeFirst(); } dstBuf.limit(dstBuf.position()); dstBuf.position(dstContent); if ((debug != null) && Debug.isOn("record")) { System.out.println(Thread.currentThread().getName() + ", WRITE: " + protocolVersion + " " + Record.contentName(memo.contentType) + ", length = " + dstBuf.remaining()); } // Encrypt the fragment and wrap up a record. long recordSN = encrypt(memo.encodeAuthenticator, memo.encodeCipher, memo.contentType, dstBuf, dstPos, dstLim, headerSize, ProtocolVersion.valueOf(memo.majorVersion, memo.minorVersion), false); if ((debug != null) && Debug.isOn("packet")) { ByteBuffer temporary = dstBuf.duplicate(); temporary.limit(temporary.position()); temporary.position(dstPos); Debug.printHex( "[Raw write]: length = " + temporary.remaining(), temporary); } // remain the limit unchanged dstBuf.limit(dstLim); // Reset the fragmentation offset. if (hsMemo != null) { return new Ciphertext(RecordType.valueOf( hsMemo.contentType, hsMemo.handshakeType), recordSN); } else { return new Ciphertext( RecordType.RECORD_CHANGE_CIPHER_SPEC, recordSN); } } boolean isEmpty() { return handshakeMemos.isEmpty(); } } /* * Need to split the payload except the following cases: * * 1. protocol version is TLS 1.1 or later; * 2. bulk cipher does not use CBC mode, including null bulk cipher suites. * 3. the payload is the first application record of a freshly * negotiated TLS session. * 4. the CBC protection is disabled; * * By default, we counter chosen plaintext issues on CBC mode * ciphersuites in SSLv3/TLS1.0 by sending one byte of application * data in the first record of every payload, and the rest in * subsequent record(s). Note that the issues have been solved in * TLS 1.1 or later. * * It is not necessary to split the very first application record of * a freshly negotiated TLS session, as there is no previous * application data to guess. To improve compatibility, we will not * split such records. * * This avoids issues in the outbound direction. For a full fix, * the peer must have similar protections. */ boolean needToSplitPayload() { return (!protocolVersion.useTLS11PlusSpec()) && writeCipher.isCBCMode() && !isFirstAppOutputRecord && Record.enableCBCProtection; } }