/*
* Flazr <http://flazr.com> Copyright (C) 2009 Peter Thomas.
*
* This file is part of Flazr.
*
* Flazr is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flazr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Flazr. If not, see <http://www.gnu.org/licenses/>.
*/
package com.flazr.rtmp;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;
import nliveroid.nlr.main.LiveSettings;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import android.util.Log;
import com.flazr.util.Utils;
//boolean rtmpを無くす
//rtmpeは0x06、rtmpe以外は0x03だそう rtmpeは非対応にしたので0x03に統一
public class RtmpHandshake {
public static final int HANDSHAKE_SIZE = 1536;
/** SHA 256 digest length */
private static final int DIGEST_SIZE = 32;
private static final int PUBLIC_KEY_SIZE = 128;
private static final byte[] SERVER_CONST = "Genuine Adobe Flash Media Server 001".getBytes();
public static final byte[] CLIENT_CONST = "Genuine Adobe Flash Player 001".getBytes();
private static final byte[] RANDOM_CRUD = Utils.fromHex(
"F0EEC24A8068BEE82E00D0D1029E7E576EEC5D2D29806FAB93B8E636CFEB31AE"
);
private static final byte[] SERVER_CONST_CRUD = concat(SERVER_CONST, RANDOM_CRUD);
private static final byte[] CLIENT_CONST_CRUD = concat(CLIENT_CONST, RANDOM_CRUD);
private static final byte[] DH_MODULUS_BYTES = Utils.fromHex(
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74"
+ "020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437"
+ "4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"
);
private static final BigInteger DH_MODULUS = new BigInteger(1, DH_MODULUS_BYTES);
private static final BigInteger DH_BASE = BigInteger.valueOf(2);
private static byte[] concat(byte[] a, byte[] b) {
byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
private static int calculateOffset(ChannelBuffer in, int pointerIndex, int modulus, int increment) {
byte[] pointer = new byte[4];
in.getBytes(pointerIndex, pointer,0,4);
int offset = 0;
// sum the 4 bytes of the pointer
for (int i = 0; i < pointer.length; i++) {
offset += pointer[i] & 0xff;
}
offset %= modulus;
offset += increment;
return offset;
}
private static byte[] digestHandshake(ChannelBuffer in, int digestOffset, byte[] key) {
final byte[] message = new byte[HANDSHAKE_SIZE - DIGEST_SIZE];
in.getBytes(0, message, 0, digestOffset);
final int afterDigestOffset = digestOffset + DIGEST_SIZE;
in.getBytes(afterDigestOffset, message, digestOffset, HANDSHAKE_SIZE - afterDigestOffset);
return Utils.sha256(message, key);
}
private static ChannelBuffer generateRandomHandshake() {
byte[] randomBytes = new byte[HANDSHAKE_SIZE];
Random random = new Random();
random.nextBytes(randomBytes);
return ChannelBuffers.wrappedBuffer(ChannelBuffers.BIG_ENDIAN,randomBytes);
}
private static final Map<Integer, Integer> clientVersionToValidationTypeMap;
static {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
map.put(0x09007c02, 1);
map.put(0x09009702, 1);
map.put(0x09009f02, 1);
map.put(0x0900f602, 1);
map.put(0x0a000202, 1);
map.put(0x0a000c02, 1);
map.put(0x80000102, 1);
map.put(0x80000302, 2);
map.put(0x0a002002, 2);
clientVersionToValidationTypeMap = map;
}
protected static int getValidationTypeForClientVersion(byte[] version) {
final int intValue = ChannelBuffers.wrappedBuffer(ChannelBuffers.BIG_ENDIAN,version).getInt(0);
Integer type = clientVersionToValidationTypeMap.get(intValue);
if(type == null) {
return 0;
}
return type;
}
private byte[] clientVersionToUse = new byte[]{0x09, 0x00, 0x7c, 0x02};
private byte[] serverVersionToUse = new byte[]{0x03, 0x05, 0x01, 0x01};
private static int digestOffset(ChannelBuffer in, int validationType) {
switch(validationType) {
case 1: return calculateOffset(in, 8, 728, 12);
case 2: return calculateOffset(in, 772, 728, 776);
default: throw new RuntimeException("cannot get digest offset for type: " + validationType);
}
}
private static int publicKeyOffset(ChannelBuffer in, int validationType) {
switch(validationType) {
case 1: return calculateOffset(in, 1532, 632, 772);
case 2: return calculateOffset(in, 768, 632, 8);
default: throw new RuntimeException("cannot get public key offset for type: " + validationType);
}
}
//==========================================================================
private KeyAgreement keyAgreement;
private byte[] peerVersion;
private byte[] ownPublicKey;
private byte[] peerPublicKey;
private byte[] ownPartOneDigest;
private byte[] peerPartOneDigest;
private Cipher cipherOut;
private Cipher cipherIn;
private byte[] peerTime;
private int validationType;
private byte[] swfHash;
private int swfSize;
private byte[] swfvBytes;
private ChannelBuffer peerPartOne;
private ChannelBuffer ownPartOne;
public RtmpHandshake() {}
public RtmpHandshake(LiveSettings session) {
this.swfHash = session.getSwfHash();
this.swfSize = session.getSwfSize();
if(session.getClientVersionToUse() != null) {
this.clientVersionToUse = session.getClientVersionToUse();
}
}
public byte[] getSwfvBytes() {
return swfvBytes;
}
public Cipher getCipherIn() {
return cipherIn;
}
public Cipher getCipherOut() {
return cipherOut;
}
public byte[] getPeerVersion() {
return peerVersion;
}
//========================= ENCRYPT / DECRYPT ==============================
private void cipherUpdate(final ChannelBuffer in, final Cipher cipher) {
Log.d("RtmpHandshake","CIPHER ------ " + cipher + " ::"+in);
final int size = in.readableBytes();
if(size == 0) {
return;
}
final int position = in.readerIndex();
final byte[] bytes = new byte[size];
in.getBytes(position, bytes,0,size);
byte[] cipher_update = cipher.update(bytes);
in.setBytes(position, cipher_update,0,cipher_update.length);
}
public void cipherUpdateIn(final ChannelBuffer in) {
Log.d("RtmpHandshake","UpdateIn");
cipherUpdate(in, cipherIn);
}
public void cipherUpdateOut(final ChannelBuffer in) {
Log.d("RtmpHandshake","UpdateOut");
cipherUpdate(in, cipherOut);
}
//============================== PKI =======================================
private void initKeyPair() {
final DHParameterSpec keySpec = new DHParameterSpec(DH_MODULUS, DH_BASE);
final KeyPair keyPair;
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DH");
keyGen.initialize(keySpec);
keyPair = keyGen.generateKeyPair();
keyAgreement = KeyAgreement.getInstance("DH");
keyAgreement.init(keyPair.getPrivate());
} catch (Exception e) {
throw new RuntimeException(e);
}
// extract public key bytes
DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic();
BigInteger dh_Y = publicKey.getY();
ownPublicKey = dh_Y.toByteArray();
byte[] temp = new byte[PUBLIC_KEY_SIZE];
if (ownPublicKey.length < PUBLIC_KEY_SIZE) {
// pad zeros on left
System.arraycopy(ownPublicKey, 0, temp, PUBLIC_KEY_SIZE - ownPublicKey.length, ownPublicKey.length);
ownPublicKey = temp;
} else if (ownPublicKey.length > PUBLIC_KEY_SIZE) {
// truncate zeros from left
System.arraycopy(ownPublicKey, ownPublicKey.length - PUBLIC_KEY_SIZE, temp, 0, PUBLIC_KEY_SIZE);
ownPublicKey = temp;
}
}
private void initCiphers() {
BigInteger otherPublicKeyInt = new BigInteger(1, peerPublicKey);
try {
KeyFactory keyFactory = KeyFactory.getInstance("DH");
KeySpec otherPublicKeySpec = new DHPublicKeySpec(otherPublicKeyInt, DH_MODULUS, DH_BASE);
PublicKey otherPublicKey = keyFactory.generatePublic(otherPublicKeySpec);
keyAgreement.doPhase(otherPublicKey, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
byte[] sharedSecret = keyAgreement.generateSecret();
byte[] digestOut = Utils.sha256(peerPublicKey, sharedSecret);
byte[] digestIn = Utils.sha256(ownPublicKey, sharedSecret);
try {
cipherOut = Cipher.getInstance("RC4");
cipherOut.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(digestOut, 0, 16, "RC4"));
cipherIn = Cipher.getInstance("RC4");
cipherIn.init(Cipher.DECRYPT_MODE, new SecretKeySpec(digestIn, 0, 16, "RC4"));
Log.d("RtmpHandshake","initialized encryption / decryption ciphers");
} catch (Exception e) {
throw new RuntimeException(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[HANDSHAKE_SIZE];
cipherIn.update(dummyBytes);
cipherOut.update(dummyBytes);
}
//============================== CLIENT ====================================
public ChannelBuffer encodeClient0() {
Log.d("RtmpHandshake","encodeClient0 client version:"+ Utils.toHex(clientVersionToUse,0,clientVersionToUse.length,false));
ChannelBuffer out = ChannelBuffers.buffer(ChannelBuffers.BIG_ENDIAN,1);
out.writeByte((byte) 0x03);
return out;
}
public ChannelBuffer encodeClient1() {
ChannelBuffer out = generateRandomHandshake();
out.setInt(0, 0); // zeros
out.setBytes(4, clientVersionToUse,0,clientVersionToUse.length);
validationType = getValidationTypeForClientVersion(clientVersionToUse);
Log.d("RtmpHandshake","encodeClient1 client version:"+ Utils.toHex(clientVersionToUse,0,clientVersionToUse.length,false));
if (validationType == 0) {
ownPartOne = out.copy(); // save for later
return out;
}
Log.d("RtmpHandshake","creating client part 1, validation "+validationType);
initKeyPair();
int publicKeyOffset = publicKeyOffset(out, validationType);
out.setBytes(publicKeyOffset, ownPublicKey,0,ownPublicKey.length);
int digestOffset = digestOffset(out, validationType);
ownPartOneDigest = digestHandshake(out, digestOffset, CLIENT_CONST);
out.setBytes(digestOffset, ownPartOneDigest,0,ownPartOneDigest.length);
return out;
}
public boolean decodeServerAll(ChannelBuffer in) {
Log.d("RtmpHandshake","decoderServerAll");
decodeServer0(in.readBytes(1));
decodeServer1(in.readBytes(HANDSHAKE_SIZE));
decodeServer2(in.readBytes(HANDSHAKE_SIZE));
return true;
}
private void decodeServer0(ChannelBuffer in) {
Log.d("RtmpHandshake","decodeServer0");
byte flag = in.getByte(0);
}
private void decodeServer1(ChannelBuffer in) {
peerTime = new byte[4];
in.getBytes(0, peerTime,0,4);
byte[] serverVersion = new byte[4];
in.getBytes(4, serverVersion,0,4);
Log.d("RtmpHandshake","decodeServer1 "+Utils.toHex(peerTime,0,peerTime.length,false)+" "+Utils.toHex(serverVersion,0,serverVersion.length,false));
if(swfHash != null) {
// swf verification
byte[] key = new byte[DIGEST_SIZE];
in.getBytes(HANDSHAKE_SIZE - DIGEST_SIZE, key,0,DIGEST_SIZE);
byte[] digest = Utils.sha256(swfHash, key);
// construct SWF verification pong payload
ChannelBuffer swfv = ChannelBuffers.buffer(ChannelBuffers.BIG_ENDIAN,42);
swfv.writeByte((byte) 0x01);
swfv.writeByte((byte) 0x01);
swfv.writeInt(swfSize);
swfv.writeInt(swfSize);
swfv.writeBytes(digest,0,digest.length);
swfvBytes = new byte[42];
swfv.readBytes(swfvBytes,0,42);
Log.d("calculated swf verification response: {}", Utils.toHex(swfvBytes,0,swfvBytes.length,false));
}
if(validationType == 0) {
peerPartOne = in; // save for later
return;
}
Log.d("processing server part 1, validation type: {}",""+ validationType);
int digestOffset = digestOffset(in, validationType);
byte[] expected = digestHandshake(in, digestOffset, SERVER_CONST);
peerPartOneDigest = new byte[DIGEST_SIZE];
in.getBytes(digestOffset, peerPartOneDigest,0,DIGEST_SIZE);
if (!Arrays.equals(peerPartOneDigest, expected)) {
int altValidationType = validationType == 1 ? 2 : 1;
Log.d("server part 1 validation failed for type {}, will try with type {}",
""+ validationType + " "+altValidationType);
digestOffset = digestOffset(in, altValidationType);
expected = digestHandshake(in, digestOffset, SERVER_CONST);
peerPartOneDigest = new byte[DIGEST_SIZE];
in.getBytes(digestOffset, peerPartOneDigest,0,DIGEST_SIZE);
if (!Arrays.equals(peerPartOneDigest, expected)) {
throw new RuntimeException("server part 1 validation failed even for type: " + altValidationType);
}
validationType = altValidationType;
}
Log.d("server part 1 validation success","");
peerPublicKey = new byte[PUBLIC_KEY_SIZE];
int publicKeyOffset = publicKeyOffset(in, validationType);
in.getBytes(publicKeyOffset, peerPublicKey,0,PUBLIC_KEY_SIZE);
initCiphers();
}
private void decodeServer2(ChannelBuffer in) {
Log.d("RtmpHandshake","decodeServer2");
if(validationType == 0) {
return; // TODO validate random echo
}
Log.d("processing server part 2 for validation"," ");
byte[] key = Utils.sha256(ownPartOneDigest, SERVER_CONST_CRUD);
int digestOffset = HANDSHAKE_SIZE - DIGEST_SIZE;
byte[] expected = digestHandshake(in, digestOffset, key);
byte[] actual = new byte[DIGEST_SIZE];
in.getBytes(digestOffset, actual,0,DIGEST_SIZE);
if (!Arrays.equals(actual, expected)) {
throw new RuntimeException("server part 2 validation failed");
}
Log.d("server part 2 validation success","");
}
public ChannelBuffer encodeClient2() {
Log.d("RtmpHandshake","encodeClient2");
if(validationType == 0) {
peerPartOne.setBytes(0, peerTime,0,peerTime.length);
peerPartOne.setInt(4, 0); // more zeros
return peerPartOne;
}
Log.d("creating client part 2 for validation"," ");
ChannelBuffer out = generateRandomHandshake();
byte[] key = Utils.sha256(peerPartOneDigest, CLIENT_CONST_CRUD);
int digestOffset = HANDSHAKE_SIZE - DIGEST_SIZE;
byte[] digest = digestHandshake(out, digestOffset, key);
out.setBytes(digestOffset, digest,0,digest.length);
return out;
}
//============================ SERVER ======================================
public void decodeClient0And1(ChannelBuffer in) {
decodeClient0(in.readBytes(1));
decodeClient1(in.readBytes(HANDSHAKE_SIZE));
}
private void decodeClient0(ChannelBuffer in) {
final byte firstByte = in.readByte();
Log.d("RtmpHandshake","decodeClient0 "+ String.valueOf(Utils.toHexChars(firstByte)));
}
private boolean decodeClient1(ChannelBuffer in) {
Log.d("RtmpHandshake","decodeClient1 ");
peerTime = new byte[4];
in.getBytes(0, peerTime,0,4);
peerVersion = new byte[4];
in.getBytes(4, peerVersion,0,4);
Log.d("client time: {}, version: {}", ""+Utils.toHex(peerTime,0,peerTime.length,false)+" "+ Utils.toHex(peerVersion,0,peerVersion.length,false));
validationType = getValidationTypeForClientVersion(peerVersion);
if(validationType == 0) {
peerPartOne = in; // save for later
return true;
}
Log.d("RtmpHandshake","processing client part 1 for validation type:"+validationType);
initKeyPair();
int digestOffset = digestOffset(in, validationType);
peerPartOneDigest = new byte[DIGEST_SIZE];
in.getBytes(digestOffset, peerPartOneDigest,0,DIGEST_SIZE);
byte[] expected = digestHandshake(in, digestOffset, CLIENT_CONST);
if(!Arrays.equals(peerPartOneDigest, expected)) {
throw new RuntimeException("client part 1 validation failed");
}
Log.d("client part 1 validation success","");
int publicKeyOffset = publicKeyOffset(in, validationType);
peerPublicKey = new byte[PUBLIC_KEY_SIZE];
in.getBytes(publicKeyOffset, peerPublicKey,0,PUBLIC_KEY_SIZE);
initCiphers();
return true;
}
public ChannelBuffer encodeServer0() {
Log.d("RtmpHandshake","encodeServer0");
ChannelBuffer out = ChannelBuffers.buffer(ChannelBuffers.BIG_ENDIAN,1);
out.writeByte((byte) 0x03);
return out;
}
public ChannelBuffer encodeServer1() {
Log.d("RtmpHandshake","encodeServer1");
ChannelBuffer out = generateRandomHandshake();
out.setInt(0, 0); // zeros
out.setBytes(4, serverVersionToUse,0,serverVersionToUse.length);
if(validationType == 0) {
ownPartOne = out.copy();
return out;
}
Log.d("creating server part 1 for validation type: {}",""+ validationType);
int publicKeyOffset = publicKeyOffset(out, validationType);
out.setBytes(publicKeyOffset, ownPublicKey,0,ownPublicKey.length);
int digestOffset = digestOffset(out, validationType);
ownPartOneDigest = digestHandshake(out, digestOffset, SERVER_CONST);
out.setBytes(digestOffset, ownPartOneDigest,0,ownPartOneDigest.length);
return out;
}
public void decodeClient2(ChannelBuffer raw) {
Log.d("RtmpHandshake","decodeClient2");
ChannelBuffer in = raw.readBytes(HANDSHAKE_SIZE);
if(validationType == 0) {
return;
}
Log.d("processing client part 2 for validation"," ");
byte[] key = Utils.sha256(ownPartOneDigest, CLIENT_CONST_CRUD);
int digestOffset = HANDSHAKE_SIZE - DIGEST_SIZE;
byte[] expected = digestHandshake(in, digestOffset, key);
byte[] actual = new byte[DIGEST_SIZE];
in.getBytes(digestOffset, actual,0,DIGEST_SIZE);
if (!Arrays.equals(actual, expected)) {
throw new RuntimeException("client part 2 validation failed");
}
Log.d("client part 2 validation success","");
}
public ChannelBuffer encodeServer2() {
Log.d("RtmpHandshake","encodeServer2");
if(validationType == 0) {
peerPartOne.setBytes(0, peerTime,0,peerTime.length); // zeros
peerPartOne.setInt(4, 0); // more zeros
return peerPartOne;
}
Log.d("creating server part 2 for validation"," ");
ChannelBuffer out = generateRandomHandshake();
byte[] key = Utils.sha256(peerPartOneDigest, SERVER_CONST_CRUD);
int digestOffset = HANDSHAKE_SIZE - DIGEST_SIZE;
byte[] digest = digestHandshake(out, digestOffset, key);
out.setBytes(digestOffset, digest,0,digest.length);
return out;
}
}