Sframe encryptor implementation This CL introduces the concrete Sframe encryptor that sits between the public key-management API and the media pipeline, implementing the sender-side wiring for Sframe end-to-end encryption. # High-level design The implementation follows a three-layer split that matches the existing WebRTC layering and respects the api/ visibility rules: 1. Public API layer (api/sframe/) - A narrow, stable interface that exposes only key management to applications: setting an encryption key on the sender side. 2. Internal media interface layer (modules/sframe/) - Extends the public key-management interface with the Encrypt and "max ciphertext size" entry points used by the media pipeline. This is the surface the media channel speaks to; application code never sees it. 3. Concrete impl layer (modules/sframe/) - Implements the media interface on top of the cisco/sframe library. Splitting key management from frame protection lets the public handle expose only the small surface applications need, while the media pipeline talks to a richer interface internally. Bug: webrtc:479862368 Change-Id: Id93dba5874b6f5b1f32723755dd404af202f6ecf Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/481301 Reviewed-by: Harald Alvestrand <hta@webrtc.org> Reviewed-by: Tomas Gunnarsson <tommi@webrtc.org> Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org> Cr-Commit-Position: refs/heads/main@{#48065}
diff --git a/modules/BUILD.gn b/modules/BUILD.gn index f0a150f..95e1af6 100644 --- a/modules/BUILD.gn +++ b/modules/BUILD.gn
@@ -84,6 +84,7 @@ "pacing:pacing_unittests", "remote_bitrate_estimator:remote_bitrate_estimator_unittests", "rtp_rtcp:rtp_rtcp_unittests", + "sframe:sframe_unittests", "video_coding:video_coding_unittests", "video_coding/timing:timing_unittests", ]
diff --git a/modules/sframe/BUILD.gn b/modules/sframe/BUILD.gn new file mode 100644 index 0000000..91708f5 --- /dev/null +++ b/modules/sframe/BUILD.gn
@@ -0,0 +1,53 @@ +# Copyright 2026 The WebRTC project authors. All Rights Reserved. +# +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file in the root of the source +# tree. An additional intellectual property rights grant can be found +# in the file PATENTS. All contributing project authors may +# be found in the AUTHORS file in the root of the source tree. + +import("../../webrtc.gni") + +rtc_library("sframe_media_encryptor_interface") { + visibility = [ "*" ] + sources = [ "sframe_media_encryptor_interface.h" ] + deps = [ + "../../api:rtc_error", + "../../api:sframe_encryptor_interface", + "../../api:sframe_types", + ] +} + +rtc_library("sframe_encryptor") { + visibility = [ "*" ] + sources = [ + "sframe_encryptor.cc", + "sframe_encryptor.h", + ] + deps = [ + ":sframe_media_encryptor_interface", + "../../api:make_ref_counted", + "../../api:rtc_error", + "../../api:scoped_refptr", + "../../api:sequence_checker", + "../../api:sframe_types", + "../../rtc_base:macromagic", + "../../rtc_base/system:no_unique_address", + "//third_party/abseil-cpp/absl/base:nullability", + "//third_party/sframe", + ] +} + +if (rtc_include_tests) { + rtc_library("sframe_unittests") { + testonly = true + sources = [ "sframe_encryptor_decryptor_unittest.cc" ] + deps = [ + ":sframe_encryptor", + "../../api:rtc_error", + "../../api:scoped_refptr", + "../../api:sframe_types", + "../../test:test_support", + ] + } +}
diff --git a/modules/sframe/DEPS b/modules/sframe/DEPS new file mode 100644 index 0000000..927f4d2 --- /dev/null +++ b/modules/sframe/DEPS
@@ -0,0 +1,3 @@ +include_rules = [ + "+third_party/sframe", +] \ No newline at end of file
diff --git a/modules/sframe/sframe_encryptor.cc b/modules/sframe/sframe_encryptor.cc new file mode 100644 index 0000000..495c0fc --- /dev/null +++ b/modules/sframe/sframe_encryptor.cc
@@ -0,0 +1,113 @@ +/* + * Copyright 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "modules/sframe/sframe_encryptor.h" + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <optional> +#include <span> + +#include "api/make_ref_counted.h" +#include "api/rtc_error.h" +#include "api/scoped_refptr.h" +#include "api/sequence_checker.h" +#include "api/sframe/sframe_types.h" +#include "third_party/sframe/src/include/sframe/result.h" +#include "third_party/sframe/src/include/sframe/sframe.h" + +namespace webrtc { + +namespace { +sframe::CipherSuite ToSframeCipherSuite(SframeCipherSuite suite) { + switch (suite) { + case SframeCipherSuite::kAes128CtrHmacSha256_80: + return sframe::CipherSuite::AES_128_CTR_HMAC_SHA256_80; + case SframeCipherSuite::kAes128CtrHmacSha256_64: + return sframe::CipherSuite::AES_128_CTR_HMAC_SHA256_64; + case SframeCipherSuite::kAes128CtrHmacSha256_32: + return sframe::CipherSuite::AES_128_CTR_HMAC_SHA256_32; + case SframeCipherSuite::kAes128GcmSha256_128: + return sframe::CipherSuite::AES_GCM_128_SHA256; + case SframeCipherSuite::kAes256GcmSha512_128: + return sframe::CipherSuite::AES_GCM_256_SHA512; + } +} +} // namespace + +scoped_refptr<SframeEncryptor> SframeEncryptor::Create( + SframeMode mode, + SframeCipherSuite cipher_suite) { + return make_ref_counted<SframeEncryptor>(mode, cipher_suite); +} + +SframeEncryptor::SframeEncryptor(SframeMode mode, + SframeCipherSuite cipher_suite) + : sequence_checker_(SequenceChecker::kDetached), + mode_(mode), + context_(std::make_unique<sframe::Context>( + ToSframeCipherSuite(cipher_suite))) {} + +SframeEncryptor::~SframeEncryptor() = default; + +RTCError SframeEncryptor::SetEncryptionKey( + uint64_t key_id, + std::span<const uint8_t> key_material) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + sframe::Result<void> result = context_->add_key( + key_id, sframe::KeyUsage::protect, + sframe::input_bytes(key_material.data(), key_material.size())); + if (result.is_err()) { + RTCError error = RTCError::InternalError("Failed to set encryption key"); + if (const char* message = result.error().message()) { + error.string_builder() << ": " << message; + } + return error; + } + + if (active_key_id_ && *active_key_id_ != key_id) { + context_->remove_key(*active_key_id_); + } + + active_key_id_ = key_id; + + return RTCError::OK(); +} + +RTCErrorOr<size_t> SframeEncryptor::Encrypt( + std::span<const uint8_t> frame, + std::span<const uint8_t> additional_data, + std::span<uint8_t> encrypted_frame) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + if (!active_key_id_) { + return RTCError::InvalidState("Sframe encryption key not set"); + } + + auto result = context_->protect( + *active_key_id_, + sframe::output_bytes(encrypted_frame.data(), encrypted_frame.size()), + sframe::input_bytes(frame.data(), frame.size()), + sframe::input_bytes(additional_data.data(), additional_data.size())); + if (result.is_err()) { + RTCError error = RTCError::InternalError("Sframe encryption failed"); + if (const char* message = result.error().message()) { + error.string_builder() << ": " << message; + } + return error; + } + return result.value().size(); +} + +size_t SframeEncryptor::GetMaxCiphertextByteSize(size_t frame_size) { + return frame_size + sframe::Context::max_overhead; +} + +} // namespace webrtc
diff --git a/modules/sframe/sframe_encryptor.h b/modules/sframe/sframe_encryptor.h new file mode 100644 index 0000000..8993d80 --- /dev/null +++ b/modules/sframe/sframe_encryptor.h
@@ -0,0 +1,70 @@ +/* + * Copyright 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_SFRAME_SFRAME_ENCRYPTOR_H_ +#define MODULES_SFRAME_SFRAME_ENCRYPTOR_H_ + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <optional> +#include <span> + +#include "absl/base/nullability.h" +#include "api/rtc_error.h" +#include "api/scoped_refptr.h" +#include "api/sequence_checker.h" +#include "api/sframe/sframe_types.h" +#include "modules/sframe/sframe_media_encryptor_interface.h" +#include "rtc_base/system/no_unique_address.h" +#include "rtc_base/thread_annotations.h" + +namespace sframe { +class Context; +} // namespace sframe + +namespace webrtc { + +class SframeEncryptor : public SframeMediaEncryptorInterface { + public: + // Creates a new SframeEncryptor. This factory method never fails. + static absl_nonnull scoped_refptr<SframeEncryptor> Create( + SframeMode mode, + SframeCipherSuite cipher_suite); + + SframeEncryptor(SframeMode mode, SframeCipherSuite cipher_suite); + ~SframeEncryptor() override; + + // SframeEncryptorInterface implementation. + RTCError SetEncryptionKey(uint64_t key_id, + std::span<const uint8_t> key_material) override; + + // SframeMediaEncryptorInterface implementation. + RTCErrorOr<size_t> Encrypt(std::span<const uint8_t> frame, + std::span<const uint8_t> additional_data, + std::span<uint8_t> encrypted_frame) override; + + size_t GetMaxCiphertextByteSize(size_t frame_size) override; + + SframeMode mode() const override { return mode_; } + + private: + // Callers must use this object from a single sequence. Today that sequence + // is the media-pipeline (worker) thread reached via the signaling thread. + RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_; + + const SframeMode mode_; + std::unique_ptr<sframe::Context> context_ RTC_GUARDED_BY(sequence_checker_); + std::optional<uint64_t> active_key_id_ RTC_GUARDED_BY(sequence_checker_); +}; + +} // namespace webrtc + +#endif // MODULES_SFRAME_SFRAME_ENCRYPTOR_H_
diff --git a/modules/sframe/sframe_encryptor_decryptor_unittest.cc b/modules/sframe/sframe_encryptor_decryptor_unittest.cc new file mode 100644 index 0000000..f0b1d9f --- /dev/null +++ b/modules/sframe/sframe_encryptor_decryptor_unittest.cc
@@ -0,0 +1,98 @@ +/* + * Copyright 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include <cstddef> +#include <cstdint> +#include <span> +#include <vector> + +#include "api/rtc_error.h" +#include "api/scoped_refptr.h" +#include "api/sframe/sframe_types.h" +#include "modules/sframe/sframe_encryptor.h" +#include "test/gtest.h" + +namespace webrtc { +namespace { + +constexpr uint64_t kKeyId = 7; +const std::vector<uint8_t> kKeyMaterial = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f}; +const std::vector<uint8_t> kPlaintext = {0xde, 0xad, 0xbe, 0xef, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06}; + +// TODO(webrtc:479862368): Add SframeDecryptor and couple it with the encryptor. +class SframeEncryptorDecryptorTest : public ::testing::Test { + protected: + SframeEncryptorDecryptorTest() + : encryptor_( + SframeEncryptor::Create(SframeMode::kPerFrame, + SframeCipherSuite::kAes128GcmSha256_128)) {} + + scoped_refptr<SframeEncryptor> encryptor_; +}; + +TEST_F(SframeEncryptorDecryptorTest, SetEncryptionKeySucceeds) { + EXPECT_TRUE(encryptor_->SetEncryptionKey(kKeyId, kKeyMaterial).ok()); +} + +TEST_F(SframeEncryptorDecryptorTest, EncryptProducesCiphertext) { + ASSERT_TRUE(encryptor_->SetEncryptionKey(kKeyId, kKeyMaterial).ok()); + + size_t max_ct_size = encryptor_->GetMaxCiphertextByteSize(kPlaintext.size()); + std::vector<uint8_t> ciphertext(max_ct_size); + + auto result = encryptor_->Encrypt(kPlaintext, /*additional_data=*/{}, + std::span<uint8_t>(ciphertext)); + ASSERT_TRUE(result.ok()); + EXPECT_GT(result.value(), kPlaintext.size()); +} + +TEST_F(SframeEncryptorDecryptorTest, EncryptFailsWithoutKey) { + // No key set — encryption should fail with INVALID_STATE. + size_t max_ct_size = encryptor_->GetMaxCiphertextByteSize(kPlaintext.size()); + std::vector<uint8_t> ciphertext(max_ct_size); + auto result = encryptor_->Encrypt(kPlaintext, /*additional_data=*/{}, + std::span<uint8_t>(ciphertext)); + ASSERT_FALSE(result.ok()); + EXPECT_EQ(result.error().type(), RTCErrorType::INVALID_STATE); +} + +TEST_F(SframeEncryptorDecryptorTest, MultipleKeyRotation) { + constexpr uint64_t kKeyId2 = 42; + const std::vector<uint8_t> key2 = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, + 0x1c, 0x1d, 0x1e, 0x1f}; + + ASSERT_TRUE(encryptor_->SetEncryptionKey(kKeyId, kKeyMaterial).ok()); + size_t max_ct_size = encryptor_->GetMaxCiphertextByteSize(kPlaintext.size()); + std::vector<uint8_t> ct1(max_ct_size); + ASSERT_TRUE( + encryptor_ + ->Encrypt(kPlaintext, /*additional_data=*/{}, std::span<uint8_t>(ct1)) + .ok()); + + // Rotate to second key and encrypt again. + ASSERT_TRUE(encryptor_->SetEncryptionKey(kKeyId2, key2).ok()); + std::vector<uint8_t> ct2(max_ct_size); + ASSERT_TRUE( + encryptor_ + ->Encrypt(kPlaintext, /*additional_data=*/{}, std::span<uint8_t>(ct2)) + .ok()); + EXPECT_NE(ct1, ct2); +} + +TEST_F(SframeEncryptorDecryptorTest, GetMaxCiphertextByteSizeIsLarger) { + EXPECT_GT(encryptor_->GetMaxCiphertextByteSize(100), 100u); +} + +} // namespace +} // namespace webrtc
diff --git a/modules/sframe/sframe_media_encryptor_interface.h b/modules/sframe/sframe_media_encryptor_interface.h new file mode 100644 index 0000000..ce0f9d6 --- /dev/null +++ b/modules/sframe/sframe_media_encryptor_interface.h
@@ -0,0 +1,42 @@ +/* + * Copyright 2026 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_SFRAME_SFRAME_MEDIA_ENCRYPTOR_INTERFACE_H_ +#define MODULES_SFRAME_SFRAME_MEDIA_ENCRYPTOR_INTERFACE_H_ + +#include <cstddef> +#include <cstdint> +#include <span> + +#include "api/rtc_error.h" +#include "api/sframe/sframe_encryptor_interface.h" +#include "api/sframe/sframe_types.h" + +namespace webrtc { + +// Internal media pipeline interface that extends the public key management +// interface with the actual encrypt operation. +class SframeMediaEncryptorInterface : public SframeEncryptorInterface { + public: + virtual SframeMode mode() const = 0; + + virtual RTCErrorOr<size_t> Encrypt(std::span<const uint8_t> frame, + std::span<const uint8_t> additional_data, + std::span<uint8_t> encrypted_frame) = 0; + + virtual size_t GetMaxCiphertextByteSize(size_t frame_size) = 0; + + protected: + ~SframeMediaEncryptorInterface() override = default; +}; + +} // namespace webrtc + +#endif // MODULES_SFRAME_SFRAME_MEDIA_ENCRYPTOR_INTERFACE_H_