跳到主要内容

自定义加密

在实时音视频互动中,开发者可选择对媒体流进行加密,从而保障用户的数据安全。融云提供两套加密方案:

  1. 开发者对媒体数据的自定义加密。即加解密完全由开发者实现,融云服务只对数据做转发,适用于对安全性有特殊要求的客户。此类加密方式,服务端无法做合流处理,所以不适用于直播。
  2. 使用自定义加密后,无法使用部分 RTC 服务端功能,包括:云端录制云端截图内容审核云播放器
提示
  • 若使用自定义加密,需要确保发送端和接收端的加解密算法一致,否则会无法正常通话。
  • 自定义加密,可分别针对音频或视频的原始数据执行,音频和视频的加密算法可以独立设置,或对其中之一进行设置。
  • 由于数据量大,考虑性能原因,自定义的加密算法需要在 C++ 层实现。 注意

无论何种加密方式,都会对客户端、服务器造成额外的资源消耗,在低性能设备上可能会影响体验。

自定义加密

鸿蒙音视频通话想要实现自定义加解密,需要实现 C++ 层提供的加解密协议方法,通过构建加密解密实例,在开始呼叫前调用 setEncryptorsetDecryptor 方法将指针传入 CallPlus 即可实现音视频数据加解密。

步骤 1 创建加解密模块

打开在项目里面创建一个 C++ 模块。

Image

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

Imgae

具体增加 RTCCustomEncryptorInterface 加密协议和 RTCCustomDecryptorInterface 解密协议,代码如下。

cpp
#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 的加密方法,代码如下。

cpp
#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 实现音视频流加密方法,以下示例代码中采用对数据的异或方式加密。

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 解密方法。

cpp
#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 实现自定义解密方法,和上述加密方式一致也是采用对数据的异或方式。

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 方法里,如下图所示:

Image

在 index.d.ts 中增加 napi 方法导出,方便后续在项目中使用。

TypeScript
export class NapiEncryptor {
public get encryptor(): number;
}

export class NapiDecryptor {
public get decryptor(): number;
}

在 cpp 文件下找到 oh-package.json5 修改 .so 库名,

JSON
{
"name": "RTCCustomCrypto.so",
"types": "./index.d.ts",
"version": "1.0.0",
"description": "Please describe the basic information."
}

对应在项目中修改 dependencies 保持库名和路径统一,修改如下:

JSON
{
"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 的接口设置,示例代码如下:

TypeScript
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}`)
}
})
}