Skip to content

Commit ed2d8da

Browse files
James BottomleyJames Bottomley
authored andcommitted
Add cryptographic (SRTP) negotiation to SDP
Update SipAudioCall.java to do sdp negotiation of RTP/SAVP (if TLS is selected as the transport) first and fall back to RTP/AVP later. The standard (RFC 4568) says that only one cipher can be used for the transmission, so this is in effect a cipher agreement but that we must have separate keys for sending and recieving which are sent by both parties in SDP. For this reason, we have to have access to both the SDP we sent and the one we received. This patch looks bigger because it adds looping over the media type for the both RTP/AVP and RTP/SAVP case. Change-Id: I28eaf39199dea7a31497bace67085c7bf30c9792 Signed-off-by: James Bottomley <James.Bottomley@HansenPartnership.com>
1 parent 1800c4b commit ed2d8da

File tree

5 files changed

+418
-90
lines changed

5 files changed

+418
-90
lines changed

src/java/android/net/rtp/AudioStream.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.InetAddress;
2020
import java.net.SocketException;
2121

22+
import android.annotation.Nullable;
2223
/**
2324
* An AudioStream is a {@link RtpStream} which carrys audio payloads over
2425
* Real-time Transport Protocol (RTP). Two different classes are developed in
@@ -46,6 +47,9 @@ public class AudioStream extends RtpStream {
4647
private AudioCodec mCodec;
4748
private int mDtmfType = -1;
4849
private AudioGroup mGroup;
50+
private String mCipher = null;
51+
private byte[] mLocalKey = null;
52+
private byte[] mRemoteKey = null;
4953

5054
/**
5155
* Creates an AudioStream on the given local address. Note that the local
@@ -164,4 +168,31 @@ public void setDtmfType(int type) {
164168
}
165169
mDtmfType = type;
166170
}
171+
172+
@Nullable
173+
public String getCipher() {
174+
return mCipher;
175+
}
176+
177+
@Nullable
178+
public byte[] getLocalKey() {
179+
return mLocalKey;
180+
}
181+
182+
@Nullable
183+
public byte[] getRemoteKey() {
184+
return mRemoteKey;
185+
}
186+
187+
public void setCipher(@Nullable String cipher) {
188+
mCipher = cipher;
189+
}
190+
191+
public void setLocalKey(@Nullable byte[] localKey) {
192+
mLocalKey = localKey;
193+
}
194+
195+
public void setRemoteKey(@Nullable byte[] remoteKey) {
196+
mRemoteKey = remoteKey;
197+
}
167198
}

src/java/android/net/rtp/Crypto.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
/*
3+
* Copyright (C) 2023 James.Bottomley@HansenPartnership.com
4+
*/
5+
6+
package android.net.rtp;
7+
8+
import java.util.Arrays;
9+
import java.util.Base64;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Random;
13+
14+
import android.annotation.NonNull;
15+
16+
/**
17+
* This class defines a collection of encryptions to be used with
18+
* {@link AudioStream}s. Their parameters are designed to be exchanged
19+
* using Session Description Protocol (SDP) which is described in
20+
* RFC4566 and RFC4568 (security extensions including a=crypto:
21+
* parameter). The crypto strings come from RFC3711 (AES_CM) and
22+
* RFC7714 (AES_GCM) and the tag values match those defined by IANA
23+
* for DTLS. Note the media in a remote offer may not have tags
24+
* matching the IANA ones, so validity is by crypto string only.
25+
*
26+
* This class is simplified to eliminate components not used by
27+
* asterisk such as key lifetime and Master Key Index (MKI).
28+
*
29+
* @see AudioStream
30+
*/
31+
public class Crypto {
32+
/**
33+
* The RTP payload type of the encoding.
34+
*/
35+
private int tag;
36+
private final String cipher;
37+
private final byte[] key;
38+
/*
39+
* Should have lifetime and MKI here, but this has only been
40+
* tested with asterisk which doesn't do either
41+
*/
42+
43+
@NonNull
44+
private static final Crypto CM_32 = new Crypto(2, "AES_CM_128_HMAC_SHA1_32", 30);
45+
@NonNull
46+
private static final Crypto CM_80 = new Crypto(1, "AES_CM_128_HMAC_SHA1_80", 30);
47+
@NonNull
48+
private static final Crypto GCM_16 = new Crypto(7, "AEAD_AES_128_GCM", 28);
49+
@NonNull
50+
private static final Crypto GCM_8 = new Crypto(9, "AEAD_AES_128_GCM_8", 28);
51+
52+
@NonNull
53+
private static final Crypto[] sCryptos = { GCM_16, GCM_8, CM_80, CM_32 };
54+
55+
/*
56+
* This can be called with just a cipher suite, in which case we
57+
* populate the key or with an entire rest of line, in which case
58+
* we pick up the key from it (and ignore any lifetime or MKI
59+
* parameters)
60+
*/
61+
public Crypto(int tag, @NonNull String rest) {
62+
this.tag = tag;
63+
String[] splits = rest.split(" ");
64+
this.cipher = splits[0];
65+
66+
if (!splits[1].startsWith("inline:"))
67+
throw new IllegalArgumentException("SDP a=crypto: Key string does not start with inline: but \"" + splits[1] + "\"");
68+
String[] keys = splits[1].substring(7).split("\\|");
69+
this.key = Base64.getDecoder().decode(keys[0]);
70+
// ignore lifetime and MKI
71+
}
72+
73+
private Crypto(int tag, String cipher, int keylen) {
74+
this.tag = tag;
75+
this.key = new byte[keylen];
76+
new Random().nextBytes(this.key);
77+
this.cipher = cipher;
78+
}
79+
80+
/**
81+
* Returns system supported Cryptos
82+
*/
83+
@NonNull
84+
public static List<Crypto> getCryptos() {
85+
return Arrays.asList(sCryptos);
86+
}
87+
88+
public int getTag() {
89+
return tag;
90+
}
91+
92+
@NonNull
93+
public String getCipher() {
94+
return cipher;
95+
}
96+
97+
@NonNull
98+
public byte[] getKey() {
99+
return key;
100+
}
101+
102+
@NonNull
103+
public String getRest() {
104+
String bkey = new String(Base64.getEncoder().encode(key));
105+
106+
return cipher + " inline:" + bkey;
107+
}
108+
public boolean valid() {
109+
for (Crypto s : sCryptos) {
110+
if (cipher.equals(s.cipher))
111+
return true;
112+
}
113+
return false;
114+
}
115+
116+
public String toString() {
117+
return getTag() + " " + getRest();
118+
}
119+
}

src/java/android/net/sip/SimpleSessionDescription.java

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.Arrays;
2121
import java.util.Locale;
2222

23+
import android.net.rtp.Crypto;
24+
2325
/**
2426
* An object used to manipulate messages of Session Description Protocol (SDP).
2527
* It is mainly designed for the uses of Session Initiation Protocol (SIP).
@@ -113,7 +115,7 @@ public SimpleSessionDescription(String message) {
113115
* @param type The media type, e.g. {@code "audio"}.
114116
* @param port The first transport port used by this media.
115117
* @param portCount The number of contiguous ports used by this media.
116-
* @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
118+
* @param protocol The transport protocol, e.g. {@code "RTP/SAVP"}.
117119
*/
118120
public Media newMedia(String type, int port, int portCount,
119121
String protocol) {
@@ -122,6 +124,18 @@ public Media newMedia(String type, int port, int portCount,
122124
return media;
123125
}
124126

127+
/**
128+
* Creates a new media description in this session description.
129+
*
130+
* @param m The original Media
131+
* @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
132+
*/
133+
public Media newMediaNoCrypt(Media m, String protocol) {
134+
Media media = new Media(m, protocol);
135+
mMedia.add(media);
136+
return media;
137+
}
138+
125139
/**
126140
* Returns all the media descriptions in this session description.
127141
*/
@@ -240,6 +254,7 @@ public static class Media extends Fields {
240254
private final int mPortCount;
241255
private final String mProtocol;
242256
private ArrayList<String> mFormats = new ArrayList<String>();
257+
private ArrayList<Integer> mCryptoTags = new ArrayList<Integer>();
243258

244259
private Media(String type, int port, int portCount, String protocol) {
245260
super("icbka");
@@ -249,6 +264,21 @@ private Media(String type, int port, int portCount, String protocol) {
249264
mProtocol = protocol;
250265
}
251266

267+
private Media(Media m, String protocol) {
268+
super(m);
269+
270+
mType = m.mType;
271+
mPort = m.mPort;
272+
mPortCount = m.mPortCount;
273+
mProtocol = protocol;
274+
mFormats = m.mFormats;
275+
276+
// strip the "a=crypto:" lines
277+
int i;
278+
while ((i = super.find("a=crypto", ':')) != -1)
279+
mLines.remove(i);
280+
}
281+
252282
/**
253283
* Returns the media type.
254284
*/
@@ -357,6 +387,38 @@ public void setRtpPayload(int type, String rtpmap, String fmtp) {
357387
super.set("a=fmtp:" + format, ' ', fmtp);
358388
}
359389

390+
public void addCrypto(Crypto c) {
391+
mCryptoTags.remove(Integer.valueOf(c.getTag()));
392+
mCryptoTags.add(Integer.valueOf(c.getTag()));
393+
394+
super.set("a=crypto:" + c.getTag(), ' ', c.getRest());
395+
}
396+
397+
public ArrayList<Crypto> getCryptos() {
398+
ArrayList l = new ArrayList<Crypto>();
399+
for (Integer mI : mCryptoTags) {
400+
int i = mI.intValue();
401+
l.add(new Crypto(i, super.get("a=crypto:" + i, ' ')));
402+
}
403+
404+
return l;
405+
}
406+
407+
void parse(String line) {
408+
super.parse(line);
409+
if (!line.startsWith("a=crypto:")) {
410+
return;
411+
}
412+
String[] parts = line.split("[: ]");
413+
int tag = Integer.valueOf(parts[1]);
414+
if (tag <= 0) {
415+
throw new IllegalArgumentException("Invalid SDP: crypto tag: "
416+
+ line);
417+
}
418+
mCryptoTags.remove(Integer.valueOf(tag));
419+
mCryptoTags.add(Integer.valueOf(tag));
420+
}
421+
360422
/**
361423
* Removes a RTP payload and its {@code rtpmap} and {@code fmtp}
362424
* attributes.
@@ -389,10 +451,18 @@ private void write(StringBuilder buffer) {
389451
*/
390452
private static class Fields {
391453
private final String mOrder;
392-
private final ArrayList<String> mLines = new ArrayList<String>();
454+
protected final ArrayList<String> mLines;
393455

394456
Fields(String order) {
395457
mOrder = order;
458+
mLines = new ArrayList<String>();
459+
}
460+
461+
Fields(Fields f) {
462+
this(f.mOrder);
463+
for (String s : f.mLines) {
464+
mLines.add(s);
465+
}
396466
}
397467

398468
/**
@@ -524,13 +594,14 @@ private void write(StringBuilder buffer) {
524594
/**
525595
* Invokes {@link #set} after splitting the line into three parts.
526596
*/
527-
private void parse(String line) {
597+
void parse(String line) {
528598
char type = line.charAt(0);
529599
if (mOrder.indexOf(type) == -1) {
530600
return;
531601
}
532602
char delimiter = '=';
533-
if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) {
603+
if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")
604+
|| line.startsWith("a=crypto:")) {
534605
delimiter = ' ';
535606
} else if (type == 'b' || type == 'a') {
536607
delimiter = ':';

0 commit comments

Comments
 (0)