媒体流加密
在实时音视频互动中,开发者可选择对媒体流进行加密,从而保障用户的数据安全。融云提供两套加密方案:
- SDK 内置 SRTP 安全实时传输协议,即协议层的标准加密方式。以开关形式提供,使用简单。
 - 开发者对媒体数据的自定义加密。即加解密完全由开发者实现,融云服务只对数据做转发,适用于对安全性有特殊要求的客户。此类加密方式,服务端无法做合流处理,所以不适用于直播。使用自定义加密后,无法使用部分 RTC 服务端功能,包括:云端录制、云端截图、内容审核、云播放器。
 
- 若使用自定义加密,需要确保发送端和接收端的加解密算法一致,否则会无法正常通话。
 - 自定义加密,可分别针对音频或视频的原始数据执行,音频和视频的加密算法可以独立设置,或对其中之一进行设置。
 - 由于数据量大,考虑性能原因,自定义的加密算法需要在 C++ 层实现。
 
注意
无论何种加密方式,都会对客户端、服务器造成额外的资源消耗,在低性能设备上可能会影响体验。
自定义加密
鸿蒙音视频通话想要实现自定义加解密,需要实现 C++ 层提供的加解密协议方法,通过构建加密解密实例,在开始呼叫前调用
setEncryptor 和 setDecryptor 方法将指针传入 CallPlus 即可实现音视频数据加解密。
步骤 1 创建加解密模块
打开在项目里面创建一个 C++ 模块。

新增命名 rtc_custom_crypto_interface.h 的 C++ 头文件,增加加解密需要实现的协议方法声明。

具体增加 RTCCustomEncryptorInterface 加密协议和 RTCCustomDecryptorInterface 解密协议,代码如下。
#pragma once
#include <cstdint>
#include <cstddef>
/** 
 * 开发者实现该接口实现自定义加密,随 RTCLib 提供
**/
class RTCCustomEncryptorInterface {
public:
    virtual ~RTCCustomEncryptorInterface() = default;
    /**
     * 开发者自定义加密方法
     * @param  payload_data 加密前的数据起始地址
     * @param  payload_size 加密前的数据大小
     * @param  encrypted_frame 加密后的数据起始地址,融云SDK已申请内存,开发者无需重新申请
     * @param  bytes_written 加密后数据的大小
     * @param  media_stream_id 当前音频或视频流的名称
     * @param  媒体类型,0为"audio" 1为"video"
     * @return  0:成功,非0:失败
    **/
    virtual int Encrypt(const uint8_t* payload_data, size_t payload_size,
                        uint8_t* encrypted_frame, size_t* bytes_written,
                        const char* media_stream_id, int media_type) = 0;
    /**
     * 计算加密后数据的长度
     * @param frame_size 明文大小
     * @param  media_stream_id 当前音频或视频流的名称
     * @param  media_type 媒体类型,0为"audio" 1为"video"
     * @return size_t 密文长度
    **/
    virtual size_t GetMaxCiphertextByteSize(size_t frame_size, const char* media_stream_id, int media_type) = 0;
};
/** 
 * 开发者实现该接口实现自定义解密,随 RTCLib 提供
**/
class RTCCustomDecryptorInterface {
public:
    virtual ~RTCCustomDecryptorInterface() = default;
    /**
     * 开发者定义解密方法
     * @param  encrypted_frame 解密前的数据起始地址
     * @param  encrypted_frame_size 解密前的数据大小
     * @param  frame 解密后的数  据起始地址,融云SDK已申请内存,开发者无需重新申请
     * @param  bytes_written 解密后数据的大小
     * @param  media_stream_id 当前音频或视频流的名称
     * @param  media_type 媒体类型,0为"audio" 1为"video"
     * @return  0:成功,非0:失败
    **/
    virtual int Decrypt(const uint8_t* encrypted_frame, size_t encrypted_frame_size,
                        uint8_t* frame, size_t* bytes_written, const char* media_stream_id,
                        int media_type) = 0;
    /**
     * 计算解密后数据的长度
     * @param frame_size 密文大小
     * @param  media_stream_id 当前音频或视频流的名称
     * @param  media_type 媒体类型,0为"audio" 1为"video"
     * @return size_t 明文长度
     **/
    virtual size_t GetMaxPlaintextByteSize(size_t frame_size, const char* media_stream_id, int media_type) = 0;
};
步骤 2 实现加密方法
新增加密 NapiEncryptor C++ 类,在 .h 引入 rtc_custom_crypto_interface.h 实现 RTCCustomEncryptorInterface 的加密方法,代码如下。
#pragma once
#include <napi/native_api.h>
#include "rtc_custom_crypto_interface.h"
class NapiEncryptor : public RTCCustomEncryptorInterface {
public:
    NapiEncryptor();
    ~NapiEncryptor();
    static napi_value Init(napi_env env, napi_value exports);
    static napi_value New(napi_env env, napi_callback_info info);
    static void Destructor(napi_env env, void* nativeObject, void* finalize_hint);
    
public:
    static napi_value GetEncryptor(napi_env env, napi_callback_info info);
    
    int Encrypt(const uint8_t* payload_data, size_t payload_size,
                uint8_t* encrypted_frame, size_t* bytes_written,
                const char* media_stream_id, int media_type) override;
    size_t GetMaxCiphertextByteSize(size_t frame_size, const char* media_stream_id, int media_type) override;
    
private:
    napi_env env_;
    napi_ref wrapper_;
};
在 NapiEncryptor.cpp 实现音视频流加密方法,以下示例代码中采用对数据的异或方式加密。
#include "napi/native_api.h"
#include "NapiEncryptor.h"
static thread_local napi_ref g_ref = nullptr;
NapiEncryptor::NapiEncryptor() : env_(nullptr), wrapper_(nullptr) {
    
}
NapiEncryptor::~NapiEncryptor() {
    napi_delete_reference(env_, wrapper_);
}
napi_value NapiEncryptor::Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"encryptor", nullptr, nullptr, NapiEncryptor::GetEncryptor, nullptr, nullptr, napi_default, nullptr}
    };
    int desc_count = sizeof(desc) / sizeof(desc[0]);
    napi_value cons = nullptr;
    if (napi_define_class(env, "NapiEncryptor", NAPI_AUTO_LENGTH, New, nullptr,
                          desc_count, desc, &cons) != napi_ok) {
        return nullptr;
    }
    if (napi_create_reference(env, cons, 1, &g_ref) != napi_ok) {
        return nullptr;
    }
    if (napi_set_named_property(env, exports, "NapiEncryptor", cons) != napi_ok) {
        return nullptr;
    }
    return exports;
}
napi_value NapiEncryptor::New(napi_env env, napi_callback_info info) {
    napi_value newTarget = nullptr;
    napi_get_new_target(env, info, &newTarget);
    if (newTarget != nullptr) {
        napi_value jsThis = nullptr;
        if (napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr) == napi_ok && jsThis != nullptr) {
            NapiEncryptor* obj = new NapiEncryptor();
            obj->env_ = env;
            // 通过napi_wrap将ArkTS对象jsThis与C++对象obj绑定
            napi_status status = napi_wrap(env, jsThis, reinterpret_cast<void*>(obj), NapiEncryptor::Destructor, nullptr, nullptr);
        }
        return jsThis;
    }
    else {
        // 使用`MyObject(...)`调用方式
        napi_status status;
        napi_value cons;
        status = napi_get_reference_value(env, g_ref, &cons);
        if (status != napi_ok) {
            return nullptr;
        }
        napi_value jsObj = nullptr;
        status = napi_new_instance(env, cons, 0, nullptr, &jsObj);
        return jsObj;
    }
}
void NapiEncryptor::Destructor(napi_env env, void* nativeObject, void* finalize_hint) {
    reinterpret_cast<NapiEncryptor*>(nativeObject)->~NapiEncryptor();
}
napi_value NapiEncryptor::GetEncryptor(napi_env env, napi_callback_info info) {
    napi_value result = nullptr;
    napi_get_undefined(env, &result);
    napi_value jsThis;
    napi_status status;
    status = napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr);
    if (status != napi_ok || !jsThis) {
        return nullptr;
    }
    NapiEncryptor *napiEncryptor;
    status = napi_unwrap(env, jsThis, reinterpret_cast<void **>(&napiEncryptor));
    if (status != napi_ok) {
        return nullptr;
    }
    
    napi_value jsObj;
    RTCCustomEncryptorInterface* encryptor = static_cast<RTCCustomEncryptorInterface *>(napiEncryptor);
    int64_t encryptorInt64 = reinterpret_cast<int64_t>(encryptor);
    status = napi_create_int64(env, encryptorInt64, &jsObj);
    return jsObj;
}
int NapiEncryptor::Encrypt(const uint8_t* payload_data, size_t payload_size,
                            uint8_t* encrypted_frame, size_t* bytes_written,
                            const char* media_stream_id, int media_type) {
    uint8_t fake_key_ = 0x88;
    if (media_type == 1) {
        for (size_t i = 0; i < payload_size; i++) {
            encrypted_frame[i] = payload_data[i] ^ fake_key_;
        }
        *bytes_written = payload_size;
    } else {
        encrypted_frame[0] = 0;
        encrypted_frame[1] = 1;
        encrypted_frame[2] = 2;
        encrypted_frame[3] = 3;
        encrypted_frame[4] = 3;
        encrypted_frame[5] = 2;
        encrypted_frame[6] = 1;
        encrypted_frame[7] = 0;
        for (size_t i = 0; i < payload_size; i++) {
            encrypted_frame[i + 8] = payload_data[i] ^ fake_key_;
        }
        *bytes_written = payload_size + 8;
    }
    return 0;
}
size_t NapiEncryptor::GetMaxCiphertextByteSize(size_t frame_size, const char* media_stream_id, int media_type) {
    if (media_type == 1) {
        return frame_size;
    }
    return frame_size + 8;
}
步骤 3:实现自定义解密
新增解密 NapiDecryptor C++ 类,实现 RTCCustomDecryptorInterface 解密方法。
#include "rtc_custom_crypto_interface.h"
#include <js_native_api_types.h>
class NapiDecryptor : public RTCCustomDecryptorInterface {
public:
    NapiDecryptor();
    ~NapiDecryptor();
    
    static napi_value Init(napi_env env, napi_value exports);
    static napi_value New(napi_env env, napi_callback_info info);
    static void Destructor(napi_env env, void* nativeObject, void* finalize_hint);
    
public:
    static napi_value GetDecryptor(napi_env env, napi_callback_info info);
    
    int Decrypt(const uint8_t* encrypted_frame, size_t encrypted_frame_size,
                uint8_t* frame, size_t* bytes_written, const char* media_stream_id,
                int media_type) override;
    size_t GetMaxPlaintextByteSize(size_t frame_size, const char* media_stream_id, int media_type) override;
    
private:
    napi_env env_;
    napi_ref wrapper_;
};
在 NapiDecryptor.cpp 实现自定义解密方法,和上述加密方式一致也是采用对数据的异或方式。
#include "NapiDecryptor.h"
#include <js_native_api.h>
static thread_local napi_ref g_ref = nullptr;
NapiDecryptor::NapiDecryptor() : env_(nullptr), wrapper_(nullptr) {
    
}
NapiDecryptor::~NapiDecryptor() {
    napi_delete_reference(env_, wrapper_);
}
napi_value NapiDecryptor::Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"decryptor", nullptr, nullptr, NapiDecryptor::GetDecryptor, nullptr, nullptr, napi_default, nullptr}
    };
    int desc_count = sizeof(desc) / sizeof(desc[0]);
    napi_value cons = nullptr;
    if (napi_define_class(env, "NapiDecryptor", NAPI_AUTO_LENGTH, New, nullptr,
                          desc_count, desc, &cons) != napi_ok) {
        return nullptr;
    }
    if (napi_create_reference(env, cons, 1, &g_ref) != napi_ok) {
        return nullptr;
    }
    if (napi_set_named_property(env, exports, "NapiDecryptor", cons) != napi_ok) {
        return nullptr;
    }
    return exports;
}
napi_value NapiDecryptor::New(napi_env env, napi_callback_info info) {
    napi_value newTarget = nullptr;
    napi_get_new_target(env, info, &newTarget);
    if (newTarget != nullptr) {
        napi_value jsThis = nullptr;
        if (napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr) == napi_ok && jsThis != nullptr) {
            NapiDecryptor* obj = new NapiDecryptor();
            obj->env_ = env;
            // 通过napi_wrap将ArkTS对象jsThis与C++对象obj绑定
            napi_status status = napi_wrap(env, jsThis, reinterpret_cast<void*>(obj), NapiDecryptor::Destructor, nullptr, nullptr);
        }
        return jsThis;
    }
    else {
        // 使用`MyObject(...)`调用方式
        napi_status status;
        napi_value cons;
        status = napi_get_reference_value(env, g_ref, &cons);
        if (status != napi_ok) {
            return nullptr;
        }
        napi_value jsObj = nullptr;
        status = napi_new_instance(env, cons, 0, nullptr, &jsObj);
        return jsObj;
    }
}
void NapiDecryptor::Destructor(napi_env env, void* nativeObject, void* finalize_hint) {
    reinterpret_cast<NapiDecryptor*>(nativeObject)->~NapiDecryptor();
}
napi_value NapiDecryptor::GetDecryptor(napi_env env, napi_callback_info info) {
    napi_value result = nullptr;
    napi_get_undefined(env, &result);
    napi_value jsThis;
    napi_status status;
    status = napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr);
    if (status != napi_ok || !jsThis) {
        return nullptr;
    }
    NapiDecryptor *napiDecryptor;
    status = napi_unwrap(env, jsThis, reinterpret_cast<void **>(&napiDecryptor));
    if (status != napi_ok) {
        return nullptr;
    }
    
    napi_value jsObj;
    RTCCustomDecryptorInterface* decryptor = static_cast<RTCCustomDecryptorInterface *>(napiDecryptor);
    int64_t decryptorInt64 = reinterpret_cast<int64_t>(decryptor);
    status = napi_create_int64(env, decryptorInt64, &jsObj);
    return jsObj;
}
int NapiDecryptor::Decrypt(const uint8_t* encrypted_frame, size_t encrypted_frame_size,
                uint8_t* frame, size_t* bytes_written, const char* media_stream_id,
                int media_type) {
    uint8_t fake_key_ = 0x88;
    if (media_type == 1) {
        for (size_t i = 0; i < encrypted_frame_size; i++) {
            frame[i] = encrypted_frame[i] ^ fake_key_;
        }
        *bytes_written = encrypted_frame_size;
    } else {
        if (encrypted_frame_size < 8) {
            return 0;
        }
        for (size_t i = 0; i < encrypted_frame_size; i++) {
            frame[i] = encrypted_frame[i + 8] ^ fake_key_;
        }
        *bytes_written = encrypted_frame_size - 8;
    }
    
    return 0;
}
size_t NapiDecryptor::GetMaxPlaintextByteSize(size_t frame_size, const char* media_stream_id, int media_type) {
    if (media_type == 1) {
        return frame_size;
    } else {
        return frame_size < 8 ?: frame_size - 8;
    }
}
步骤 4:配置加解密库
修改 CMakeLists.txt 文件,将 NapiEncryptor.cpp NapiDecryptor.cpp 添加 add_library 方法里,如下图所示:

在 index.d.ts 中增加 napi 方法导出,方便后续在项目中使用。
export class NapiEncryptor {
  public get encryptor(): number;
}
export class NapiDecryptor {
  public get decryptor(): number;
}
在 cpp 文件下找到 oh-package.json5 修改 .so 库名,
{
  "name": "RTCCustomCrypto.so",
  "types": "./index.d.ts",
  "version": "1.0.0",
  "description": "Please describe the basic information."
}
对应在项目中修改 dependencies 保持库名和路径统一,修改如下:
{
    "name": "entry",
    "version": "1.0.0",
    "description": "Please describe the basic information.",
    "main": "",
    "author": "",
    "license": "",
    "dependencies": {
        "RTCCustomCrypto.so": "file:./src/main/cpp/types/RTCCustomCrypto"
    }
}
注意修改库名步骤非必须,如需修改则要保证定义的库名和外部引用一致。
步骤 5:在 CallPlus 中使用加解密
引入前述步骤中创建的加解密类,在发起和接听前通过 RCCallPlusClient 的接口设置,示例代码如下:
import {NapiDecryptor, NapiEncryptor} from "RTCCustomCrypto.so";
private _statCall(params: IRCCallPlusStartParams): void {
    
    let callPlusClient: RCCallPlusClient = RCCallPlusClient.getInstance();
    
    /// 设置加解密
    callPlusClient.setDecryptor(this.decryptor.decryptor)
    callPlusClient.setEncryptor(this.encryptor.encryptor);
    
    /// 开始通话
    callPlusClient.startCallWithParams(params)
        .then((res) => {
            if(res.code === RCCallPlusCode.SUCCESS){
                let callId = res.callId
                let busyUsers = res.busyUsers
                console.log(`startCallWithParams success, callId: ${callId}, busyUsers: ${JSON.stringify(busyUsers)}`)
            }
            else{
                console.log(`startCallWithParams failed, code: ${res.code}`)
            }
        })
}
private _acceptCall(callId: string) :void {
    let callPlusClient: RCCallPlusClient = RCCallPlusClient.getInstance();
    /// 设置加解密
    callPlusClient.setDecryptor(this.decryptor.decryptor)
    callPlusClient.setEncryptor(this.encryptor.encryptor);
    /// 接听
    callPlusClient.accept(callId).then((res) => {
            if(res.code === RCCallPlusCode.SUCCESS){
                console.log('accept success')
            }
            else {
                console.log(`accept failed, code: ${res.code}`)
            }
        })
}