Home › Audio & video › Real-time communication
algorithm low

SRTP

Problem Two endpoints stream audio or video as small UDP packets that arrive late, lost, duplicated or reordered, and a third party on the path can read, alter or replay them. Each packet must be protected on its own.

Per-packet encryption, authentication and replay protection for RTP and RTCP that keeps the headers readable, with the packet index derived from the 16-bit sequence number.

per packet one AES pass over the payload plus one tag computation; index and replay check O(1), 2.5 ns measured
media-encryptionrtpreplay-protectiondtls-srtpreal-time
Overview

Datagram media cannot use a cipher with running state: packets are lost and reordered. SRTP (RFC 3711) makes each RTP packet self-contained: the header stays in the clear for routing and statistics, the payload is encrypted with a keystream derived from the packet's 48-bit index, and a tag is appended. The index is not sent: the receiver rebuilds it from the 16-bit sequence number and a local rollover counter.

Key facts

Use when

  • Any RTP stream over a network you do not control: WebRTC video calls, SIP/VoIP telephony, IP-camera streams, game voice chat, telemedicine.

Avoid when

  • A relay (SFU) must not see the media: SRTP is hop-by-hop; add SFrame (RFC 9605).
  • The transport already encrypts (TLS, QUIC): do not layer it.

Deep dive

How it works

In: an SRTP packet (RTP header, encrypted payload, tag) and the stream's context · Out: the plain RTP packet, or a drop · State: per SSRC: session keys k_e, k_a, k_s, ROC (32 bit), s_l (last sequence number), replay window (at least 64 packets) · Cost: one AES-CTR pass over the payload plus one HMAC (or one GCM pass); index and replay check O(1)

Receive path, RFC 3711 §3.3:

  1. Find the context by SSRC and destination.
  2. Estimate the index (Appendix A, signed arithmetic): if s_l < 32768, then v = ROC-1 when SEQ - s_l > 32768, else ROC; otherwise v = ROC+1 when s_l - 32768 > SEQ, else ROC. i = SEQ + v * 65536.
  3. Session keys, derived once per master key (rate 0, §4.3): x = (label || index DIV rate) XOR master_salt; key = AES-CM(master_key, IV = x * 2^16) truncated. Labels 0x00 k_e, 0x01 k_a, 0x02 k_s (SRTCP 0x03-0x05).
  4. Replay check against the window; drop if seen or too old, before any crypto.
  5. Verify the tag over M = header || ciphertext || ROC (HMAC-SHA1 truncated to the left-most 80 or 32 bits); drop on mismatch.
  6. Decrypt: IV = (k_s * 2^16) XOR (SSRC * 2^64) XOR (i * 2^16); keystream block j is AES(k_e, IV + j), XORed into the payload.
  7. Commit only now: v = ROC+1 sets s_l = SEQ, ROC = v; v = ROC raises s_l if SEQ is larger; v = ROC-1 changes nothing. Mark i in the window.
  8. Strip the tag (and optional MKI).

The sender: ROC starts at 0, increments when SEQ wraps; encrypt, then tag.

SRTCP carries its index explicitly: a 31-bit counter plus a 1-bit E flag (payload encrypted or not) after the compound packet; the mandatory tag covers both. Same master key, other labels, no estimation.

AES-GCM profile (RFC 7714): 12-byte IV = 2 zero bytes, SSRC (4), ROC (4), SEQ (2), XOR 12-byte salt; the RTP header is associated data; a full 16-byte tag, no separate HMAC (profiles SRTP_AEAD_AES_128_GCM, SRTP_AEAD_AES_256_GCM).

Keys: DTLS-SRTP (RFC 5764) runs a DTLS handshake on the media path and exports 2 * (key + salt) bytes under the label EXTRACTOR-dtls_srtp: client key, server key, client salt, server salt; one context per direction. SDES (RFC 4568) puts the key inside the SDP, so it is only as private as the signalling path; RFC 8827 says WebRTC must not offer or select it and must offer DTLS-SRTP on every media channel.

Through an SFU: SRTP is hop-by-hop; the SFU decrypts and re-encrypts. SFrame (RFC 9605) encrypts whole frames end to end, independent of RTP.

Code

Std only. AES is not implemented here (use a vetted library); the block cipher is a trait and XorStandIn is a stand-in that only tests the plumbing, never a cipher.

/// One SRTP stream (SSRC): RFC 3711 s_l/ROC plus a 64-packet replay window.
#[derive(Default)]
pub struct Rx { started: bool, roc: u32, s_l: u16, top: u64, mask: u64 }

impl Rx {
    /// Index estimate (RFC 3711 App. A) + replay check; `commit` only after the tag verified.
    pub fn check(&self, seq: u16) -> Option<(u32, u64)> {
        if !self.started { return Some((self.roc, (self.roc as u64) << 16 | seq as u64)); }
        let (s, q) = (self.s_l as i32, seq as i32);
        let v = if s < 32768 { if q - s > 32768 { self.roc.wrapping_sub(1) } else { self.roc } }
                else if s - 32768 > q { self.roc.wrapping_add(1) } else { self.roc };
        if self.roc == 0 && v == u32::MAX { return None; } // would precede the session start
        let i = (v as u64) << 16 | seq as u64;
        if i > self.top { return Some((v, i)); }
        let d = self.top - i;
        if d >= 64 || self.mask >> d & 1 == 1 { None } else { Some((v, i)) } // too old / replay
    }

    /// After the tag verified: update s_l, ROC (RFC 3711 s3.3.1) and the window.
    pub fn commit(&mut self, v: u32, i: u64, seq: u16) {
        if !self.started { self.started = true; self.s_l = seq; self.top = i; self.mask = 1; return; }
        if v == self.roc.wrapping_add(1) { self.s_l = seq; self.roc = v; }
        else if v == self.roc && seq > self.s_l { self.s_l = seq; } // v == ROC-1: no update
        if i > self.top {
            let sh = i - self.top;
            self.mask = if sh >= 64 { 1 } else { self.mask << sh | 1 };
            self.top = i;
        } else { self.mask |= 1 << (self.top - i); }
    }
}

/// IV = (k_s * 2^16) XOR (SSRC * 2^64) XOR (i * 2^16), RFC 3711 s4.1.1; k_s = 112-bit session salt.
pub fn iv(salt: &[u8; 14], ssrc: u32, index: u64) -> [u8; 16] {
    let mut s = [0u8; 16];
    s[..14].copy_from_slice(salt);
    (u128::from_be_bytes(s) ^ (ssrc as u128) << 64 ^ (index as u128) << 16).to_be_bytes()
}

pub trait BlockCipher { fn encrypt(&self, block: [u8; 16]) -> [u8; 16]; }

/// Segmented integer counter mode: keystream block j = E(k, IV + j mod 2^128).
pub fn ctr_xor<C: BlockCipher>(c: &C, iv: [u8; 16], data: &mut [u8]) {
    let mut ctr = u128::from_be_bytes(iv);
    for chunk in data.chunks_mut(16) {
        let ks = c.encrypt(ctr.to_be_bytes());
        chunk.iter_mut().zip(ks).for_each(|(b, k)| *b ^= k);
        ctr = ctr.wrapping_add(1);
    }
}

/// STAND-IN, NOT A CIPHER: XOR with a fixed block, only to test the plumbing without AES.
pub struct XorStandIn(pub [u8; 16]);
impl BlockCipher for XorStandIn {
    fn encrypt(&self, mut b: [u8; 16]) -> [u8; 16] { b.iter_mut().zip(self.0).for_each(|(x, k)| *x ^= k); b }
}

#[cfg(test)]
mod tests {
    use super::*;
    fn feed(r: &mut Rx, seqs: &[u16]) -> Vec<bool> {
        seqs.iter().map(|&s| r.check(s).map(|(v, i)| r.commit(v, i, s)).is_some()).collect()
    }
    #[test]
    fn wrap_late_replay_window() {
        let mut r = Rx::default();
        assert_eq!(feed(&mut r, &[65533, 65534, 0, 1]), [true; 4]);
        assert_eq!(r.check(2), Some((1, 65538)));        // ROC went 0 -> 1 at 65535 -> 0
        assert_eq!(r.check(65535), Some((0, 65535)));    // late packet from before the wrap: old ROC
        r.commit(0, 65535, 65535);
        assert_eq!(r.check(65535), None);                // replay
        let mut r = Rx::default();
        feed(&mut r, &[100, 200]);
        assert!(r.check(137).is_some() && r.check(136).is_none()); // 200-136 = 64: too old
    }
    #[test]
    fn iv_and_counter_mode() {
        let salt = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; // hand-computed XOR, ROC 1, SEQ 2
        assert_eq!(iv(&salt, 0xDEADBEEF, 0x1_0002),
            [1, 2, 3, 4, 0xDB, 0xAB, 0xB9, 0xE7, 9, 10, 11, 0x0D, 0x0D, 0x0C, 0, 0]);
        // RFC 3711 B.2: salt F0..FD, ROC 0, SEQ 0, SSRC 0 gives first counter F0F1..FD0000
        let b2 = [0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD];
        let ivb = iv(&b2, 0, 0);
        assert!(ivb[..14] == b2 && ivb[14..] == [0, 0]);
        let mut z = [0u8; 32];
        ctr_xor(&XorStandIn([0; 16]), ivb, &mut z);      // key 0: keystream = counter blocks
        assert!(z[..16] == ivb && z[30..] == [0, 1]);    // second counter ends ...0001
    }
}

Complexity and constants

  • Index estimate, replay check and commit: O(1), a handful of compares and one shift. Measured on this Mac (Apple M4 Pro), release build, single thread, packets 1 in 8 reordered: about 394 M checks/s (2.5 ns each, three runs 394.1-394.7 M/s). Cheap and branch-predictable; real traffic adds context-lookup cache misses.
  • State per stream in the code: ROC 4 B + s_l 2 B + top 8 B + window 8 B, plus session keys (16 B k_e, 20 B k_a, 14 B k_s).
  • Cost drivers: the AES pass and the HMAC, linear in packet size.
  • Limits per master key: 2^48 SRTP packets (RFC 3711 §3.3.1); 2^31 SRTCP packets (RFC 7714 profiles).

Efficiency

  • Cost: per packet: one AES-CTR pass over the payload, one HMAC-SHA1 over header plus payload (or one GCM pass), index and replay check 2.5 ns (measured on this Mac, Apple M4 Pro). Bound: compute per payload byte; it matters most for an SFU that re-encrypts every forwarded copy. Added bytes: 10 / 4 / 16.
  • Levers:
    • Replay check before crypto — no HMAC or AES spent on duplicates (RFC 3711 §3.3) — always.
    • AES-GCM profile instead of CM + HMAC — one pass, no auth key (RFC 7714) — when both ends support it.
    • -32 tag — 6 bytes less per packet (RFC 5764 profiles) — only where one forged packet is tolerable (RFC 3711 §7.5); RTCP keeps 80 bits.
    • Keys derived once per master key (rate 0, RFC 3711 §4.3.1) — no per-packet derivation — default.
    • SFrame — end-to-end without SRTP double encryption, which RFC 9605 §1 calls inefficient — group calls via an SFU.
  • Naive → standard → state of the art:
    • Keys derived per packet, software AES, separate HMAC · derivation dominates.
    • Keys derived once, AES-CM + HMAC-SHA1-80, hardware AES · two passes, +10 B.
    • AES-GCM (RFC 7714), hardware AES and carry-less multiply, SFrame for end-to-end · one pass, +16 B.

Implementation notes

  • Check replay and tag before decrypting, commit state only after the tag verified; RFC 3711 §3.3.1 warns the ROC cannot be made robust without authentication.
  • Window as a u64 bitmask relative to the highest authenticated index; a shift by 64 or more must reset to one bit (<< 64 panics in debug Rust).
  • Compare tags in constant time. One context per SSRC and direction.
  • At most 2^16 AES blocks per packet (IV reserves 16 counter bits): always true for UDP.

Pitfalls

  • Updating ROC/s_l from unauthenticated packets: one forged packet desynchronises the stream.
  • Reusing (key, SSRC, index) is a two-time pad (RFC 3711 §9.1): two senders with one SSRC and key, or a restart that resets SEQ and ROC.
  • ROC 0 with v = ROC-1 wraps to an index near 2^48: reject it (done in the code).
  • Resetting the ROC on re-keying: RFC 3711 forbids it.
  • -32 tags on stateful codecs: one forged packet can corrupt later decoding (RFC 3711 §7.5).

Variants and alternatives

AES-GCM / AES-CM + HMAC-SHA1: the GCM profile (RFC 7714) is the modern default; the CM profile is the baseline every WebRTC stack must support (RFC 8827).
SFrame (RFC 9605): end-to-end frame encryption through an SFU, on top of hop-by-hop SRTP.
SDES (RFC 4568): keys in SDP; only with protected signalling (SIP over TLS); forbidden for WebRTC.

In production

  • WebRTC must secure all media with SRTP keyed by DTLS-SRTP (RFC 8827 §6.5); SIP stacks negotiate it through SDP offer/answer.
  • The key derivation is the RFC 3711 AES-CM PRF, not HKDF; the handshake underneath is DTLS, a TLS relative.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
libsrtpCcisco/libsrtpapt install libsrtp2-dev————reference C library; pluggable crypto backend; no Homebrew formula, build with CMake
webrtc-srtpRustwebrtc-rs/webrtc (srtp/)cargo add webrtc-srtp————pure Rust SRTP/SRTCP
pion/srtpGopion/srtpgo get github.com/pion/srtp/v3————Go SRTP used by Pion WebRTC
str0mRustalgesten/str0mcargo add str0m————sans-IO WebRTC library incl. SRTP; crypto backend selectable

Sources

  • Baugher, McGrew, Naslund, Carrara, Norrman, The Secure Real-time Transport Protocol (SRTP), RFC 3711, 2004. rfc
  • McGrew, Igoe, AES-GCM Authenticated Encryption in the Secure Real-time Transport Protocol (SRTP), RFC 7714, 2015. rfc
  • McGrew, Rescorla, Datagram Transport Layer Security (DTLS) Extension to Establish Keys for the Secure Real-time Transport Protocol (SRTP), RFC 5764, 2010. rfc
  • Andreasen, Baugher, Wing, Session Description Protocol (SDP) Security Descriptions for Media Streams, RFC 4568, 2006. rfc
  • Rescorla, WebRTC Security Architecture, RFC 8827, 2021. rfc
  • Omara, Uberti, Murillo, Barnes (ed.), Fablet, Secure Frame (SFrame): Lightweight Authenticated Encryption for Real-Time Media, RFC 9605, 2024. rfc

Used by

Systems (7):

Alternatives and related