src/java.base/share/classes/sun/security/ssl/SSLEngineOutputRecord.java
changeset 47216 71c04702a3d5
parent 34687 d302ed125dc9
child 50768 68fa3d4026ea
child 56542 56aaa6cb3693
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/sun/security/ssl/SSLEngineOutputRecord.java	Tue Sep 12 19:03:39 2017 +0200
@@ -0,0 +1,570 @@
+/*
+ * 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;
+    }
+}