blob: cba13d51b49650f6177f2160ff6c84a36750b3b6 [file] [log] [blame]
package org.bouncycastle.crypto.tls;
import java.io.IOException;
class DTLSRecordLayer
implements DatagramTransport
{
private static final int RECORD_HEADER_LENGTH = 13;
private static final int MAX_FRAGMENT_LENGTH = 1 << 14;
private static final long TCP_MSL = 1000L * 60 * 2;
private static final long RETRANSMIT_TIMEOUT = TCP_MSL * 2;
private final DatagramTransport transport;
private final TlsContext context;
private final TlsPeer peer;
private final ByteQueue recordQueue = new ByteQueue();
private volatile boolean closed = false;
private volatile boolean failed = false;
private volatile ProtocolVersion discoveredPeerVersion = null;
private volatile boolean inHandshake;
private volatile int plaintextLimit;
private DTLSEpoch currentEpoch, pendingEpoch;
private DTLSEpoch readEpoch, writeEpoch;
private DTLSHandshakeRetransmit retransmit = null;
private DTLSEpoch retransmitEpoch = null;
private long retransmitExpiry = 0;
DTLSRecordLayer(DatagramTransport transport, TlsContext context, TlsPeer peer, short contentType)
{
this.transport = transport;
this.context = context;
this.peer = peer;
this.inHandshake = true;
this.currentEpoch = new DTLSEpoch(0, new TlsNullCipher(context));
this.pendingEpoch = null;
this.readEpoch = currentEpoch;
this.writeEpoch = currentEpoch;
setPlaintextLimit(MAX_FRAGMENT_LENGTH);
}
void setPlaintextLimit(int plaintextLimit)
{
this.plaintextLimit = plaintextLimit;
}
ProtocolVersion getDiscoveredPeerVersion()
{
return discoveredPeerVersion;
}
ProtocolVersion resetDiscoveredPeerVersion()
{
ProtocolVersion result = discoveredPeerVersion;
discoveredPeerVersion = null;
return result;
}
void initPendingEpoch(TlsCipher pendingCipher)
{
if (pendingEpoch != null)
{
throw new IllegalStateException();
}
/*
* TODO "In order to ensure that any given sequence/epoch pair is unique, implementations
* MUST NOT allow the same epoch value to be reused within two times the TCP maximum segment
* lifetime."
*/
// TODO Check for overflow
this.pendingEpoch = new DTLSEpoch(writeEpoch.getEpoch() + 1, pendingCipher);
}
void handshakeSuccessful(DTLSHandshakeRetransmit retransmit)
{
if (readEpoch == currentEpoch || writeEpoch == currentEpoch)
{
// TODO
throw new IllegalStateException();
}
if (retransmit != null)
{
this.retransmit = retransmit;
this.retransmitEpoch = currentEpoch;
this.retransmitExpiry = System.currentTimeMillis() + RETRANSMIT_TIMEOUT;
}
this.inHandshake = false;
this.currentEpoch = pendingEpoch;
this.pendingEpoch = null;
}
void resetWriteEpoch()
{
if (retransmitEpoch != null)
{
this.writeEpoch = retransmitEpoch;
}
else
{
this.writeEpoch = currentEpoch;
}
}
public int getReceiveLimit()
throws IOException
{
return Math.min(this.plaintextLimit,
readEpoch.getCipher().getPlaintextLimit(transport.getReceiveLimit() - RECORD_HEADER_LENGTH));
}
public int getSendLimit()
throws IOException
{
return Math.min(this.plaintextLimit,
writeEpoch.getCipher().getPlaintextLimit(transport.getSendLimit() - RECORD_HEADER_LENGTH));
}
public int receive(byte[] buf, int off, int len, int waitMillis)
throws IOException
{
byte[] record = null;
for (;;)
{
int receiveLimit = Math.min(len, getReceiveLimit()) + RECORD_HEADER_LENGTH;
if (record == null || record.length < receiveLimit)
{
record = new byte[receiveLimit];
}
try
{
if (retransmit != null && System.currentTimeMillis() > retransmitExpiry)
{
retransmit = null;
retransmitEpoch = null;
}
int received = receiveRecord(record, 0, receiveLimit, waitMillis);
if (received < 0)
{
return received;
}
if (received < RECORD_HEADER_LENGTH)
{
continue;
}
int length = TlsUtils.readUint16(record, 11);
if (received != (length + RECORD_HEADER_LENGTH))
{
continue;
}
short type = TlsUtils.readUint8(record, 0);
// TODO Support user-specified custom protocols?
switch (type)
{
case ContentType.alert:
case ContentType.application_data:
case ContentType.change_cipher_spec:
case ContentType.handshake:
case ContentType.heartbeat:
break;
default:
// TODO Exception?
continue;
}
int epoch = TlsUtils.readUint16(record, 3);
DTLSEpoch recordEpoch = null;
if (epoch == readEpoch.getEpoch())
{
recordEpoch = readEpoch;
}
else if (type == ContentType.handshake && retransmitEpoch != null
&& epoch == retransmitEpoch.getEpoch())
{
recordEpoch = retransmitEpoch;
}
if (recordEpoch == null)
{
continue;
}
long seq = TlsUtils.readUint48(record, 5);
if (recordEpoch.getReplayWindow().shouldDiscard(seq))
{
continue;
}
ProtocolVersion version = TlsUtils.readVersion(record, 1);
if (discoveredPeerVersion != null && !discoveredPeerVersion.equals(version))
{
continue;
}
byte[] plaintext = recordEpoch.getCipher().decodeCiphertext(
getMacSequenceNumber(recordEpoch.getEpoch(), seq), type, record, RECORD_HEADER_LENGTH,
received - RECORD_HEADER_LENGTH);
recordEpoch.getReplayWindow().reportAuthenticated(seq);
if (plaintext.length > this.plaintextLimit)
{
continue;
}
if (discoveredPeerVersion == null)
{
discoveredPeerVersion = version;
}
switch (type)
{
case ContentType.alert:
{
if (plaintext.length == 2)
{
short alertLevel = plaintext[0];
short alertDescription = plaintext[1];
peer.notifyAlertReceived(alertLevel, alertDescription);
if (alertLevel == AlertLevel.fatal)
{
fail(alertDescription);
throw new TlsFatalAlert(alertDescription);
}
// TODO Can close_notify be a fatal alert?
if (alertDescription == AlertDescription.close_notify)
{
closeTransport();
}
}
else
{
// TODO What exception?
}
continue;
}
case ContentType.application_data:
{
if (inHandshake)
{
// TODO Consider buffering application data for new epoch that arrives
// out-of-order with the Finished message
continue;
}
break;
}
case ContentType.change_cipher_spec:
{
// Implicitly receive change_cipher_spec and change to pending cipher state
for (int i = 0; i < plaintext.length; ++i)
{
short message = TlsUtils.readUint8(plaintext, i);
if (message != ChangeCipherSpec.change_cipher_spec)
{
continue;
}
if (pendingEpoch != null)
{
readEpoch = pendingEpoch;
}
}
continue;
}
case ContentType.handshake:
{
if (!inHandshake)
{
if (retransmit != null)
{
retransmit.receivedHandshakeRecord(epoch, plaintext, 0, plaintext.length);
}
// TODO Consider support for HelloRequest
continue;
}
break;
}
case ContentType.heartbeat:
{
// TODO[RFC 6520]
continue;
}
}
/*
* NOTE: If we receive any non-handshake data in the new epoch implies the peer has
* received our final flight.
*/
if (!inHandshake && retransmit != null)
{
this.retransmit = null;
this.retransmitEpoch = null;
}
System.arraycopy(plaintext, 0, buf, off, plaintext.length);
return plaintext.length;
}
catch (IOException e)
{
// NOTE: Assume this is a timeout for the moment
throw e;
}
}
}
public void send(byte[] buf, int off, int len)
throws IOException
{
short contentType = ContentType.application_data;
if (this.inHandshake || this.writeEpoch == this.retransmitEpoch)
{
contentType = ContentType.handshake;
short handshakeType = TlsUtils.readUint8(buf, off);
if (handshakeType == HandshakeType.finished)
{
DTLSEpoch nextEpoch = null;
if (this.inHandshake)
{
nextEpoch = pendingEpoch;
}
else if (this.writeEpoch == this.retransmitEpoch)
{
nextEpoch = currentEpoch;
}
if (nextEpoch == null)
{
// TODO
throw new IllegalStateException();
}
// Implicitly send change_cipher_spec and change to pending cipher state
// TODO Send change_cipher_spec and finished records in single datagram?
byte[] data = new byte[]{ 1 };
sendRecord(ContentType.change_cipher_spec, data, 0, data.length);
writeEpoch = nextEpoch;
}
}
sendRecord(contentType, buf, off, len);
}
public void close()
throws IOException
{
if (!closed)
{
if (inHandshake)
{
warn(AlertDescription.user_canceled, "User canceled handshake");
}
closeTransport();
}
}
void fail(short alertDescription)
{
if (!closed)
{
try
{
raiseAlert(AlertLevel.fatal, alertDescription, null, null);
}
catch (Exception e)
{
// Ignore
}
failed = true;
closeTransport();
}
}
void warn(short alertDescription, String message)
throws IOException
{
raiseAlert(AlertLevel.warning, alertDescription, message, null);
}
private void closeTransport()
{
if (!closed)
{
/*
* RFC 5246 7.2.1. Unless some other fatal alert has been transmitted, each party is
* required to send a close_notify alert before closing the write side of the
* connection. The other party MUST respond with a close_notify alert of its own and
* close down the connection immediately, discarding any pending writes.
*/
try
{
if (!failed)
{
warn(AlertDescription.close_notify, null);
}
transport.close();
}
catch (Exception e)
{
// Ignore
}
closed = true;
}
}
private void raiseAlert(short alertLevel, short alertDescription, String message, Exception cause)
throws IOException
{
peer.notifyAlertRaised(alertLevel, alertDescription, message, cause);
byte[] error = new byte[2];
error[0] = (byte)alertLevel;
error[1] = (byte)alertDescription;
sendRecord(ContentType.alert, error, 0, 2);
}
private int receiveRecord(byte[] buf, int off, int len, int waitMillis)
throws IOException
{
if (recordQueue.size() > 0)
{
int length = 0;
if (recordQueue.size() >= RECORD_HEADER_LENGTH)
{
byte[] lengthBytes = new byte[2];
recordQueue.read(lengthBytes, 0, 2, 11);
length = TlsUtils.readUint16(lengthBytes, 0);
}
int received = Math.min(recordQueue.size(), RECORD_HEADER_LENGTH + length);
recordQueue.removeData(buf, off, received, 0);
return received;
}
int received = transport.receive(buf, off, len, waitMillis);
if (received >= RECORD_HEADER_LENGTH)
{
int fragmentLength = TlsUtils.readUint16(buf, off + 11);
int recordLength = RECORD_HEADER_LENGTH + fragmentLength;
if (received > recordLength)
{
recordQueue.addData(buf, off + recordLength, received - recordLength);
received = recordLength;
}
}
return received;
}
private void sendRecord(short contentType, byte[] buf, int off, int len)
throws IOException
{
if (len > this.plaintextLimit)
{
throw new TlsFatalAlert(AlertDescription.internal_error);
}
/*
* RFC 5264 6.2.1 Implementations MUST NOT send zero-length fragments of Handshake, Alert,
* or ChangeCipherSpec content types.
*/
if (len < 1 && contentType != ContentType.application_data)
{
throw new TlsFatalAlert(AlertDescription.internal_error);
}
int recordEpoch = writeEpoch.getEpoch();
long recordSequenceNumber = writeEpoch.allocateSequenceNumber();
byte[] ciphertext = writeEpoch.getCipher().encodePlaintext(
getMacSequenceNumber(recordEpoch, recordSequenceNumber), contentType, buf, off, len);
// TODO Check the ciphertext length?
byte[] record = new byte[ciphertext.length + RECORD_HEADER_LENGTH];
TlsUtils.writeUint8(contentType, record, 0);
ProtocolVersion version = discoveredPeerVersion != null ? discoveredPeerVersion : context.getClientVersion();
TlsUtils.writeVersion(version, record, 1);
TlsUtils.writeUint16(recordEpoch, record, 3);
TlsUtils.writeUint48(recordSequenceNumber, record, 5);
TlsUtils.writeUint16(ciphertext.length, record, 11);
System.arraycopy(ciphertext, 0, record, RECORD_HEADER_LENGTH, ciphertext.length);
transport.send(record, 0, record.length);
}
private static long getMacSequenceNumber(int epoch, long sequence_number)
{
return ((long)epoch << 48) | sequence_number;
}
}