/*
* RED5 Open Source Flash Server - http://code.google.com/p/red5/
*
* Copyright 2006-2014 by respective authors (see below). All rights reserved.
*
* Licensed 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.red5.server.net.rtmp;
import java.security.KeyPair;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
import org.apache.mina.core.buffer.IoBuffer;
import org.red5.server.api.Red5;
import org.red5.server.net.rtmp.message.Constants;
/**
* Performs handshaking for server connections.
*
* @author Paul Gregoire
*/
public class InboundHandshake extends RTMPHandshake {
public InboundHandshake() {
super();
}
/**
* Generates response for versioned connections.
*
* @param input incoming RTMP bytes
* @return outgoing handshake
*/
public IoBuffer doHandshake(IoBuffer input) {
log.trace("doHandshake: {}", input);
if (log.isDebugEnabled()) {
log.debug("Player encryption byte: {}", handshakeType);
byte[] bIn = input.array();
log.debug("Detecting flash player version {},{},{},{}", new Object[]{(bIn[4] & 0x0ff), (bIn[5] & 0x0ff), (bIn[6] & 0x0ff), (bIn[7] & 0x0ff)});
//if the 5th byte is 0 then dont generate new-style handshake
if (log.isTraceEnabled()) {
log.trace("First few bytes (in): {},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", new Object[] {
bIn[0], bIn[1], bIn[2], bIn[3], bIn[4],
bIn[5], bIn[6], bIn[7], bIn[8], bIn[9],
bIn[10], bIn[11], bIn[12], bIn[13], bIn[14],
bIn[15] });
//client version hex
byte[] ver = new byte[4];
System.arraycopy(bIn, 4, ver, 0, 4);
log.trace("Version string: {}", Hex.encodeHexString(ver));
//dump
byte[] buf = new byte[KEY_LENGTH];
System.arraycopy(bIn, 0, buf, 0, KEY_LENGTH);
log.trace("Hex: {}", Hex.encodeHexString(buf));
}
}
input.mark();
byte versionByte = input.get(4);
log.debug("Player version byte: {}", (versionByte & 0x0ff));
input.reset();
if (versionByte == 0) {
return generateUnversionedHandshake(input);
}
//create output buffer
IoBuffer output = IoBuffer.allocate(HANDSHAKE_SIZE_SERVER);
input.mark();
//make sure this is a client we can communicate with
if (validate(input)) {
log.debug("Valid RTMP client detected");
} else {
log.info("Invalid RTMP connection data detected, you may experience errors");
}
input.reset();
log.debug("Using new style handshake");
input.mark();
//create all the dh stuff and add to handshake bytes
prepareResponse(input);
input.reset();
if (handshakeType == RTMPConnection.RTMP_ENCRYPTED) {
log.debug("Incoming public key [{}]: {}", incomingPublicKey.length, Hex.encodeHexString(incomingPublicKey));
log.debug("Outgoing public key [{}]: {}", outgoingPublicKey.length, Hex.encodeHexString(outgoingPublicKey));
byte[] sharedSecret = getSharedSecret(outgoingPublicKey, keyAgreement);
// create output cipher
byte[] digestOut = calculateHMAC_SHA256(outgoingPublicKey, sharedSecret);
try {
cipherOut = Cipher.getInstance("RC4");
cipherOut.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(digestOut, 0, 16, "RC4"));
} catch (Exception e) {
log.warn("Encryption cipher creation failed", e);
}
// create input cipher
byte[] digestIn = calculateHMAC_SHA256(incomingPublicKey, sharedSecret);
try {
cipherIn = Cipher.getInstance("RC4");
cipherIn.init(Cipher.DECRYPT_MODE, new SecretKeySpec(digestIn, 0, 16, "RC4"));
} catch (Exception e) {
log.warn("Decryption cipher creation failed", e);
}
// update 'encoder / decoder state' for the RC4 keys
// both parties *pretend* as if handshake part 2 (1536 bytes) was encrypted
// effectively this hides / discards the first few bytes of encrypted session
// which is known to increase the secure-ness of RC4
// RC4 state is just a function of number of bytes processed so far
// that's why we just run 1536 arbitrary bytes through the keys below
byte[] dummyBytes = new byte[Constants.HANDSHAKE_SIZE];
cipherIn.update(dummyBytes);
cipherOut.update(dummyBytes);
}
input.mark();
//create the server digest
int serverDigestOffset = getDigestOffset(handshakeBytes);
byte[] tempBuffer = new byte[Constants.HANDSHAKE_SIZE - DIGEST_LENGTH];
System.arraycopy(handshakeBytes, 0, tempBuffer, 0, serverDigestOffset);
System.arraycopy(handshakeBytes, serverDigestOffset + DIGEST_LENGTH, tempBuffer, serverDigestOffset, Constants.HANDSHAKE_SIZE - serverDigestOffset - DIGEST_LENGTH);
//calculate the hash
byte[] tempHash = calculateHMAC_SHA256(tempBuffer, GENUINE_FMS_KEY, 36);
//add the digest
System.arraycopy(tempHash, 0, handshakeBytes, serverDigestOffset, DIGEST_LENGTH);
//compute the challenge digest
byte[] inputBuffer = new byte[Constants.HANDSHAKE_SIZE - DIGEST_LENGTH];
//log.debug("Before get: {}", input.position());
input.get(inputBuffer);
//log.debug("After get: {}", input.position());
int keyChallengeIndex = getDigestOffset(inputBuffer);
byte[] challengeKey = new byte[DIGEST_LENGTH];
input.position(keyChallengeIndex);
input.get(challengeKey, 0, DIGEST_LENGTH);
input.reset();
//compute key
tempHash = calculateHMAC_SHA256(challengeKey, GENUINE_FMS_KEY, 68);
//generate hash
byte[] randBytes = new byte[Constants.HANDSHAKE_SIZE - DIGEST_LENGTH];
random.nextBytes(randBytes);
byte[] lastHash = calculateHMAC_SHA256(randBytes, tempHash, DIGEST_LENGTH);
//set handshake with encryption type
output.put(handshakeType);
output.put(handshakeBytes);
output.put(randBytes);
output.put(lastHash);
output.flip();
if (log.isTraceEnabled()) {
byte[] bOut = output.array();
log.trace("First few bytes (out): {},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}", new Object[] {
bOut[0], bOut[1], bOut[2], bOut[3], bOut[4],
bOut[5], bOut[6], bOut[7], bOut[8], bOut[9],
bOut[10], bOut[11], bOut[12], bOut[13], bOut[14],
bOut[15]});
byte[] buf = new byte[KEY_LENGTH];
System.arraycopy(bOut, 0, buf, 0, KEY_LENGTH);
log.trace("Hex: {}", Hex.encodeHexString(buf));
}
return output;
}
/**
* Generates response for non-versioned connections, such as those before FP9.
*
* @param input incoming RTMP bytes
* @return outgoing handshake
*/
private IoBuffer generateUnversionedHandshake(IoBuffer input) {
log.debug("Using old style (un-versioned) handshake");
//save resource by only doing this after the first request
if (HANDSHAKE_PAD_BYTES == null) {
HANDSHAKE_PAD_BYTES = new byte[Constants.HANDSHAKE_SIZE - 4];
//fill pad bytes
Arrays.fill(HANDSHAKE_PAD_BYTES, (byte) 0x00);
}
IoBuffer output = IoBuffer.allocate(HANDSHAKE_SIZE_SERVER);
//non-encrypted
output.put(RTMPConnection.RTMP_NON_ENCRYPTED);
//set server uptime in seconds
output.putInt((int) Red5.getUpTime() / 1000); //0x01
output.put(RTMPHandshake.HANDSHAKE_PAD_BYTES);
output.put(input);
output.flip();
return output;
}
/**
* Creates the servers handshake bytes
*/
@Override
protected void createHandshakeBytes() {
handshakeBytes = new byte[Constants.HANDSHAKE_SIZE];
//timestamp
handshakeBytes[0] = 0;
handshakeBytes[1] = 0;
handshakeBytes[2] = 0;
handshakeBytes[3] = 0;
//version (0x01020304)
handshakeBytes[4] = 1;
handshakeBytes[5] = 2;
handshakeBytes[6] = 3;
handshakeBytes[7] = 4;
//fill the rest with random bytes
byte[] rndBytes = new byte[Constants.HANDSHAKE_SIZE - 8];
random.nextBytes(rndBytes);
//copy random bytes into our handshake array
System.arraycopy(rndBytes, 0, handshakeBytes, 8, (Constants.HANDSHAKE_SIZE - 8));
}
/**
* Gets the DH offset in the handshake bytes array based on validation scheme
* Generates DH keypair
* Adds public key to handshake bytes
* @param input
*/
private void prepareResponse(IoBuffer input) {
//put the clients input into a byte array
byte[] inputBuffer = new byte[input.limit()];
input.get(inputBuffer);
//get the clients dh offset
int clientDHOffset = getDHOffset(inputBuffer);
log.trace("Incoming DH offset: {}", clientDHOffset);
//get the clients public key
outgoingPublicKey = new byte[KEY_LENGTH];
System.arraycopy(inputBuffer, clientDHOffset, outgoingPublicKey, 0, KEY_LENGTH);
//get the servers dh offset
int serverDHOffset = getDHOffset(handshakeBytes);
log.trace("Outgoing DH offset: {}", serverDHOffset);
//create keypair
KeyPair keys = generateKeyPair();
//get public key
incomingPublicKey = getPublicKey(keys);
//add to handshake bytes
System.arraycopy(incomingPublicKey, 0, handshakeBytes, serverDHOffset, KEY_LENGTH);
}
/**
* Determines the validation scheme for given input.
*
* @param input
* @return true if client used a supported validation scheme, false if unsupported
*/
@Override
public boolean validate(IoBuffer input) {
byte[] pBuffer = new byte[input.remaining()];
//put all the input bytes into our buffer
input.get(pBuffer, 0, input.remaining());
if (validateScheme(pBuffer, 0)) {
validationScheme = 0;
log.debug("Selected scheme: 0");
return true;
}
if (validateScheme(pBuffer, 1)) {
validationScheme = 1;
log.debug("Selected scheme: 1");
return true;
}
log.error("Unable to validate client");
return false;
}
private boolean validateScheme(byte[] pBuffer, int scheme) {
int digestOffset = -1;
switch (scheme) {
case 0:
digestOffset = getDigestOffset0(pBuffer);
break;
case 1:
digestOffset = getDigestOffset1(pBuffer);
break;
default:
log.error("Unknown scheme: {}", scheme);
}
log.debug("Scheme: {} client digest offset: {}", scheme, digestOffset);
byte[] tempBuffer = new byte[Constants.HANDSHAKE_SIZE - DIGEST_LENGTH];
System.arraycopy(pBuffer, 0, tempBuffer, 0, digestOffset);
System.arraycopy(pBuffer, digestOffset + DIGEST_LENGTH, tempBuffer, digestOffset, Constants.HANDSHAKE_SIZE - digestOffset - DIGEST_LENGTH);
byte[] tempHash = calculateHMAC_SHA256(tempBuffer, GENUINE_FP_KEY, 30);
log.debug("Temp: {}", Hex.encodeHexString(tempHash));
boolean result = true;
for (int i = 0; i < DIGEST_LENGTH; i++) {
//log.trace("Digest: {} Temp: {}", (pBuffer[digestOffset + i] & 0x0ff), (tempHash[i] & 0x0ff));
if (pBuffer[digestOffset + i] != tempHash[i]) {
result = false;
break;
}
}
return result;
}
}