一、项目背景
公司各业务线以及相关产品,会收集用户敏感数据,这些敏感数据在通讯链路上以明文形式传输。伴随着个人信息保护法的出台,对于个人隐私保护的力度持续加大。个人信息安全规范明确要求,个人敏感信息在传输时必须进行加密。根据规范中提出的敏感信息加密的标准,我们将实现一套符合标准的敏感数据加密传输方案。为满足敏感数据生命周期传输环节的监管合规要求和数据安全要求,需要对用户敏感数据在传输环节进行协议级别和字段级别的加密处理,避免用户敏感数据发生泄露。
在推进业务方完成敏感数据的卸载和脱敏处理过程中,业务自行加密面临的核心问题有:
- 业务客户端需配合改造,成本高效率低
- 加密安全标准无法对齐和验收
为此,需要提供一套通用的端到端下行数据加解密能力。
二、项目目标
在客户端和服务端交互的过程中,原应只有图中的红色模块才能获取数据明文,但是现在所有的绿色模块也均能获取数据的明文,因此存在被攻击的可能。
本项目为解决上述问题,实现了完整的端到端加密传输链路,技术方案覆盖安卓/IOS/Web/RN等多平台,本文将介绍Web端数据传输加密SDK的设计思路与实现原理。
三、整体方案
技术架构
抽象通用零信任加解密能力(Zero-Trust Data Protection),封装对应SDK以支持上层多种应用场景
Encryption Web SDK
数据加密Web端SDK,劫持业务网络请求,对请求进行上层封装,携带加密信息和相关证书,对响应进行解密并返回给业务调用
ZDP SDK
Zero-Trust Data Protection
零信任数据保护方案SDK,集成可信SDK Delta,支持请求数据时带上加密相关证书,提供数据解密能力
ZDP Lib
零信任数据保护方案服务端,存储加密密钥,提供数据加密能力
Encryption Config Server
传输加密配置服务端,下发加密配置(如:哪些域名、path需要加密传输)到web端
网关Loader
网关层loader,加密传输与ZDP Lib交互,修改加密字段内容并返回响应
Delta
收敛敏感数据加解密标准算法的SDK,提供加解密算法支持
核心流程
主要分为两个阶段:初始化阶段和下行加密阶段
初始化阶段
- web浏览器在启动时,Encryption Web SDK初始化本地私钥并发向证书server发起CSR请求
- 证书server签发客户端证书, 返回返回服务端证书
- Encryption Web SDK向Encryption Config Server发起请求,获取包含加密的域名 +path信息
下行加密阶段
- web浏览器发起请求, Encryption Config Server判读当前请求的resp是否包含需要加密的字段, 如果包含, 则在请求头上附带客户端证书信息
- 请求响应到达网关, 网关loader判断当前path是否包含需要加密的字段, 如果包含, 则从请求头中获取客户端证书, 进行字段抽取, 请求ZDP-lib使用客户端证书和服务端私钥进行加密.
- loader修改字段, 返回给客户端, 客户端请求ZDP-SDK进行字段解密, 获取明文数据
四、详细方案
SDK设计思路
ZDP初始化
- 核心流程:创建ZDP对象 -> 获取证书信息 -> 获取加密配置 -> 开启请求拦截器
- 初始化ZDP-SDK:由ZDP-SDK生成客户端公私钥,并获取服务端证书。需要ZDP-SDK提供的功能如下:
功能 | 说明 |
---|---|
初始化SDK | 初始化ZDP-SDK,检查本地环境,必要情况下生成客户端公私钥,请求服务端证书与颁发客户端证书 |
更新证书 | 重新请求颁发新的证书(客户端证书、服务端证书) |
重新生成公私钥 | 重新生成客户端公私钥、请求服务端证书与颁发客户端证书。用于本地公私钥异常的情况(未正常生成) |
获取证书信息 | 获取客户端证书信息,用于在Request Header中带给server |
// 初始化ZDP SDK
async initZDP() {
this.secureSDK = new (window as any).UCSecuritySDK({
proxy: false, // 由于SDK目前支持劫持能力,所有初始化的时候必须关闭劫持能力
aid: 1128, // 当前aid
cryptType: 'delta',
certType: 'request',
});
// 初始化完成之后获取当前的端上证书数据
const res = await this.secureSDK.cryptoSDK.getClientAndServerData();
const { sn, clientCert } = res || {};
// 服务端证书序列号
this.sn = sn;
// 客户端证书
this.clientCert = clientCert;
// 获取加密配置(待拦截的域名+path集合)
const response = await fetch(
'/api/transport_control/downstream/conf/client/get_enc_api',
{
method: 'GET',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
}
);
const body = await response.json();
this.interceptUrls = body.data?.encrypt_api_list ?? [];
// 拦截请求
this.requestInterceptor();
}
请求拦截器
- 请求拦截: 分为XHR与Fetch拦截器,XHR实现依赖ajax-hook,Fetch实现依赖fetch-intercept
XHR 实现
import {
proxy,
XhrRequestConfig,
XhrRequestHandler,
XhrError,
XhrErrorHandler,
XhrResponse,
XhrResponseHandler
} from 'ajax-hook';
// 拦截xhr请求
xhrInterceptor() {
const that = this;
proxy({
// 请求发起前进入
onRequest: (config: XhrRequestConfig, handler: XhrRequestHandler) => {
// 判断config.host、config.url是否在待加密请求interceptUrls中
const request = that.isRequestEncyptionNeeded(config);
// 域名+path匹配命中,请求头携带加密信息
if (request) {
const newConfig = that.modifyRequestHeaders(IRequestType.XHR, config);
handler.next(newConfig);
} else { // 匹配未命中,请求不做任何处理
handler.next(config);
}
},
// 请求发生错误时进入,比如超时;注意,不包括http状态码错误,如404仍然会认为请求成功
onError: (err: XhrError, handler: XhrErrorHandler) => {
// 错误上报
handler.next(err);
},
// 请求成功后进入
onResponse: async (response: XhrResponse, handler: XhrResponseHandler) => {
// 获取response header
const responseHeaders = response.headers;
const { code, mode, field } = that.resolveResponseHeader(responseHeaders);
switch (code) {
// 直接明文返回
case IReqResponseCode.DecodeError:
case IReqResponseCode.HeaderParamsMiss:
case IReqResponseCode.NoEncryption:
handler.next(response);
break;
// 正常加密,分字段解密和body解密进行处理
case IReqResponseCode.Encryption:
that.handleEncryptionResponse(mode, field, response, handler);
break;
// 客户端证书错误,刷新数据返回错误
case IReqResponseCode.ClientError:
await that.refreshData();
await that.refreshServerData();
await that.initZDP();
handler.next(response);
break;
// 服务端错误,返回错误
case IReqResponseCode.ServerError:
// 错误上报
handler.next(response);
default:
break;
}
},
});
}
Fetch实现
// 拦截fetch请求
fetchInterceptor() {
const that = this;
fetchIntercept.register({
request(url, config) {
// 判断config.host、config.url是否在待加密请求interceptUrls中
const request = that.isRequestEncyptionNeeded(url);
// 域名+path匹配命中,请求头携带加密信息
if (request) {
const newConfig = that.modifyRequestHeaders(IRequestType.Fetch, config);
return [url, newConfig];
}
// 匹配未命中,请求不做任何处理
return [url, config];
},
requestError(error) {
// 错误上报
return Promise.reject(error);
},
response(response: FetchInterceptorResponse) {
// Modify the reponse object
const clonedResponse = response.clone();
// 获取headers
const headers = {};
for (const pair of clonedResponse.headers.entries()) {
headers[pair[0]] = pair[1];
}
const { code, mode, field } = that.resolveResponseHeader(headers);
switch (code) {
// 直接明文返回
case IReqResponseCode.DecodeError:
case IReqResponseCode.HeaderParamsMiss:
case IReqResponseCode.NoEncryption:
return response;
// 正常加密,分字段解密和body解密进行处理
case IReqResponseCode.Encryption:
try {
const json = () =>
clonedResponse.json().then(async data => {
const res = JSON.stringify(data);
if (mode === IReqResponseMode.BodyEncrypt) {
return await that.secureSDK.cryptoSDK.decrypt(res);
}
return await that.decryptResponseField(field, res);
});
response.json = json;
return response;
} catch (err) {
// 错误上报
return response;
}
// 客户端证书错误,刷新数据返回错误
case IReqResponseCode.ClientError:
that.refreshData();
that.refreshServerData();
that.initZDP();
return response;
// 服务端错误,返回错误
case IReqResponseCode.ServerError:
// 错误上报
return response;
default:
break;
}
return response;
},
responseError(error) {
// 错误上报
return Promise.reject(error);
},
});
}
- 拦截处理: 分为上行Request header标记与下行Response Header解析
上行Request header标记
对命中配置的请求,需在Request header中增加标记,服务端识别标记后,对body进行加密;
具体标记字段如下:
key | 说明 |
---|---|
bd-timon-client-crt | 客户端证书信息(等同于公钥) |
bd-timon-version | 客户端SDK版本 |
bd-timon-ts | 时间戳 |
bd-timon-req-sign | 签名,使用客户端私钥对上述三个key的摘要进行加签 |
bd-timon-local-server-crt-sn | 客户端本地保存的服务端证书序列号 |
// 修改请求头
modifyRequestHeaders(requestType: IRequestType, config: IRequestConfig): IRequestConfig {
const cert = this.clientCert;
const timestamp = new Date().getTime().toString();
const sn = this.sn;
if (requestType === IRequestType.XHR) {
const headers = config.headers || {};
config.headers = {
...headers,
// 客户端证书信息(等同于公钥)
'bd-timon-client-crt': cert,
// 时间戳
'bd-timon-ts': timestamp,
// 服务端证书序列号
'bd-timon-local-server-crt-sn': sn,
};
} else {
config = config || {};
const modifiedHeaders = new Headers(config?.headers || {});
modifiedHeaders.append('bd-timon-client-crt', cert);
modifiedHeaders.append('bd-timon-ts', timestamp);
modifiedHeaders.append('bd-timon-local-server-crt-sn', sn);
config.headers = modifiedHeaders;
}
return config;
}
下行Response Header解析
服务端对回包进行加密后,会在Response header增加特定字段,用于标识该body已加密,并给定body加密字段路径。
key | 说明 |
---|---|
bd-timon-code | 响应码,标识加解密状态: |
bd-timon-encrypt-mode | 0 明文, 1 字段加密, 2 body加密 |
bd-timon-encrypt-field | 服务端正常加密后, 说明哪些字段是加密的字段协议: body描述 |
bd-timon-remote-server-crt-sn | 服务端本地的证书版本 |
对下行ResponseHeader进行解析:
// 解析响应头,获取解密信息
resolveResponseHeader(headers: XhrResponse['headers']): IResponseHeaders {
try {
const code = headers['bd-timon-code'];
const mode = headers['bd-timon-encrypt-mode'];
const field = headers['bd-timon-encrypt-field'];
return {
code, // 以1、2、3、4、5开头
mode, // 0 明文, 1 字段加密, 2 body加密
field: field ? JSON.parse(field) : [], // 哪些字段是加密的
};
} catch (err) {
// 错误上报
return {
code: -2, // 解析错误码
};
}
}
- 响应解密:分为响应body与字段类型的解密
响应解密实现
- handleEncryptionResponse:解析响应,对body解密类型的响应直接使用ZDP SDK提供解密方法secureSDK.cryptoSDK.decrypt()进行解密,对字段解密类型的响应,调用decryptResponseField()方法进行处理:
// 处理响应解密
async handleEncryptionResponse(
mode: IReqResponseMode,
field: string,
response: XhrResponse,
handler: XhrResponseHandler
) {
// 解析body
const responseBody = response.response;
// body解密
if (mode === IReqResponseMode.BodyEncrypt) {
const res = this.secureSDK.cryptoSDK.decrypt(responseBody);
res.then(data => {
// 返回解密后的res
handler.next(
Object.assign({}, response, {
response: data,
})
);
});
} else {
// 字段解密
const res = await this.decryptResponseField(field, responseBody);
handler.next(
Object.assign({}, response, {
response: res,
})
);
}
}
- decryptResponseField:遍历待解密字段数组field,解密每一个具体字段,使用JsonParser类的modifyNodeByPath()处理field每一个加密字段,解密修改字段为原始内容:
// 解密响应字段
async decryptResponseField(field: string, responseBody: XhrResponse['response']): Promise<XhrResponse['response']> {
try {
// 初始化JsonParser对象,解析responseBody
const parser = new JsonParser(JSON.parse(responseBody));
// 循环处理field每一个加密字段,解密修改字段为原始内容
for (const path of field) {
await parser.modifyNodeByPath(path, (val: string) => {
return new Promise(async resolve => {
const res = await this.secureSDK.cryptoSDK.decrypt(val);
resolve(res);
});
});
}
return JSON.stringify(parser.nodeVal);
} catch (err) {
return null;
}
}
解密算法实现
实现原理
举个🌰:
- 加密传输请求响应内容如下,可以看到body里的字段均为加密传输的字段:
{
"list":[
{
"mp":{
"1":{
"body":"{"user_name_desensitize":"vQHmA2qBvCkEXbqN6mP2BY+7WKj2TIzFhe4zVxOT0VgP/AVlag==","login_password_encrypt1":"vQH+pBpxmwvWwMz3QoCGlT3Icbvl82zA4PGPn1CdiB1two8CEThvUeiT4Ni1","login_password_encrypt2":"vQGe/BrBKsVTk27wvSHWrgssSIowiqEjCgPnIllNXSqGQmR/+VUCyjOD8gYW","login_password_encrypt3":"vQFw1oAracMBmxOwfJo1XmoS6e1MGskAEzllS6WGoAfFAIDSSIXuocTumF63","login_password_encrypt4":"vQH2vK4DU6Ez1dgEAdlAkhO9u5Rz/ZHI8JJ1DCLImuTeDSmhyvDzU0mR9a0P","login_password_encrypt5":"vQGK5iUF6wJsDWyah2Ml0fVE/WXXWGwIGMjufU4DVFJx391o2J080hlW4Cg+","login_password_encrypt6":"vQFLmWzujgHy25yMyjL5P93TvG6XLUaFmXmJ9HVrtieTAnhYgp8DQ+p3BpA+","login_password_encrypt7":"vQGvnz08IdD2lgpL4OTIz9/aMFo44zWK4ZfM92liiUCGTtnjkPoQMtRhph8u","login_password_encrypt8":"vQGLoeGB/dwVkyyiCQPHs+wm9vJkGIRzBRk6UF4VmhQvmHE89pQ10NfXY4d9","login_password_encrypt9":"vQHRDUE+jb+cLzkLnaHejCITS049KdjzXYRbnbXqat+URsXUR9//f9+i4djf","login_password_encrypt10":"@PassWord_10"}"
}
}
}
],
"status_code":0
}
- 服务端将待解密字段存储在Response Header的bd-timon-encrypt-field字段中,格式如下:
bd-timon-encrypt-field: ["list::<idx>::mp::<id>::body::user_name_desensitize","list::<idx>::mp::<id>::body::login_password_encrypt1","list::<idx>::mp::<id>::body::login_password_encrypt2","list::<idx>::mp::<id>::body::login_password_encrypt3","list::<idx>::mp::<id>::body::login_password_encrypt4","list::<idx>::mp::<id>::body::login_password_encrypt5","list::<idx>::mp::<id>::body::login_password_encrypt6","list::<idx>::mp::<id>::body::login_password_encrypt7","list::<idx>::mp::<id>::body::login_password_encrypt8","list::<idx>::mp::<id>::body::login_password_encrypt9"]
bd-timon-encrypt-field字段采用正则表达式来记录待解密的字段,bd-timon-encrypt-field为一个数组,数组每一项为一个字符串,表达一个待解密字段,如:
list::<idx>::mp::<id>::body::user_name_desensitize
表示:取list数组下的每一项,每一项的对象再取mp字段,mp字段的对象再取每一个key,每一个key的对象再取body字段,body字段再取user_name_desensitize字段,即为待解密字段。
待解密字段表达规则:
- :: —— json字段分隔符,用于处理json下一层的字段
- < idx > —— 用于遍历数组每一项
- < id >、< norm >、< form >、< cn_key > —— 用于遍历Map的每一个key
- 解析field字段,对不同字段类型进行解密,使用Json Parser解析器来递归处理,将解密后的结果返回给业务。解密后的响应如下,可以看到body下的字段均完成了解密:
{
"list":[
{
"mp":{
"1":{
"body":"{\"user_name_desensitize\":\"ybw\",\"login_password_encrypt1\":\"@PassWord_1\",\"login_password_encrypt2\":\"@PassWord_2\",\"login_password_encrypt3\":\"@PassWord_3\",\"login_password_encrypt4\":\"@PassWord_4\",\"login_password_encrypt5\":\"@PassWord_5\",\"login_password_encrypt6\":\"@PassWord_6\",\"login_password_encrypt7\":\"@PassWord_7\",\"login_password_encrypt8\":\"@PassWord_8\",\"login_password_encrypt9\":\"@PassWord_9\",\"login_password_encrypt10\":\"@PassWord_10\"}"
}
}
}
],
"status_code":0
}
Json Parser
由于解密响应字段采用上述表达规则,须要递归处理Response Body各待解密字段,须实现一个Json Parser类,提供Response Body解析的能力,用于body和字段解密,并封装modifyNodeByPath解密函数,修改Json节点信息,实现如下:
import { JsonNodeType, JsonNodeBasicType } from './config';
import { getNodeType, getBasicType, getNodeByPath, genValidPaths } from './jsonNode';
/* Json解析器
* 作用:提供Response Body解析能力,用于body和字段解密
* 参数:node节点值
* 方法:modifyNodeByPath解密函数,修改节点信息
*/
export default class JsonParser {
// 节点值
private node;
constructor(json) {
this.node = json;
}
// 获取节点值
get nodeVal() {
return this.node;
}
// 节点转化为字符串类型
private nodeToString() {
if (!this.node) {
return '';
} else if (
getBasicType(this.node) === JsonNodeBasicType.V_OBJECT ||
getBasicType(this.node) === JsonNodeBasicType.V_ARRAY
) {
return JSON.stringify(this.node);
}
return this.node.toString();
}
// 遍历field所有字段,修改节点(外部调用)
async modifyNodeByPath(path: string, modFunc: (val: string) => Promise<string>) {
const paths = genValidPaths(path);
return await this.modifyNode(paths, modFunc);
}
// 递归修改节点
private async modifyNode(paths: string[], modFunc: (val: string) => Promise<string>) {
let travel;
travel = async function (node, paths: string[]): Promise<string> {
if (!node) {
return '';
}
// 递归结束条件:paths数组为空
if (paths.length === 0) {
// 解密,修改节点值
const res = await modFunc(node);
return res;
}
const path = paths[0];
const nodeType = getNodeType(node, path);
// 根据节点类型,递归处理子节点
switch (nodeType) {
// 子节点为数组类型,遍历数组,递归travel每一项
case JsonNodeType.JsonNodeType_Array:
for (let i = 0; i < node.length; i++) {
const res = await travel(node[i], paths.slice(1));
node[i] = res;
}
return node;
// 子节点为Map类型,遍历Map,递归travel各个key节点
case JsonNodeType.JsonNodeType_Map:
for (const key of Object.keys(node)) {
const res = await travel(node[key], paths.slice(1));
node[key] = res;
}
return node;
// 子节点为普通对象,递归travel子节点
case JsonNodeType.JsonNodeType_Object:
const subNode = getNodeByPath(node, path);
const res = await travel(subNode, paths.slice(1));
node[path] = res;
return node;
// 子节点为JsonString类型,重新初始化一个JsonParser,传入parse解析后的json对象,调用modifyNode方法进行解密处理
case JsonNodeType.JsonNodeType_JsonString:
const val = JSON.parse(node);
const parser = new JsonParser(val);
await parser.modifyNode(paths, modFunc);
return parser.nodeToString();
default:
return node;
}
};
this.node = await travel(this.node, paths);
return this.node;
}
}
Json Node
这里可能你会对不同类型的节点处理逻辑有疑问,我们再来看看Json Parser依赖的Json Node节点类,它针对不同类型的Json Node进行解析与处理,首先,我们来回顾一下解密字段bd-timon-encrypt-field的每一项,形如:
list::<idx>::mp::<id>::body::user_name_desensitize
可以发现:
- bd-timon-encrypt-field字段数组的每一项为一个JsonPath
- JsonPath包含不同类型的label,如:< id >、 < idx >等
- 不同类型的label对应不同的节点类型JsonNodeType
- JsonNodeType对应JS数据基本类型JsonNodeBasicType
Json Node节点各数据类型定义如下:
// JsonPath Label类型
enum JsonPathLabelType {
JsonPathLabelType_Normal,
JsonPathLabelType_Array,
JsonPathLabelType_Map,
}
// JsonPath Labels
const jsonPathLabels = {
'<idx>': JsonPathLabelType.JsonPathLabelType_Array,
'<id>': JsonPathLabelType.JsonPathLabelType_Map,
'<norm>': JsonPathLabelType.JsonPathLabelType_Map,
'<form>': JsonPathLabelType.JsonPathLabelType_Map,
'<cn_key>': JsonPathLabelType.JsonPathLabelType_Map,
};
// JsonNode 扩展类型
enum JsonNodeType {
JsonNodeType_Null,
JsonNodeType_Normal,
JsonNodeType_Object,
JsonNodeType_Array,
JsonNodeType_Map,
JsonNodeType_JsonString,
}
// JsonNode 基本类型
enum JsonNodeBasicType {
V_STRING,
V_ARRAY,
V_OBJECT,
V_OTHER,
}
export { JsonPathLabelType, jsonPathLabels, JsonNodeType, JsonNodeBasicType };
有了数据类型定义,JsonNode便可以实现节点类型相关的方法,核心方法如下:
- getNodeType:根据field label判断节点类型
- getBasicType:获取节点基本数据类型
- getJsonPathLabelType:获取field label类型
import { JsonPathLabelType, JsonNodeType, jsonPathLabels, JsonNodeBasicType } from './config';
// 根据field label判断节点类型
function getNodeType(node, label: string): JsonNodeType {
const labelType = getJsonPathLabelType(label);
switch (getBasicType(node)) {
case JsonNodeBasicType.V_OBJECT:
if (labelType === JsonPathLabelType.JsonPathLabelType_Map) {
return JsonNodeType.JsonNodeType_Map;
} else if (labelType === JsonPathLabelType.JsonPathLabelType_Array) {
return JsonNodeType.JsonNodeType_Null;
}
return JsonNodeType.JsonNodeType_Object;
case JsonNodeBasicType.V_ARRAY:
if (labelType === JsonPathLabelType.JsonPathLabelType_Array) {
return JsonNodeType.JsonNodeType_Array;
}
return JsonNodeType.JsonNodeType_Null;
case JsonNodeBasicType.V_STRING:
// 去除空格、换行、tab
const v = node.replace(/[\n\t\r\s]/g, '');
if (
(labelType === JsonPathLabelType.JsonPathLabelType_Array && v.startsWith('[') && v.endsWith(']')) ||
(v.startsWith('{') && v.endsWith('}'))
) {
return JsonNodeType.JsonNodeType_JsonString;
}
return JsonNodeType.JsonNodeType_Null;
default:
return JsonNodeType.JsonNodeType_Null;
}
}
// 获取节点基本数据类型
function getBasicType(node): JsonNodeBasicType {
switch (Object.prototype.toString.call(node)) {
case '[object Object]':
return JsonNodeBasicType.V_OBJECT;
case '[object Array]':
return JsonNodeBasicType.V_ARRAY;
case '[object String]':
return JsonNodeBasicType.V_STRING;
default:
return JsonNodeBasicType.V_OTHER;
}
}
// 根据field label获取子节点
function getNodeByPath(node, path: string) {
return node[path];
}
// 获取field label类型
function getJsonPathLabelType(label: string): JsonPathLabelType {
return jsonPathLabels[label] ?? JsonPathLabelType.JsonPathLabelType_Normal;
}
// 根据field生成path数组
function genValidPaths(path: string): string[] {
return path.split('::');
}
export { getNodeType, getBasicType, getNodeByPath, getJsonPathLabelType, genValidPaths };
到此,解密算法的整个实现过程就完成了。
埋点方案
加密传输SDK对性能与稳定性要求很高,埋点上报的核心事件有:
- timon_network_transmission_encrypt(耗时、状态)
事件key | 类型 | 含义 |
---|---|---|
transmission_encrypt_cost_time | float | 传输加密模块耗时 |
crt_consistent_state | float | 证书是否一致,0表示不一致,1表示一致 |
zdp_crt_init_state | float | 公钥/证书初始化状态(初始化方法调用时上报),0为失败,1为成功 |
zdp_encrypt_cost_time | float | zpd加密耗时 |
zdp_decrypt_cost_time | float | zpd解密耗时 |
zdp_init_cost_time | float | zpd初始化耗时 |
zdp_encrypt_state | float | zdp加密状态,0标识失败,1表示成功 |
zdp_decrypt_state | float | zdp解密状态,0标识失败,1表示成功 |
- 错误上报(稳定性指标,如:证书过期、解密失败等逻辑异常)
特殊情况处理
客户端 证书缺失
证书缺失等同于公钥缺失
ZDP-SDK在初始化过程中由于某种原因导致服务端证书未能正确获取到,可增加一些重试机制,让客户端在运行过程中再次尝试获取服务端证书。
-
传输加密模块执行时,需校验本地是否存在证书,若本地无证书,则在子线程请求证书,且不阻塞业务请求;同时带上bd-timon-version和bd-timon-ts,明确告知服务端当前客户端缺失证书;
-
服务端从客户端请求头缺失bd-timon-client-crt,感知客户端证书缺失;此时优先明文传输,返回bd-timon-code=1004;支持兜底能力,bd-timon-code=4001,服务端抹除敏感参数,客户端返回请求失败;
客户端 与服务端证书不一致/客户端证书不合法
服务端证书发生更新时,客户端无法感知;因此存在客户端证书与服务端证书不一致的场景,需进行兜底处理。客户端生成证书不合法,也需要感知;
证书不一致
- 服务端从请求头解析bd-timon-local-server-crt-sn,发现客户端证书版本号与服务端证书版本号不一致,优先返回bd-timon-code=1006,明文传输;支持兜底能力,bd-timon-code=4003,服务端会抹除敏感参数,客户端返回请求失败;
- 客户端从请求头发现本地证书版本与服务端版本不一致时,会重新获取证书;此时body不加密,可直接返回给业务。
证书不合法
-
客户端从响应头bd-timon-code=1005,感知证书不合法,需重新更新证书,此时不加密;bd-timon-code=4002,失败,敏感参数卸载(手动配置,能感知)
注1: 重试拉取证书采取递增时间间隔,避免频繁请求引起server稳定性问题。
注2: 保证抹除敏感参数业务明确感知
解密失败
在有公私钥、server证书的情况下导致ZDP-SDK最终解密失败,此时大概率为证书、公钥私钥信息不匹配,可以采取的处理策略有:
- 策略1:解密失败,将这一次请求结果视作失败,给业务方返回请求错误,同时触发重新初始化ZDP-SDK,重新生成公钥私钥和证书。
策略2: 将请求重放,在Request Header中添加一个新的标记位,表示~~~~客户端~~~~此次需要明文,server返回明文;同时客户端触发重新初始化ZDP-SDK,重新生成公钥私钥和证书。(阻塞请求,感觉不合适,直接失败处理比较好)
注1:需要关注下历史ZDP-SDK的解密失败率及原因,如果失败率偏高,会导致ZDP-SDK频繁初始化。
五、总结
- 随着国家监管机构对于个人隐私保护的力度持续加大,高敏数据传输加密问题对于业务和产品安全越来越重要
- 数据传输交给业务自行加密,成本高效率低,且标准无法对齐,需要标准统一的数据加密传输SDK来解决
- 数据加密传输方案的核心原理是请求劫持,零信任数据保护方案,加解密算法,SDK稳定性、容错机制与性能问题
- 本文着重讲解Web端SDK的实现,客户端(安卓/IOS)/跨端(RN、Flutter)的设计思想和方案流程与Web端类似,具体细节的实现方法不同