/* * RED5 Open Source Flash Server - https://github.com/Red5/ * * Copyright 2006-2015 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.client.net.rtmp; import java.io.File; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.KeyPair; import java.util.Arrays; import org.apache.commons.codec.binary.Hex; import org.apache.mina.core.buffer.IoBuffer; import org.bouncycastle.util.BigIntegers; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.rtmp.RTMPHandshake; import org.red5.server.net.rtmp.message.Constants; import org.red5.server.util.FileUtil; import org.slf4j.LoggerFactory; /** * Performs handshaking for client connections. * * @author Paul Gregoire */ public class OutboundHandshake extends RTMPHandshake { private byte[] outgoingDigest = new byte[DIGEST_LENGTH]; private byte[] incomingDigest = new byte[DIGEST_LENGTH]; private byte[] swfHash; private int digestPosClient; private int digestPosServer; // client initial request C1 private byte[] c1 = null; // server initial response S1 private byte[] s1 = null; // whether or not verification is mandatory private boolean forceVerification; public OutboundHandshake() { super(RTMPConnection.RTMP_NON_ENCRYPTED); log = LoggerFactory.getLogger(OutboundHandshake.class); } public OutboundHandshake(byte handshakeType) { super(handshakeType); log = LoggerFactory.getLogger(OutboundHandshake.class); } public OutboundHandshake(byte handshakeType, int algorithm) { this(handshakeType); this.algorithm = algorithm; } @Override public IoBuffer doHandshake(IoBuffer input) { throw new UnsupportedOperationException("Not used, call server response decoders directly"); } /** * Creates the servers handshake bytes */ @Override protected void createHandshakeBytes() { log.trace("createHandshakeBytes"); BigInteger bi = new BigInteger((Constants.HANDSHAKE_SIZE * 8), random); handshakeBytes = BigIntegers.asUnsignedByteArray(bi); // prevent AOOB error that can occur, sometimes if (handshakeBytes.length < Constants.HANDSHAKE_SIZE) { // resize the handshake bytes ByteBuffer b = ByteBuffer.allocate(Constants.HANDSHAKE_SIZE); b.put(handshakeBytes); b.put((byte) 0x13); b.flip(); handshakeBytes = b.array(); } } /** * Create the first part of the outgoing connection request (C0 and C1). * <pre> * C0 = 0x03 (client handshake type - 0x03, 0x06, 0x08, or 0x09) * C1 = 1536 bytes from the client * </pre> * @return outgoing handshake C0+C1 */ public IoBuffer generateClientRequest1() { log.debug("generateClientRequest1"); IoBuffer request = IoBuffer.allocate(Constants.HANDSHAKE_SIZE + 1); // set the handshake type byte request.put(handshakeType); if (useEncryption() || swfSize > 0) { fp9Handshake = true; algorithm = 1; } else { //fp9Handshake = false; } // timestamp int time = 5; handshakeBytes[0] = (byte) (time >>> 24); handshakeBytes[1] = (byte) (time >>> 16); handshakeBytes[2] = (byte) (time >>> 8); handshakeBytes[3] = (byte) time; if (fp9Handshake) { // flash player version > 9.0.115.0 handshakeBytes[4] = (byte) 0x80; handshakeBytes[5] = 0; handshakeBytes[6] = 7; handshakeBytes[7] = 2; } else { log.debug("Using pre-version 9.0.115.0 handshake"); handshakeBytes[4] = 0; handshakeBytes[5] = 0; handshakeBytes[6] = 0; handshakeBytes[7] = 0; } if (log.isTraceEnabled()) { log.trace("Time and version handshake bytes: {}", Hex.encodeHexString(Arrays.copyOf(handshakeBytes, 8))); } // get the handshake digest c1 = new byte[Constants.HANDSHAKE_SIZE]; if (fp9Handshake) { // handle encryption setup if (useEncryption()) { // create keypair KeyPair keys = generateKeyPair(); // get public key outgoingPublicKey = getPublicKey(keys); log.debug("Client public key: {}", Hex.encodeHexString(outgoingPublicKey)); // get the DH offset in the handshake bytes int clientDHOffset = getDHOffset(algorithm, handshakeBytes, 0); log.trace("Outgoing DH offset: {}", clientDHOffset); // adds the public key to handshake bytes System.arraycopy(outgoingPublicKey, 0, handshakeBytes, clientDHOffset, KEY_LENGTH); // perform special processing for each type if needed switch (handshakeType) { case RTMPConnection.RTMP_ENCRYPTED: break; case RTMPConnection.RTMP_ENCRYPTED_XTEA: break; case RTMPConnection.RTMP_ENCRYPTED_BLOWFISH: break; } } digestPosClient = getDigestOffset(algorithm, handshakeBytes, 0); log.debug("Client digest position offset: {} algorithm: {}", digestPosClient, algorithm); System.arraycopy(handshakeBytes, 0, c1, 0, Constants.HANDSHAKE_SIZE); calculateDigest(digestPosClient, handshakeBytes, 0, GENUINE_FP_KEY, 30, c1, digestPosClient); // local storage of outgoing digest System.arraycopy(c1, digestPosClient, outgoingDigest, 0, DIGEST_LENGTH); log.debug("Client digest: {}", Hex.encodeHexString(outgoingDigest)); log.debug("Digest is valid: {}", verifyDigest(digestPosClient, c1, RTMPHandshake.GENUINE_FP_KEY, 30)); } if (log.isTraceEnabled()) { log.trace("C1: {}", Hex.encodeHexString(c1)); } // put the generated data into our request request.put(c1); request.flip(); // clear original base bytes handshakeBytes = null; return request; } /** * Decodes the first server response (S1) and returns a client response (C2). * <pre> * S1 = 1536 bytes from the server * C2 = Copy of S1 bytes * </pre> * @param in incoming handshake S1 * @return client response C2 */ public IoBuffer decodeServerResponse1(IoBuffer in) { log.debug("decodeServerResponse1"); IoBuffer response = null; // the handshake type byte is not included if (in.hasArray()) { s1 = in.array(); } else { s1 = new byte[Constants.HANDSHAKE_SIZE]; in.get(s1); } //if (log.isTraceEnabled()) { // log.trace("S1: {}", Hex.encodeHexString(serverSig)); //} if (log.isDebugEnabled()) { log.debug("Server version {}", Hex.encodeHexString(Arrays.copyOfRange(s1, 4, 8))); } // skip key / digest stuff if we're not doing any encryption or server says it doesnt support it if (fp9Handshake && handshakeType == RTMPConnection.RTMP_NON_ENCRYPTED && s1[4] == 0) { log.debug("Switching to pre-fp9 handshake"); fp9Handshake = false; } if (fp9Handshake) { // make sure this is a client we can communicate with //if (validate(serverSig)) { // log.debug("Valid RTMP server detected, algorithm: {}", algorithm); //} else { // log.info("Invalid RTMP connection data detected, you may experience errors"); //} // get the server digest if (!getServerDigestPosition()) { return null; } // digest verification passed, store the digest locally System.arraycopy(s1, digestPosServer, incomingDigest, 0, DIGEST_LENGTH); log.debug("Server digest: {}", Hex.encodeHexString(incomingDigest)); // generate the SWF verification token if (swfSize > 0) { calculateSwfVerification(s1, swfHash, swfSize); } if (useEncryption()) { // get the DH offset in the handshake bytes int serverDHOffset = getDHOffset(algorithm, s1, 0); log.trace("Incoming DH offset: {}", serverDHOffset); // get the servers public key incomingPublicKey = new byte[KEY_LENGTH]; System.arraycopy(s1, serverDHOffset, incomingPublicKey, 0, KEY_LENGTH); log.debug("Server public key: {}", Hex.encodeHexString(incomingPublicKey)); // create the RC4 ciphers initRC4Encryption(getSharedSecret(incomingPublicKey, keyAgreement)); switch (handshakeType) { case RTMPConnection.RTMP_ENCRYPTED: // 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); break; case RTMPConnection.RTMP_ENCRYPTED_XTEA: break; case RTMPConnection.RTMP_ENCRYPTED_BLOWFISH: break; } } // create the response BigInteger bi = new BigInteger(Constants.HANDSHAKE_SIZE * 8, random); byte[] c2 = BigIntegers.asUnsignedByteArray(bi); // calculate response now byte[] signatureResp = new byte[DIGEST_LENGTH]; byte[] digestResp = new byte[DIGEST_LENGTH]; calculateHMAC_SHA256(s1, digestPosServer, DIGEST_LENGTH, GENUINE_FP_KEY, GENUINE_FP_KEY.length, digestResp, 0); calculateHMAC_SHA256(c2, 0, Constants.HANDSHAKE_SIZE - DIGEST_LENGTH, digestResp, DIGEST_LENGTH, signatureResp, 0); log.debug("Calculated digest key from secure key and server digest: {}", Hex.encodeHexString(digestResp)); // FP10 stuff if (handshakeType == RTMPConnection.RTMP_ENCRYPTED_XTEA) { log.debug("RTMPE type 8 XTEA"); // encrypt signatureResp for (int i = 0; i < DIGEST_LENGTH; i += 8) { //encryptXtea(signatureResp, i, digestResp[i] % 15); } } else if (handshakeType == RTMPConnection.RTMP_ENCRYPTED_BLOWFISH) { log.debug("RTMPE type 9 Blowfish"); // encrypt signatureResp for (int i = 0; i < DIGEST_LENGTH; i += 8) { //encryptBlowfish(signatureResp, i, digestResp[i] % 15); } } log.debug("Client signature calculated: {}", Hex.encodeHexString(signatureResp)); response = IoBuffer.allocate(Constants.HANDSHAKE_SIZE); response.put(c2, 0, Constants.HANDSHAKE_SIZE - DIGEST_LENGTH); response.put(signatureResp); response.flip(); } else { // send the server handshake back as a response response = IoBuffer.allocate(Constants.HANDSHAKE_SIZE); response.put(s1, 0, Constants.HANDSHAKE_SIZE); response.flip(); } // send the response return response; } /** * Decodes the second server response (S2). * <pre> * S2 = Copy of C1 bytes * </pre> * @param in incoming handshake S2 * @return true if validation passes and false otherwise */ public boolean decodeServerResponse2(IoBuffer in) { log.debug("decodeServerResponse2"); // the handshake type byte is not included byte[] s2; if (in.hasArray()) { s2 = in.array(); } else { s2 = new byte[Constants.HANDSHAKE_SIZE]; in.get(s2); } //if (log.isTraceEnabled()) { // log.trace("S2: {}", Hex.encodeHexString(s2)); //} if (fp9Handshake) { if (s2[4] == 0 && s2[5] == 0 && s2[6] == 0 && s2[7] == 0) { log.warn("Server refused signed authentication"); } // validate server response part 2, not really required for client byte[] signature = new byte[DIGEST_LENGTH]; byte[] digest = new byte[DIGEST_LENGTH]; calculateHMAC_SHA256(c1, digestPosClient, DIGEST_LENGTH, GENUINE_FMS_KEY, GENUINE_FMS_KEY.length, digest, 0); calculateHMAC_SHA256(s2, 0, Constants.HANDSHAKE_SIZE - DIGEST_LENGTH, digest, DIGEST_LENGTH, signature, 0); log.debug("Digest key: {}", Hex.encodeHexString(digest)); // FP10 stuff if (handshakeType == RTMPConnection.RTMP_ENCRYPTED_XTEA) { log.debug("RTMPE type 8 XTEA"); // encrypt signatureResp for (int i = 0; i < DIGEST_LENGTH; i += 8) { //encryptXtea(signature, i, digest[i] % 15); } } else if (handshakeType == RTMPConnection.RTMP_ENCRYPTED_BLOWFISH) { log.debug("RTMPE type 9 Blowfish"); // encrypt signatureResp for (int i = 0; i < DIGEST_LENGTH; i += 8) { //encryptBlowfish(signature, i, digest[i] % 15); } } log.debug("Signature calculated: {}", Hex.encodeHexString(signature)); log.debug("Server sent signature: {}", Hex.encodeHexString(s2)); if (!Arrays.equals(signature, Arrays.copyOfRange(s2, (Constants.HANDSHAKE_SIZE - DIGEST_LENGTH), (Constants.HANDSHAKE_SIZE - DIGEST_LENGTH) + DIGEST_LENGTH))) { log.info("Server not genuine"); return false; } else { log.debug("Compatible flash server"); } } else { if (!Arrays.equals(s2, c1)) { log.info("Client signature doesn't match!"); } } return true; } /** * Gets and verifies the server digest. * * @return true if the server digest is found and verified, false otherwise */ private boolean getServerDigestPosition() { boolean result = false; //log.trace("BigEndian bytes: {}", Hex.encodeHexString(s1)); log.trace("Trying algorithm: {}", algorithm); digestPosServer = getDigestOffset(algorithm, s1, 0); log.debug("Server digest position offset: {}", digestPosServer); if (!(result = verifyDigest(digestPosServer, s1, GENUINE_FMS_KEY, 36))) { // try a different position algorithm ^= 1; log.trace("Trying algorithm: {}", algorithm); digestPosServer = getDigestOffset(algorithm, s1, 0); log.debug("Server digest position offset: {}", digestPosServer); if (!(result = verifyDigest(digestPosServer, s1, GENUINE_FMS_KEY, 36))) { log.warn("Server digest verification failed"); // if we dont mind that verification routines failed if (!forceVerification) { return true; } } else { log.debug("Server digest verified"); } } else { log.debug("Server digest verified"); } return result; } /** * Determines the validation scheme for given input. * * @param input handshake bytes from the server * @return true if server used a supported validation scheme, false if unsupported */ @Override public boolean validate(byte[] handshake) { if (validateScheme(handshake, 0)) { algorithm = 0; return true; } if (validateScheme(handshake, 1)) { algorithm = 1; return true; } log.error("Unable to validate server"); return false; } private boolean validateScheme(byte[] handshake, int scheme) { int digestOffset = -1; switch (scheme) { case 0: digestOffset = getDigestOffset1(handshake, 0); break; case 1: digestOffset = getDigestOffset2(handshake, 0); break; default: log.error("Unknown algorithm: {}", scheme); } log.debug("Algorithm: {} digest offset: {}", scheme, digestOffset); byte[] tempBuffer = new byte[Constants.HANDSHAKE_SIZE - DIGEST_LENGTH]; System.arraycopy(handshake, 0, tempBuffer, 0, digestOffset); System.arraycopy(handshake, digestOffset + DIGEST_LENGTH, tempBuffer, digestOffset, Constants.HANDSHAKE_SIZE - digestOffset - DIGEST_LENGTH); byte[] tempHash = new byte[DIGEST_LENGTH]; calculateHMAC_SHA256(tempBuffer, 0, tempBuffer.length, GENUINE_FMS_KEY, 36, tempHash, 0); log.debug("Hash: {}", Hex.encodeHexString(tempHash)); boolean result = true; for (int i = 0; i < DIGEST_LENGTH; i++) { if (handshake[digestOffset + i] != tempHash[i]) { result = false; break; } } return result; } /** * Initialize SWF verification data. * * @param swfFilePath path to the swf file or null */ public void initSwfVerification(String swfFilePath) { log.info("Initializing swf verification for: {}", swfFilePath); byte[] bytes = null; if (swfFilePath != null) { File localSwfFile = new File(swfFilePath); if (localSwfFile.exists() && localSwfFile.canRead()) { log.info("Swf file path: {}", localSwfFile.getAbsolutePath()); bytes = FileUtil.readAsByteArray(localSwfFile); } else { bytes = "Red5 is awesome for handling non-accessable swf file".getBytes(); } } else { bytes = new byte[42]; } calculateHMAC_SHA256(bytes, 0, bytes.length, GENUINE_FP_KEY, 30, swfHash, 0); swfSize = bytes.length; log.info("Verification - size: {}, hash: {}", swfSize, Hex.encodeHexString(swfHash)); } public byte[] getHandshakeBytes() { return c1; } public void setForceVerification(boolean forceVerification) { this.forceVerification = forceVerification; } }