package javaforce.voip; /** Secure RTP * * @author pquiring * * Created : Dec ?, 2013 * * RFCs: * http://tools.ietf.org/html/rfc3711 - SRTP * http://tools.ietf.org/html/rfc5764 - Using DTLS to exchange keys for SRTP * http://tools.ietf.org/html/rfc4568 - Using SDP to exchange keys for SRTP (old method before DTLS) */ import java.io.*; import java.net.*; import java.nio.*; import java.security.*; import java.util.*; import javax.crypto.Mac; import javaforce.*; import org.bouncycastle.crypto.tls.*; import org.bouncycastle.crypto.params.*; import org.bouncycastle.asn1.pkcs.*; import org.bouncycastle.crypto.util.*; public class SRTPChannel extends RTPChannel { protected SRTPChannel(RTP rtp, int ssrc, SDP.Stream stream) { super(rtp,ssrc,stream); } private boolean have_keys = false; private boolean dtls = false; private boolean dtlsServerMode = false; private boolean stunReceived = false; private boolean dtlsReady = false; private SRTPContext srtp_in, srtp_out; private int _tailIn, _tailOut; private long _seqno = 0; //must keep track of seqno beyond 16bits private DatagramSocket dtlsSocket, rawSocket; private DefaultTlsServer2 tlsServer; private DefaultTlsClient2 tlsClient; private DTLSTransport dtlsTransport; //DTLS is actually never used to send/receive data (only key exchange) private Worker worker; private String local_iceufrag, local_icepwd; private static DTLSServerProtocol dtlsServer; private static DTLSClientProtocol dtlsClient; private static InetAddress localhost; private static org.bouncycastle.crypto.tls.Certificate dtlsCertChain; private static AsymmetricKeyParameter dtlsPrivateKey; private static final int KEY_LENGTH = 16; private static final int SALT_LENGTH = 14; private byte[] sharedSecret; private byte[] remoteKey = new byte[KEY_LENGTH], remoteSalt = new byte[SALT_LENGTH+2]; private byte[] localKey = new byte[KEY_LENGTH], localSalt = new byte[SALT_LENGTH+2]; public void writeRTP(byte data[], int off, int len) { if (rtp.rawMode) { super.writeRTP(data, off, len); return; } if (srtp_out == null) { if (!have_keys) return; //not ready try { srtp_out = new SRTPContext(); srtp_out.setCrypto("AES_CM_128_HMAC_SHA1_80", localKey, localSalt); _tailOut = srtp_out.getAuthTail(); srtp_out.deriveKeys(0); } catch (Exception e) { JFLog.log(e); return; } } try { byte payload[] = Arrays.copyOfRange(data, off+12, off + len); int ssrc = BE.getuint32(data, off + 8); // int stamp = BE.getuint32(data, off + 4); int seqno = BE.getuint16(data, off + 2); // if (stream.keyExchange == SDP.KeyExchange.SDP) { // srtp_out.deriveKeys(stamp); //not used in RFC 5764 (RFC 3711:only needed if kdr != 0) // } _seqno &= 0xffff0000; _seqno |= seqno; encrypt(payload, ssrc, _seqno++); byte packet[] = new byte[len + _tailOut]; System.arraycopy(data, off, packet, 0, 12); System.arraycopy(payload, 0, packet, 12, payload.length); appendAuth(packet, srtp_out, seqno); super.writeRTP(packet, 0, packet.length); } catch (Exception e) { JFLog.log(e); } } /** Sets keys found in SDP used on this side of SRTP (not used in DTLS mode) */ public void setLocalKeys(byte key[], byte salt[]) { System.arraycopy(key, 0, localKey, 0, KEY_LENGTH); System.arraycopy(salt, 0, localSalt, 0, SALT_LENGTH); have_keys = true; } /** Sets keys found in SDP used on other side of SRTP (not used in DTLS mode) */ public void setRemoteKeys(byte key[], byte salt[]) { System.arraycopy(key, 0, remoteKey, 0, KEY_LENGTH); System.arraycopy(salt, 0, remoteSalt, 0, SALT_LENGTH); have_keys = true; } /** Enables DTLS mode (otherwise you MUST call setServerKeys() AND setClientKeys() before calling start()). */ public void setDTLS(boolean server, String local_iceufrag, String local_icepwd) { dtls = true; dtlsServerMode = server; this.local_iceufrag = local_iceufrag; this.local_icepwd = local_icepwd; } public boolean start() { JFLog.log("SRTPChannel.start:dtls=" + dtls); if (!super.start()) return false; if (rtp.rawMode) return true; if (!dtls) return have_keys; //create a thread to do STUN/DTLS requests new Thread() { public void run() { while (rtp.active && !stunReceived) { JF.sleep(500); Random r = new Random(); byte request[] = new byte[1500]; ByteBuffer bb = ByteBuffer.wrap(request); bb.order(ByteOrder.BIG_ENDIAN); int offset = 0; bb.putShort(offset, BINDING_REQUEST); offset += 2; int lengthOffset = offset; bb.putShort(offset, (short)0); //length (patch later) offset += 2; long id1; id1 = 0x2112a442; //magic cookie id1 <<= 32; id1 += Math.abs(r.nextInt()); bb.putLong(offset, id1); offset += 8; long id2 = r.nextLong(); bb.putLong(offset, id2); offset += 8; String user = stream.sdp.iceufrag + ":" + local_iceufrag; int strlen = user.length(); bb.putShort(offset, USERNAME); offset += 2; bb.putShort(offset, (short)strlen); offset += 2; System.arraycopy(user.getBytes(), 0, request, offset, strlen); offset += strlen; if ((offset & 3) > 0) { offset += 4 - (offset & 3); //padding } //ICE:PRIORITY (MUST) bb.putShort(offset, PRIORITY); offset += 2; bb.putShort(offset, (short)4); offset += 2; bb.putInt(offset, Math.abs(r.nextInt())); //TODO : calc this??? offset += 4; //ICE:ICE_CONTROLLED (MUST) bb.putShort(offset, ICE_CONTROLLED); offset += 2; bb.putShort(offset, (short)8); offset += 2; bb.putLong(offset, r.nextLong()); //random tie-breaker offset += 8; bb.putShort(lengthOffset, (short)(offset - 20 + 24)); //patch length (24=MSG_INT) byte id[] = STUN.calcMsgIntegrity(request, offset, STUN.calcKey(stream.sdp.icepwd)); strlen = id.length; bb.putShort(offset, MESSAGE_INTEGRITY); offset += 2; bb.putShort(offset, (short)strlen); offset += 2; System.arraycopy(id, 0, request, offset, strlen); offset += strlen; if ((offset & 3) > 0) { offset += 4 - (offset & 3); //padding } bb.putShort(lengthOffset, (short)(offset - 20 + 8)); //patch length (8=FINGERPRINT) //fingerprint bb.putShort(offset, FINGERPRINT); offset += 2; bb.putShort(offset, (short)4); offset += 2; bb.putInt(offset, STUN.calcFingerprint(request, offset - 4)); offset += 4; bb.putShort(lengthOffset, (short)(offset - 20)); //patch length try { if (rtp.useTURN) { rtp.stun1.sendData(turn1ch, request, 0, offset); } else { DatagramPacket dp = new DatagramPacket(request, 0, offset, InetAddress.getByName(stream.getIP()), stream.getPort()); rtp.sock1.send(dp); } } catch (Exception e) { JFLog.log(e); } } //see https://github.com/bcgit/bc-java/tree/master/core/src/test/java/org/bouncycastle/crypto/tls/test try { localhost = InetAddress.getByName("localhost"); dtlsSocket = new DatagramSocket(RTP.getnextlocalrtpport()); rawSocket = new DatagramSocket(RTP.getnextlocalrtpport()); dtlsSocket.connect(localhost, rawSocket.getLocalPort()); rawSocket.connect(localhost, dtlsSocket.getLocalPort()); } catch (Exception e) { JFLog.log(e); return; } worker = new Worker(); worker.start(); if (!dtlsServerMode) { try { dtlsClient = new DTLSClientProtocol(new SecureRandom()); } catch (Exception e) { JFLog.log(e); dtlsClient = null; return; } tlsClient = new DefaultTlsClient2() { protected TlsSession session; public TlsSession getSessionToResume() { return this.session; } public ProtocolVersion getClientVersion() { return ProtocolVersion.DTLSv12; } public ProtocolVersion getMinimumVersion() { return ProtocolVersion.DTLSv10; } public Hashtable getClientExtensions() throws IOException { //see : http://bouncy-castle.1462172.n4.nabble.com/DTLS-SRTP-with-bouncycastle-1-49-td4656286.html Hashtable table = super.getClientExtensions(); if (table == null) table = new Hashtable(); int[] protectionProfiles = { SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80 //this is the only one supported for now // SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32 // SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32 // SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80 }; byte mki[] = new byte[0]; //do not use mki UseSRTPData srtpData = new UseSRTPData(protectionProfiles, mki); TlsSRTPUtils.addUseSRTPExtension(table, srtpData); return table; } public TlsAuthentication getAuthentication() throws IOException { return new TlsAuthentication() { public void notifyServerCertificate(org.bouncycastle.crypto.tls.Certificate serverCertificate) throws IOException { //info only } public TlsCredentials getClientCredentials(CertificateRequest certificateRequest) throws IOException { short[] certificateTypes = certificateRequest.getCertificateTypes(); if (certificateTypes == null) return null; boolean ok = false; for(int a=0;a<certificateTypes.length;a++) { if (certificateTypes[a] == ClientCertificateType.rsa_sign) { ok = true; break; } } if (!ok) return null; SignatureAndHashAlgorithm signatureAndHashAlgorithm = null; Vector sigAlgs = certificateRequest.getSupportedSignatureAlgorithms(); if (sigAlgs != null) { for (int i = 0; i < sigAlgs.size(); ++i) { SignatureAndHashAlgorithm sigAlg = (SignatureAndHashAlgorithm) sigAlgs.elementAt(i); if (sigAlg.getSignature() == SignatureAlgorithm.rsa) { signatureAndHashAlgorithm = sigAlg; break; } } if (signatureAndHashAlgorithm == null) { return null; } } return new DefaultTlsSignerCredentials(context, dtlsCertChain, dtlsPrivateKey, signatureAndHashAlgorithm); } }; } public void notifyHandshakeComplete() throws IOException { JFLog.log("SRTPChannel:DTLS:Client:Handshake complete"); super.notifyHandshakeComplete(); TlsSession newSession = context.getResumableSession(); if (newSession != null) { /* byte[] newSessionID = newSession.getSessionID(); String hex = Hex.toHexString(newSessionID); if (this.session != null && Arrays.areEqual(this.session.getSessionID(), newSessionID)) { System.out.println("Resumed session: " + hex); } else { System.out.println("Established session: " + hex); } */ this.session = newSession; } getKeys(); } }; new Thread() { public void run() { try { JFLog.log("SRTPChannel:connecting to DTLS server"); dtlsTransport = dtlsClient.connect(tlsClient, new UDPTransport(dtlsSocket, 1500 - 20 - 8)); } catch (Exception e) { JFLog.log(e); } } }.start(); } else { try { dtlsServer = new DTLSServerProtocol(new SecureRandom()); } catch (Exception e) { JFLog.log(e); dtlsServer = null; return; } try { tlsServer = new DefaultTlsServer2() { public void notifyClientCertificate(org.bouncycastle.crypto.tls.Certificate clientCertificate) throws IOException { org.bouncycastle.asn1.x509.Certificate[] chain = clientCertificate.getCertificateList(); // JFLog.log("Received client certificate chain of length " + chain.length); for (int i = 0; i != chain.length; i++) { org.bouncycastle.asn1.x509.Certificate entry = chain[i]; // JFLog.log("fingerprint:SHA-256 " + KeyMgmt.fingerprintSHA256(entry.getEncoded()) + " (" + entry.getSubject() + ")"); // JFLog.log("cert length=" + entry.getEncoded().length); } } protected ProtocolVersion getMaximumVersion() { return ProtocolVersion.DTLSv12; } protected ProtocolVersion getMinimumVersion() { return ProtocolVersion.DTLSv10; } protected TlsEncryptionCredentials getRSAEncryptionCredentials() throws IOException { return new DefaultTlsEncryptionCredentials(context, dtlsCertChain, dtlsPrivateKey); } protected TlsSignerCredentials getRSASignerCredentials() throws IOException { SignatureAndHashAlgorithm signatureAndHashAlgorithm = null; Vector sigAlgs = supportedSignatureAlgorithms; if (sigAlgs != null) { for (int i = 0; i < sigAlgs.size(); ++i) { SignatureAndHashAlgorithm sigAlg = (SignatureAndHashAlgorithm) sigAlgs.elementAt(i); if (sigAlg.getSignature() == SignatureAlgorithm.rsa) { signatureAndHashAlgorithm = sigAlg; break; } } if (signatureAndHashAlgorithm == null) { return null; } } return new DefaultTlsSignerCredentials(context, dtlsCertChain, dtlsPrivateKey, signatureAndHashAlgorithm); } public Hashtable getServerExtensions() throws IOException { //see : http://bouncy-castle.1462172.n4.nabble.com/DTLS-SRTP-with-bouncycastle-1-49-td4656286.html Hashtable table = super.getServerExtensions(); if (table == null) table = new Hashtable(); int[] protectionProfiles = { // TODO : need to pick ONE that client offers SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80 //this is the only one supported for now // SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_32 // SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_32 // SRTPProtectionProfile.SRTP_NULL_HMAC_SHA1_80 }; byte mki[] = new byte[0]; //should match client or use nothing UseSRTPData srtpData = new UseSRTPData(protectionProfiles, mki); TlsSRTPUtils.addUseSRTPExtension(table, srtpData); return table; } public void notifyHandshakeComplete() throws IOException { JFLog.log("SRTPChannel:DTLS:Server:Handshake complete"); super.notifyHandshakeComplete(); getKeys(); } }; new Thread() { public void run() { try { JFLog.log("SRTPChannel:accepting from DTLS client"); dtlsTransport = dtlsServer.accept(tlsServer, new UDPTransport(dtlsSocket, 1500 - 20 - 8)); } catch (Exception e) { JFLog.log(e); } } }.start(); } catch (Exception e) { JFLog.log(e); dtlsSocket = null; rawSocket = null; return; } } dtlsReady = true; } }.start(); return true; } private final static short BINDING_REQUEST = 0x0001; private final static short BINDING_RESPONSE = 0x0101; private final static short MAPPED_ADDRESS = 0x0001; private final static short USERNAME = 0x0006; private final static short XOR_MAPPED_ADDRESS = 0x0020; private final static short MESSAGE_INTEGRITY = 0x0008; //http://tools.ietf.org/html/rfc5245 - ICE private final static short USE_CANDIDATE = 0x25; //used by ICE_CONTROLLING only private final static short PRIORITY = 0x24; //see section 4.1.2 private final static short ICE_CONTROLLING = (short)0x802a; private final static short ICE_CONTROLLED = (short)0x8029; private final static short FINGERPRINT = (short)0x8028; private byte stun[]; private boolean isClassicStun(long id1) { return ((id1 >>> 32) != 0x2112a442); } private int IP4toInt(String ip) { String o[] = ip.split("[.]"); int ret = 0; for (int a = 0; a < 4; a++) { ret <<= 8; ret += (JF.atoi(o[a])); } return ret; } protected void processSTUN(byte data[], int off, int len) { JFLog.log("SRTPChannel:received STUN request"); //the only command supported is BINDING_REQUEST String username = null, flipped = null; ByteBuffer bb = ByteBuffer.wrap(data, off, len); bb.order(ByteOrder.BIG_ENDIAN); int offset = off; short code = bb.getShort(offset); boolean auth = false; if (code != BINDING_REQUEST) { JFLog.log("RTPSecureChannel:Error:STUN Request is not Binding request"); return; } offset += 2; int lengthOffset = offset; short length = bb.getShort(offset); offset += 2; long id1 = bb.getLong(offset); offset += 8; long id2 = bb.getLong(offset); offset += 8; while (offset < len) { short attr = bb.getShort(offset); offset += 2; length = bb.getShort(offset); offset += 2; switch (attr) { case USERNAME: username = new String(data, offset, length); String f[] = username.split("[:]"); flipped = f[1] + ":" + f[0]; //reverse username break; case MESSAGE_INTEGRITY: bb.putShort(lengthOffset, (short) (offset)); //patch length byte correct[] = STUN.calcMsgIntegrity(data, offset - 4, STUN.calcKey(local_icepwd)); byte supplied[] = Arrays.copyOfRange(data, offset, offset + 20); auth = Arrays.equals(correct, supplied); break; } offset += length; if ((length & 0x3) > 0) { offset += 4 - (length & 0x3); //padding } } if (!auth) { return; //wrong credentials } //build response if (stun == null) { stun = new byte[1500]; } bb = ByteBuffer.wrap(stun); bb.order(ByteOrder.BIG_ENDIAN); offset = 0; bb.putShort(offset, BINDING_RESPONSE); offset += 2; lengthOffset = offset; bb.putShort(offset, (short) 0); //length (patch later) offset += 2; bb.putLong(offset, id1); offset += 8; bb.putLong(offset, id2); offset += 8; if (isClassicStun(id1)) { bb.putShort(offset, MAPPED_ADDRESS); offset += 2; bb.putShort(offset, (short) 8); //length of attr offset += 2; bb.put(offset, (byte) 0); //reserved offset++; bb.put(offset, (byte) 1); //IP family offset++; bb.putShort(offset, (short) stream.port); offset += 2; bb.putInt(offset, IP4toInt(stream.getIP())); offset += 4; } else { //use XOR_MAPPED_ADDRESS instead bb.putShort(offset, XOR_MAPPED_ADDRESS); offset += 2; bb.putShort(offset, (short) 8); //length of attr offset += 2; bb.put(offset, (byte) 0); //reserved offset++; bb.put(offset, (byte) 1); //IP family offset++; bb.putShort(offset, (short) (stream.port ^ bb.getShort(4))); offset += 2; bb.putInt(offset, IP4toInt(stream.getIP()) ^ bb.getInt(4)); offset += 4; } bb.putShort(lengthOffset, (short) (offset - 20 + 24)); //patch length byte id[] = STUN.calcMsgIntegrity(stun, offset, STUN.calcKey(local_icepwd)); int strlen = id.length; bb.putShort(offset, MESSAGE_INTEGRITY); offset += 2; bb.putShort(offset, (short) strlen); offset += 2; System.arraycopy(id, 0, stun, offset, strlen); offset += strlen; if ((offset & 3) > 0) { offset += 4 - (offset & 3); //padding } bb.putShort(lengthOffset, (short) (offset - 20)); //patch length try { if (rtp.useTURN) { rtp.stun1.sendData(turn1ch, stun, 0, offset); } else { rtp.sock1.send(new DatagramPacket(stun, 0, offset, InetAddress.getByName(stream.getIP()), stream.getPort())); } } catch (Exception e) { JFLog.log(e); } } public static boolean initDTLS(java.util.List<byte []> certChain, byte privateKey[], boolean pkRSA) { try { org.bouncycastle.asn1.x509.Certificate x509certs[] = new org.bouncycastle.asn1.x509.Certificate[certChain.size()]; for (int i = 0; i < certChain.size(); ++i) { x509certs[i] = org.bouncycastle.asn1.x509.Certificate.getInstance(certChain.get(i)); } dtlsCertChain = new org.bouncycastle.crypto.tls.Certificate(x509certs); if (pkRSA) { RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKey); dtlsPrivateKey = new RSAPrivateCrtKeyParameters(rsa.getModulus(), rsa.getPublicExponent(), rsa.getPrivateExponent(), rsa.getPrime1(), rsa.getPrime2(), rsa.getExponent1(), rsa.getExponent2(), rsa.getCoefficient()); } else { dtlsPrivateKey = PrivateKeyFactory.createKey(privateKey); } return true; } catch (Exception e) { JFLog.log(e); return false; } } //the sharedSecret is the same on each side private class DefaultTlsServer2 extends DefaultTlsServer { public void getKeys() { try { sharedSecret = context.exportKeyingMaterial(ExporterLabel.dtls_srtp, null, (KEY_LENGTH + SALT_LENGTH) * 2); if (sharedSecret == null) throw new Exception("null keys"); } catch (Exception e) { JFLog.log(e); return; } // printArray("keys", sharedSecret, 0, sharedSecret.length); int offset = 0; System.arraycopy(sharedSecret, offset, remoteKey, 0, KEY_LENGTH); offset += KEY_LENGTH; System.arraycopy(sharedSecret, offset, localKey, 0, KEY_LENGTH); offset += KEY_LENGTH; System.arraycopy(sharedSecret, offset, remoteSalt, 0, SALT_LENGTH); offset += SALT_LENGTH; System.arraycopy(sharedSecret, offset, localSalt, 0, SALT_LENGTH); offset += SALT_LENGTH; have_keys = true; } } private abstract class DefaultTlsClient2 extends DefaultTlsClient { public void getKeys() { try { sharedSecret = context.exportKeyingMaterial(ExporterLabel.dtls_srtp, null, (KEY_LENGTH + SALT_LENGTH) * 2); if (sharedSecret == null) throw new Exception("null keys"); } catch (Exception e) { JFLog.log(e); return; } // printArray("keys", sharedSecret, 0, sharedSecret.length); int offset = 0; System.arraycopy(sharedSecret, offset, localKey, 0, KEY_LENGTH); offset += KEY_LENGTH; System.arraycopy(sharedSecret, offset, remoteKey, 0, KEY_LENGTH); offset += KEY_LENGTH; System.arraycopy(sharedSecret, offset, localSalt, 0, SALT_LENGTH); offset += SALT_LENGTH; System.arraycopy(sharedSecret, offset, remoteSalt, 0, SALT_LENGTH); offset += SALT_LENGTH; have_keys = true; } } protected void processDTLS(byte data[], int off, int len) { if (!dtlsReady) return; //not ready try { rawSocket.send(new DatagramPacket(data, off, len, localhost, dtlsSocket.getLocalPort())); } catch (Exception e) { JFLog.log(e); } } /** Transfers DTLS packets from rawSocket to rtpSocket */ private class Worker extends Thread { public void run() { try { byte data[] = new byte[1500]; while (active) { DatagramPacket pack = new DatagramPacket(data, 1500); rawSocket.receive(pack); int len = pack.getLength(); int off = 0; if (rtp.useTURN) { rtp.stun1.sendData(turn1ch, data, off, len); } else { rtp.sock1.send(new DatagramPacket(data, off, len, InetAddress.getByName(stream.getIP()), stream.getPort())); } } } catch (Exception e) { JFLog.log(e); } } } protected void processRTP(byte data[], int off, int len) { if (rtp.rawMode) { rtp.iface.rtpPacket(this, data, off, len); return; } int firstByte = ((int)data[off]) & 0xff; //see http://tools.ietf.org/html/rfc5764#section-5 if (firstByte == 0) { //STUN request processSTUN(data, off, len); return; } if (firstByte == 1) { //STUN response (contents not used) stunReceived = true; return; } if (firstByte > 127 && firstByte < 192) { //SRTP data if (!have_keys) { JFLog.log("SRTPChannel:warn:received SRTP data but keys undefined"); return; } if (srtp_in == null) { try { srtp_in = new SRTPContext(); srtp_in.setCrypto("AES_CM_128_HMAC_SHA1_80", remoteKey, remoteSalt); _tailIn = srtp_in.getAuthTail(); srtp_in.deriveKeys(0); } catch (Exception e) { JFLog.log(e); } } int seqno = BE.getuint16(data, off + 2); int ssrc = BE.getuint32(data, off + 8); long index = getIndex(seqno); updateCounters(seqno, index); if (checkAuth(data, len)) return; if (checkForReplay(index)) return; byte payload[] = Arrays.copyOfRange(data, off + 12, off + len); try { decrypt(payload, ssrc, seqno, index); } catch (Exception e) { JFLog.log(e); } System.arraycopy(payload, 0, data, off+12, payload.length); super.processRTP(data, off, len - _tailIn); return; } //raw DTLS data (handshaking) if (dtls) processDTLS(data, off, len); } private void printArray(String msg, byte data[], int off, int len) { StringBuilder sb = new StringBuilder(); int sum = 0; //crc kinda for(int a=0;a<len;a++) { sb.append(","); sb.append(Integer.toString(((int)data[off + a]) & 0xff, 16)); sum += data[off + a]; } JFLog.log(msg + "(" + len + ")=" + sb.toString() + "=" + sum); } private ByteBuffer getPepper(int ssrc, long idx) { //(SSRC * 2^64) XOR (i * 2^16) ByteBuffer pepper = ByteBuffer.allocate(16); pepper.putInt(4, ssrc); long sindex = idx << 16; pepper.putLong(8, sindex); return pepper; } private void decrypt(byte[] payload, int ssrc, int seqno, long index) throws GeneralSecurityException { ByteBuffer in = ByteBuffer.wrap(payload); // aes likes the buffer a multiple of 32 and longer than the input. int pl = (((payload.length / 32) + 2) * 32); ByteBuffer out = ByteBuffer.allocate(pl); ByteBuffer pepper = getPepper(ssrc, index); srtp_in.decipher(in, out, pepper); System.arraycopy(out.array(), 0, payload, 0, payload.length); } private void encrypt(byte[] payload, int ssrc, long idx) throws GeneralSecurityException { ByteBuffer in = ByteBuffer.wrap(payload); int pl = (((payload.length / 32) + 2) * 32); ByteBuffer out = ByteBuffer.allocate(pl); ByteBuffer pepper = getPepper(ssrc, idx); srtp_out.decipher(in, out, pepper); System.arraycopy(out.array(), 0, payload, 0, payload.length); } private void appendAuth(byte[] packet, SRTPContext sc, int seqno) { try { // strictly we might need to derive the keys here too - // since we might be doing auth but no crypt. // we don't support that so nach. Mac mac = sc.getAuthMac(); int offs = packet.length - _tailOut; ByteBuffer m = ByteBuffer.allocate(offs + 4); m.put(packet, 0, offs); int oroc = (int) (seqno >>> 16); m.putInt(oroc); m.position(0); mac.update(m); byte[] auth = mac.doFinal(); for (int i = 0; i < _tailOut; i++) { packet[offs + i] = auth[i]; } } catch (Exception e) { JFLog.log(e); } } private final static int SRTPWINDOWSIZE = 64; private long[] _replay = new long[SRTPWINDOWSIZE]; private long _windowLeadingEdge = 0; private boolean checkForReplay(long _index) { if (_index < _windowLeadingEdge) { // old packet.... if ((_windowLeadingEdge - _index) > SRTPWINDOWSIZE) { JFLog.log("SRTPChannel:Replay:packet too old"); return true; //replay } // in window but .... is it a replay ? int tidx = (int) (_index % SRTPWINDOWSIZE); if (_replay[tidx] == _index) { JFLog.log("SRTPChannel:Replay:seen that packet"); return true; //replay } } return false; } private boolean checkAuth(byte[] packet, int plen) { try { srtp_in.deriveKeys(0/*_index*/); Mac hmac = srtp_in.getAuthMac(); int alen = _tailIn; int offs = plen - alen; ByteBuffer m = ByteBuffer.allocate(offs + 4); m.put(packet, 0, offs); m.putInt((int) _roc); byte[] auth = new byte[alen]; System.arraycopy(packet, offs, auth, 0, alen); int mlen = (plen - 12) - alen; m.position(0); hmac.update(m); byte[] mac = hmac.doFinal(); for (int i = 0; i < alen; i++) { if (auth[i] != mac[i]) { throw new Exception("SRTPChannel:not authorized byte " + i + " does not match"); } } return false; } catch (Exception e) { JFLog.log(e); return true; } } protected long _roc = 0; // only used for inbound we _know_ the answer for outbound. //roc = rollover counter (counts everytime seqno(16bit) rolls over) protected int _s_l = 0; // only used for inbound we _know_ the answer for outbound. //s_l = seqno last seen long getIndex(int seqno) { long v = _roc; // default assumption // detect wrap(s) int diff = seqno - _s_l; // normally we expect this to be 1 if (diff < Short.MIN_VALUE) { // large negative offset so v = _roc + 1; // if the old value is more than 2^15 smaller // then we have wrapped } if (diff > Short.MAX_VALUE) { // big positive offset v = _roc - 1; // we wrapped recently and this is an older packet. } if (v < 0) { v = 0; // trap odd initial cases } /* if (_s_l < 32768) { v = ((seqno - _s_l) > 32768) ? (_roc - 1) % (1 << 32) : _roc; } else { v = ((_s_l - 32768) > seqno) ? (_roc + 1) % (1 << 32) : _roc; }*/ long low = (long) seqno; long high = ((long) v << 16); long ret = low | high; return ret; } void updateCounters(int seqno, long index) { //SRTPProtocolImpl // note that we have seen it. int tidx = (int) (index % SRTPWINDOWSIZE); _replay[tidx] = index; // and update the leading edge if needed. if (index > _windowLeadingEdge) { _windowLeadingEdge = index; } //RTPProtocolImpl // note that we have seen it. int diff = seqno - _s_l; // normally we expect this to be 1 if (diff < Short.MIN_VALUE) { // large negative offset so _roc++; // if the old value is more than 2^15 smaller // then we have wrapped } _s_l = seqno; } } /* Data Flowcharts: RTP Flow (0x80): media <---> SRTPContext <---> rtpSocket STUN Flow (0x00 or 0x01): stun <---> rtpSocket DTLS Flow (*): dtlsSocket <---> rawSocket <---> rtpSocket NOTE:DTLS:caller=client callee=server */