/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.harmony.xnet.provider.jsse; import java.io.IOException; import javax.net.ssl.SSLProtocolException; /** * This class performs functionality dedicated to SSL record layer. * It unpacks and routes income data to the appropriate * client protocol (handshake, alert, application data protocols) * and packages outcome data into SSL/TLS records. * Initially created object has null connection state and does not * perform any cryptography computations over the income/outcome data. * After handshake protocol agreed upon security parameters they are placed * into SSLSessionImpl object and available for record protocol as * pending session. The order of setting up of the pending session * as an active session differs for client and server modes. * So for client mode the parameters are provided by handshake protocol * during retrieving of change_cipher_spec message to be sent (by calling of * getChangeCipherSpecMesage method). * For server side mode record protocol retrieves the parameters from * handshake protocol after receiving of client's change_cipher_spec message. * After the pending session has been set up as a current session, * new connection state object is created and used for encryption/decryption * of the messages. * Among with base functionality this class provides the information about * constrains on the data length, and information about correspondence * of plain and encrypted data lengths. * For more information on TLS v1 see http://www.ietf.org/rfc/rfc2246.txt, * on SSL v3 see http://wp.netscape.com/eng/ssl3, * on SSL v2 see http://wp.netscape.com/eng/security/SSL_2.html. */ public class SSLRecordProtocol { /** * Maximum length of allowed plain data fragment * as specified by TLS specification. */ protected static final int MAX_DATA_LENGTH = 16384; // 2^14 /** * Maximum length of allowed compressed data fragment * as specified by TLS specification. */ protected static final int MAX_COMPRESSED_DATA_LENGTH = MAX_DATA_LENGTH + 1024; /** * Maximum length of allowed ciphered data fragment * as specified by TLS specification. */ protected static final int MAX_CIPHERED_DATA_LENGTH = MAX_COMPRESSED_DATA_LENGTH + 1024; /** * Maximum length of ssl record. It is counted as: * type(1) + version(2) + length(2) + MAX_CIPHERED_DATA_LENGTH */ protected static final int MAX_SSL_PACKET_SIZE = MAX_CIPHERED_DATA_LENGTH + 5; // the SSL session used for connection private SSLSessionImpl session; // protocol version of the connection private byte[] version; // input stream of record protocol private SSLInputStream in; // handshake protocol object to which handshaking data will be transmitted private HandshakeProtocol handshakeProtocol; // alert protocol to indicate alerts occurred/received private AlertProtocol alertProtocol; // application data object to which application data will be transmitted private org.apache.harmony.xnet.provider.jsse.Appendable appData; // connection state holding object private ConnectionState activeReadState, activeWriteState, pendingConnectionState; // logger private Logger.Stream logger = Logger.getStream("record"); // flag indicating if session object has been changed after // handshake phase (to distinguish session pending state) private boolean sessionWasChanged = false; // change cipher spec message content private static final byte[] change_cipher_spec_byte = new byte[] {1}; /** * Creates an instance of record protocol and tunes * up the client protocols to use ut. * @param handshakeProtocol: HandshakeProtocol * @param alertProtocol: AlertProtocol * @param in: SSLInputStream * @param appData: Appendable */ protected SSLRecordProtocol(HandshakeProtocol handshakeProtocol, AlertProtocol alertProtocol, SSLInputStream in, Appendable appData) { this.handshakeProtocol = handshakeProtocol; this.handshakeProtocol.setRecordProtocol(this); this.alertProtocol = alertProtocol; this.alertProtocol.setRecordProtocol(this); this.in = in; this.appData = appData; } /** * Returns the session obtained during the handshake negotiation. * If the handshake process was not completed, method returns null. * @return the session in effect. */ protected SSLSessionImpl getSession() { return session; } /** * Returns the minimum possible length of the SSL record. * @return */ protected int getMinRecordSize() { return (activeReadState == null) ? 6 // type + version + length + 1 byte of data : 5 + activeReadState.getMinFragmentSize(); } /** * Returns the record length for the specified incoming data length. * If actual resulting record length is greater than * MAX_CIPHERED_DATA_LENGTH, MAX_CIPHERED_DATA_LENGTH is returned. */ protected int getRecordSize(int data_size) { if (activeWriteState == null) { return 5+data_size; // type + version + length + data_size } else { int res = 5 + activeWriteState.getFragmentSize(data_size); return (res > MAX_CIPHERED_DATA_LENGTH) ? MAX_CIPHERED_DATA_LENGTH // so the source data should be // split into several packets : res; } } /** * Returns the upper bound of length of data containing in the record with * specified length. * If the provided record_size is greater or equal to * MAX_CIPHERED_DATA_LENGTH the returned value will be * MAX_DATA_LENGTH * counted as for data with * MAX_CIPHERED_DATA_LENGTH length. */ protected int getDataSize(int record_size) { record_size -= 5; // - (type + version + length + data_size) if (record_size > MAX_CIPHERED_DATA_LENGTH) { // the data of such size consists of the several packets return MAX_DATA_LENGTH; } if (activeReadState == null) { return record_size; } return activeReadState.getContentSize(record_size); } /** * Depending on the Connection State (Session) encrypts and compress * the provided data, and packs it into TLSCiphertext structure. * @param content_type: int * @return ssl packet created over the current connection state */ protected byte[] wrap(byte content_type, DataStream dataStream) { byte[] fragment = dataStream.getData(MAX_DATA_LENGTH); return wrap(content_type, fragment, 0, fragment.length); } /** * Depending on the Connection State (Session) encrypts and compress * the provided data, and packs it into TLSCiphertext structure. * @param content_type: int * @param fragment: byte[] * @return ssl packet created over the current connection state */ protected byte[] wrap(byte content_type, byte[] fragment, int offset, int len) { if (logger != null) { logger.println("SSLRecordProtocol.wrap: TLSPlaintext.fragment[" +len+"]:"); logger.print(fragment, offset, len); } if (len > MAX_DATA_LENGTH) { throw new AlertException( AlertProtocol.INTERNAL_ERROR, new SSLProtocolException( "The provided chunk of data is too big: " + len + " > MAX_DATA_LENGTH == "+MAX_DATA_LENGTH)); } byte[] ciphered_fragment = fragment; if (activeWriteState != null) { ciphered_fragment = activeWriteState.encrypt(content_type, fragment, offset, len); if (ciphered_fragment.length > MAX_CIPHERED_DATA_LENGTH) { throw new AlertException( AlertProtocol.INTERNAL_ERROR, new SSLProtocolException( "The ciphered data increased more than on 1024 bytes")); } if (logger != null) { logger.println("SSLRecordProtocol.wrap: TLSCiphertext.fragment[" +ciphered_fragment.length+"]:"); logger.print(ciphered_fragment); } } return packetize(content_type, version, ciphered_fragment); } private byte[] packetize(byte type, byte[] version, byte[] fragment) { byte[] buff = new byte[5+fragment.length]; buff[0] = type; if (version != null) { buff[1] = version[0]; buff[2] = version[1]; } else { buff[1] = 3; buff[2] = 1; } buff[3] = (byte) ((0x00FF00 & fragment.length) >> 8); buff[4] = (byte) (0x0000FF & fragment.length); System.arraycopy(fragment, 0, buff, 5, fragment.length); return buff; } /** * Set the ssl session to be used after sending the changeCipherSpec message * @param session: SSLSessionImpl */ private void setSession(SSLSessionImpl session) { if (!sessionWasChanged) { // session was not changed for current handshake process if (logger != null) { logger.println("SSLRecordProtocol.setSession: Set pending session"); logger.println(" cipher name: " + session.getCipherSuite()); } this.session = session; // create new connection state pendingConnectionState = ((version == null) || (version[1] == 1)) ? (ConnectionState) new ConnectionStateTLS(getSession()) : (ConnectionState) new ConnectionStateSSLv3(getSession()); sessionWasChanged = true; } else { // wait for rehandshaking's session sessionWasChanged = false; } } /** * Returns the change cipher spec message to be sent to another peer. * The pending connection state will be built on the base of provided * session object * The calling of this method triggers pending write connection state to * be active. * @return ssl record containing the "change cipher spec" message. */ protected byte[] getChangeCipherSpecMesage(SSLSessionImpl session) { // make change_cipher_spec_message: byte[] change_cipher_spec_message; if (activeWriteState == null) { change_cipher_spec_message = new byte[] { ContentType.CHANGE_CIPHER_SPEC, version[0], version[1], 0, 1, 1 }; } else { change_cipher_spec_message = packetize(ContentType.CHANGE_CIPHER_SPEC, version, activeWriteState.encrypt(ContentType.CHANGE_CIPHER_SPEC, change_cipher_spec_byte, 0, 1)); } setSession(session); activeWriteState = pendingConnectionState; if (logger != null) { logger.println("SSLRecordProtocol.getChangeCipherSpecMesage"); logger.println("activeWriteState = pendingConnectionState"); logger.print(change_cipher_spec_message); } return change_cipher_spec_message; } /** * Retrieves the fragment field of TLSCiphertext, and than * depending on the established Connection State * decrypts and decompresses it. The following structure is expected * on the input at the moment of the call: * * struct { * ContentType type; * ProtocolVersion version; * uint16 length; * select (CipherSpec.cipher_type) { * case stream: GenericStreamCipher; * case block: GenericBlockCipher; * } fragment; * } TLSCiphertext; * * (as specified by RFC 2246, TLS v1 Protocol specification) * * In addition this method can recognize SSLv2 hello message which * are often used to establish the SSL/TLS session. * * @throws IOException if some io errors have been occurred * @throws EndOfSourceException if underlying input stream * has ran out of data. * @throws EndOfBufferException if there was not enough data * to build complete ssl packet. * @return the type of unwrapped message. */ protected int unwrap() throws IOException { if (logger != null) { logger.println("SSLRecordProtocol.unwrap: BEGIN ["); } int type = in.readUint8(); if ((type < ContentType.CHANGE_CIPHER_SPEC) || (type > ContentType.APPLICATION_DATA)) { if (logger != null) { logger.println("Non v3.1 message type:" + type); } if (type >= 0x80) { // it is probably SSL v2 client_hello message // (see SSL v2 spec at: // http://wp.netscape.com/eng/security/SSL_2.html) int length = (type & 0x7f) << 8 | in.read(); byte[] fragment = in.read(length); handshakeProtocol.unwrapSSLv2(fragment); if (logger != null) { logger.println( "SSLRecordProtocol:unwrap ] END, SSLv2 type"); } return ContentType.HANDSHAKE; } throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE, new SSLProtocolException( "Unexpected message type has been received: "+type)); } if (logger != null) { logger.println("Got the message of type: " + type); } if (version != null) { if ((in.read() != version[0]) || (in.read() != version[1])) { throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE, new SSLProtocolException( "Unexpected message type has been received: " + type)); } } else { in.skip(2); // just skip the version number } int length = in.readUint16(); if (logger != null) { logger.println("TLSCiphertext.fragment["+length+"]: ..."); } if (length > MAX_CIPHERED_DATA_LENGTH) { throw new AlertException(AlertProtocol.RECORD_OVERFLOW, new SSLProtocolException( "Received message is too big.")); } byte[] fragment = in.read(length); if (logger != null) { logger.print(fragment); } if (activeReadState != null) { fragment = activeReadState.decrypt((byte) type, fragment); if (logger != null) { logger.println("TLSPlaintext.fragment:"); logger.print(fragment); } } if (fragment.length > MAX_DATA_LENGTH) { throw new AlertException(AlertProtocol.DECOMPRESSION_FAILURE, new SSLProtocolException( "Decompressed plain data is too big.")); } switch (type) { case ContentType.CHANGE_CIPHER_SPEC: // notify handshake protocol: handshakeProtocol.receiveChangeCipherSpec(); setSession(handshakeProtocol.getSession()); // change cipher spec message has been received, so: if (logger != null) { logger.println("activeReadState = pendingConnectionState"); } activeReadState = pendingConnectionState; break; case ContentType.ALERT: alert(fragment[0], fragment[1]); break; case ContentType.HANDSHAKE: handshakeProtocol.unwrap(fragment); break; case ContentType.APPLICATION_DATA: if (logger != null) { logger.println( "TLSCiphertext.unwrap: APP DATA["+length+"]:"); logger.println(new String(fragment)); } appData.append(fragment); break; default: throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE, new SSLProtocolException( "Unexpected message type has been received: " + type)); } if (logger != null) { logger.println("SSLRecordProtocol:unwrap ] END, type: " + type); } return type; } /** * Passes the alert information to the alert protocol. * @param level: byte * @param description: byte */ protected void alert(byte level, byte description) { if (logger != null) { logger.println("SSLRecordProtocol.allert: "+level+" "+description); } alertProtocol.alert(level, description); } /** * Sets up the SSL version used in this connection. * This method is calling from the handshake protocol after * it becomes known which protocol version will be used. * @param ver: byte[] * @return */ protected void setVersion(byte[] ver) { this.version = ver; } /** * Shuts down the protocol. It will be impossible to use the instance * after the calling of this method. */ protected void shutdown() { session = null; version = null; in = null; handshakeProtocol = null; alertProtocol = null; appData = null; if (pendingConnectionState != null) { pendingConnectionState.shutdown(); } pendingConnectionState = null; if (activeReadState != null) { activeReadState.shutdown(); } activeReadState = null; if (activeReadState != null) { activeReadState.shutdown(); } activeWriteState = null; } }