Нет видео на экране при звонке с iOS на iOS по зашифрованному каналу с WebRTC.
Есть приложение, которое работает на Android и iOS и имеет вызовы WebRTC. На Android голос и видео зашифрованы с помощью WebView с пользовательским кодом JS, а на iOS есть библиотека WebRTC от Google с дополнительной возможностью использования шифрования (с использованием FrameEncryptorInterface и FrameDecryptorInterface в качестве основы).
Ситуация:
Сбоев нет, вроде все должно работать, но видео с iOS на iOS по-прежнему не показывается на экране.
Так как используется так много компонентов на разных языках (Typescript, Haskell, Swift), я привожу основную их часть:
Android (источник):
function callCryptoFunction(): CallCrypto {
const initialPlainTextRequired = {
key: 10,
delta: 3,
empty: 1,
}
const IV_LENGTH = 12
function encryptFrame(key: CryptoKey): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
return async (frame, controller) => {
const data = new Uint8Array(frame.data)
const n = initialPlainTextRequired[frame.type] || 1
const iv = randomIV()
const initial = data.subarray(0, n)
const plaintext = data.subarray(n, data.byteLength)
try {
const ciphertext = plaintext.length
? new Uint8Array(await crypto.subtle.encrypt({name: "AES-GCM", iv: iv.buffer}, key, plaintext))
: new Uint8Array(0)
frame.data = concatN(initial, ciphertext, iv).buffer
controller.enqueue(frame)
} catch (e) {
console.info(`encryption error ${e}`)
throw e
}
}
}
function decryptFrame(key: CryptoKey): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
return async (frame, controller) => {
const data = new Uint8Array(frame.data)
const n = initialPlainTextRequired[frame.type] || 1
const initial = data.subarray(0, n)
const ciphertext = data.subarray(n, data.byteLength - IV_LENGTH)
const iv = data.subarray(data.byteLength - IV_LENGTH, data.byteLength)
try {
const plaintext = ciphertext.length
? new Uint8Array(await crypto.subtle.decrypt({name: "AES-GCM", iv}, key, ciphertext))
: new Uint8Array(0)
frame.data = concatN(initial, plaintext).buffer
controller.enqueue(frame)
} catch (e) {
console.info(`decryption error ${e}`)
throw e
}
}
}
function decodeAesKey(aesKey: string): Promise<CryptoKey> {
const keyData = callCrypto.decodeBase64url(callCrypto.encodeAscii(aesKey))
return crypto.subtle.importKey("raw", keyData!, {name: "AES-GCM", length: 256}, true, ["encrypt", "decrypt"])
}
function randomIV() {
return crypto.getRandomValues(new Uint8Array(IV_LENGTH))
}
//...
}
Дополнения встроенной библиотеки WebRTC для iOS (источник):
// ДЕКРИПТОР
#import <Foundation/Foundation.h>
#import "RTCFrameDecryptor+Private.h"
#import "base/RTCLogging.h"
#include "rtc_base/checks.h"
#include "api/crypto/frame_decryptor_interface.h"
#include "RTCRtpReceiver+Private.h"
namespace webrtc {
RTCFrameDecryptorAdapter::RTCFrameDecryptorAdapter(RTC_OBJC_TYPE(RTCFrameDecryptor) * decryptor, size_t sizeChange) {
RTC_CHECK(decryptor);
decryptor_ = decryptor;
sizeChange_ = sizeChange;
}
RTCFrameDecryptorAdapter::Result RTCFrameDecryptorAdapter::Decrypt(
cricket::MediaType media_type,
const std::vector<uint32_t>& csrcs,
rtc::ArrayView<const uint8_t> additional_data,
rtc::ArrayView<const uint8_t> encrypted_frame,
rtc::ArrayView<uint8_t> frame) {
RTC_OBJC_TYPE(RTCFrameDecryptor) *decryptor = decryptor_;
NSData *encrypted = [NSData dataWithBytes: encrypted_frame.data() length: encrypted_frame.size()];
NSData *decrypted = [decryptor.delegate frameDecryptor:decryptor mediaType:[RTC_OBJC_TYPE(RTCRtpReceiver) mediaTypeForNativeMediaType: media_type] withFrame:encrypted];
if (decrypted) {
const char * decryptedBytes = (const char *)[decrypted bytes];
NSUInteger size = [decrypted length];
for (size_t i = 0; i < size; i++) {
frame[i] = decryptedBytes[i];
}
return Result(Status::kOk, size);
} else {
return Result(Status::kFailedToDecrypt, 0);
}
}
size_t RTCFrameDecryptorAdapter::GetMaxPlaintextByteSize(
cricket::MediaType media_type,
size_t encrypted_frame_size) {
return encrypted_frame_size + sizeChange_;
}
} // namespace webrtc
@implementation RTC_OBJC_TYPE (RTCFrameDecryptor) {
rtc::scoped_refptr<webrtc::FrameDecryptorInterface> _nativeFrameDecryptor;
}
@synthesize delegate = _delegate;
@synthesize nativeFrameDecryptor = _nativeFrameDecryptor;
- (NSString *)description {
return [NSString
stringWithFormat:@"RTC_OBJC_TYPE(RTCFrameDecryptor)"];
}
- (void)dealloc {
_nativeFrameDecryptor = nullptr;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (object == nil) {
return NO;
}
if (![object isMemberOfClass:[self class]]) {
return NO;
}
RTC_OBJC_TYPE(RTCFrameDecryptor) *decryptor = (RTC_OBJC_TYPE(RTCFrameDecryptor) *)object;
return _nativeFrameDecryptor == decryptor.nativeFrameDecryptor;
}
- (NSUInteger)hash {
return (NSUInteger)_nativeFrameDecryptor.get();
}
- (instancetype)initWithSizeChange:(int)sizeChange {
if (self = [super init]) {
RTCLogInfo(@"RTC_OBJC_TYPE(RTCFrameDecryptor)(%p): created decryptor: %@", self, self.description);
_nativeFrameDecryptor = new webrtc::RTCFrameDecryptorAdapter(self, sizeChange);
}
return self;
}
@end
// ШИФРОВАТЕЛЬ
#import <Foundation/Foundation.h>
#import "RTCFrameEncryptor+Private.h"
#import "base/RTCLogging.h"
#include "rtc_base/checks.h"
#include "api/crypto/frame_encryptor_interface.h"
#include "RTCRtpReceiver+Private.h"
namespace webrtc {
RTCFrameEncryptorAdapter::RTCFrameEncryptorAdapter(RTC_OBJC_TYPE(RTCFrameEncryptor) * encryptor, size_t sizeChange) {
RTC_CHECK(encryptor);
encryptor_ = encryptor;
sizeChange_ = sizeChange;
}
int RTCFrameEncryptorAdapter::Encrypt(
cricket::MediaType media_type,
uint32_t ssrc,
rtc::ArrayView<const uint8_t> additional_data,
rtc::ArrayView<const uint8_t> frame,
rtc::ArrayView<uint8_t> encrypted_frame,
size_t* bytes_written) {
RTC_OBJC_TYPE(RTCFrameEncryptor) *encryptor = encryptor_;
NSData *unencrypted = [NSData dataWithBytes: frame.data() length: frame.size()];
NSData *encrypted = [encryptor.delegate frameEncryptor:encryptor mediaType:[RTC_OBJC_TYPE(RTCRtpReceiver) mediaTypeForNativeMediaType: media_type] withFrame:unencrypted];
if (encrypted) {
const char *encryptedBytes = (const char *)[encrypted bytes];
NSUInteger size = [encrypted length];
for (size_t i = 0; i < size; i++) {
encrypted_frame[i] = encryptedBytes[i];
}
*bytes_written = size;
return 0;
} else {
return 1;
}
}
size_t RTCFrameEncryptorAdapter::GetMaxCiphertextByteSize(
cricket::MediaType media_type,
size_t frame_size) {
return frame_size + sizeChange_;
}
} // namespace webrtc
@implementation RTC_OBJC_TYPE (RTCFrameEncryptor) {
rtc::scoped_refptr<webrtc::FrameEncryptorInterface> _nativeFrameEncryptor;
}
@synthesize delegate = _delegate;
@synthesize nativeFrameEncryptor = _nativeFrameEncryptor;
- (NSString *)description {
return [NSString
stringWithFormat:@"RTC_OBJC_TYPE(RTCFrameEncryptor)"];
}
- (void)dealloc {
_nativeFrameEncryptor = nullptr;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (object == nil) {
return NO;
}
if (![object isMemberOfClass:[self class]]) {
return NO;
}
RTC_OBJC_TYPE(RTCFrameEncryptor) *encryptor = (RTC_OBJC_TYPE(RTCFrameEncryptor) *)object;
return _nativeFrameEncryptor == encryptor.nativeFrameEncryptor;
}
- (NSUInteger)hash {
return (NSUInteger)_nativeFrameEncryptor.get();
}
- (instancetype)initWithSizeChange:(int)sizeChange {
if (self = [super init]) {
RTCLogInfo(@"RTC_OBJC_TYPE(RTCFrameEncryptor)(%p): created encryptor: %@", self, self.description);
_nativeFrameEncryptor = new webrtc::RTCFrameEncryptorAdapter(self, sizeChange);
}
return self;
}
@end
iOS Swift (источник):
// WebRTCClient.swift
private static let ivTagBytes: Int = 28 // iv + tag, 12 + 16
let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
guard encrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
let isKeyFrame = encrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))
return Data(bytes: pointer, count: encrypted.count - WebRTCClient.ivTagBytes)
} else {
return nil
}
}
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
guard unencrypted.count > 0 else { return nil }
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
let isKeyFrame = unencrypted[0] & 1 == 0
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
} else {
return nil
}
}
Часть шифрования/дешифрования iOS (источник):
chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString
chatEncryptMedia keyStr frame = do
len <- checkFrameLen frame
key <- decodeKey keyStr
iv <- liftIO C.randomGCMIV
(tag, frame') <- withExceptT show $ C.encryptAESNoPad key iv $ B.take len frame
pure $ frame' <> BA.convert (C.unAuthTag tag) <> C.unGCMIV iv
chatDecryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString
chatDecryptMedia keyStr frame = do
len <- checkFrameLen frame
key <- decodeKey keyStr
let (frame', rest) = B.splitAt len frame
(tag, iv) = B.splitAt C.authTagSize rest
authTag = C.AuthTag $ AES.AuthTag $ BA.convert tag
withExceptT show $ do
iv' <- liftEither $ C.gcmIV iv
frame'' <- C.decryptAESNoPad key iv' frame' authTag
pure $ frame'' <> framePad
checkFrameLen :: ByteString -> ExceptT String IO Int
checkFrameLen frame = do
let len = B.length frame - reservedSize
when (len < 0) $ throwError "frame has no [reserved space for] IV and/or auth tag"
pure len
{-# INLINE checkFrameLen #-}
decodeKey :: ByteString -> ExceptT String IO C.Key
decodeKey = liftEither . bimap ("invalid key: " <>) C.Key . U.decode
{-# INLINE decodeKey #-}
reservedSize :: Int
reservedSize = C.authTagSize + C.gcmIVSize
framePad :: ByteString
framePad = B.replicate reservedSize 0
Пожалуйста, нажмите на ссылку «исходный код», чтобы просмотреть весь код, если вам нужно больше контекста.
У вас есть идея, почему нет видео на экране при вызове iOS-to-iOS?
Я пытался:
sizeChange
, но приложение вылетает или после этого ничего не делает (зависит от увеличения или уменьшения значения)logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))
и
logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
После этого все работает нормально, но шифрования в данном случае, естественно, нет.
Ответом была только эта одна строка. По какой-то причине, даже при звонках между iOS-устройствами, мне приходится указывать в RTCDefaultVideoDecoderFactory PreferredCodec. Так:
private static let factory: RTCPeerConnectionFactory = {
RTCInitializeSSL()
let videoEncoderFactory = RTCDefaultVideoEncoderFactory()
let videoDecoderFactory = RTCDefaultVideoDecoderFactory()
+ videoEncoderFactory.preferredCodec = RTCVideoCodecInfo(name: kRTCVp8CodecName)
return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
}()
Теперь все работает нормально.