blob: e9f77f72a82040daeb5cfa00beabbbb81ed537b2 [file] [log] [blame]
/*
* 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;
}
}