1.1 问题说明****
业务中需处理的数据类型多样,包括用户个人敏感信息(如身份证号、手机号、银行账号等)、业务核心数据(如交易记录、合同信息等)、传输数据(如客户端与服务器的交互数据)以及存储数据(如数据库中的各类数据),如何确保用户信息安全是首要解决的问题。
1.2 原因分析****
· 抵御数据泄露,保护信息隐私
数据在存储和流转过程中,随时面临被未授权获取的风险,加密解密是防范此类风险的基础手段,在个人场景中,用户的身份证号、银行卡密码、医疗记录等敏感信息,若以明文形式在多端交互的业务场景中,一旦被破解,信息会直接泄露,可能导致身份冒用、财产损失等后果。而通过加密算法(如 AES)对敏感字段加密后,即使数据被窃取,攻击者因无密钥无法解密,可直接降低信息滥用风险
· 防范数据篡改,保障数据完整性;
数据不仅需 “不被偷”,还需 “不被改”—— 加密解密结合校验机制,可有效防止数据被恶意篡改,确保数据的真实性例在政务数据传输(如电子公文、证件信息)中,数据完整性直接关系到流程的合法性。若内容被篡改,可能导致决策失误 — 加密解密技术通过 “密文不可篡改(篡改后无法正确解密)+ 校验机制”:接收方解密后需重新计算哈希值并与发送方的哈希值比对为数据完整性提供双重保障
· 维护业务秩序,保障系统稳定**
**数据是业务运行的核心支撑,若数据因未加密被恶意利用,可能直接导致业务中断或系统瘫痪,加密解密是维护业务安全的重要保障,在网络服务场景中,客户端与服务器的交互数据(如登录凭证、操作指令)若未加密,攻击者可能伪造数据发起 “重放攻击”—— 例如,截取用户的 “充值成功” 指令明文,重复向服务器发送,导致用户账户被多次充值(或扣款),破坏业务规则。而通过加密 + 动态计数器(如 CTR 模式中的 Nonce),可确保每次交互的指令密文唯一,攻击者无法重复使用旧指令,保障业务逻辑正常运行。
· ****
· 应对技术演进,抵御新型威胁****
随着技术的发展和科技的进步,早期的 “弱加密”(如 DES 算法、短密钥)已因计算能力提升可被暴力破解 —— 例如,DES 算法的 56 位密钥,在现代计算机算力下可在短时间内被枚举破解;而升级为 AES-256、RSA-2048 等强加密算法后,即使攻击者拥有大量算力,也需数千年才能破解密钥,可有效抵御新型算力攻击。****
1.3 解决思路****
ArkTs提供@ohos.security.cryptoFramework(加解密算法库框架)
在加密解密技术体系中,AES作为主流对称加密算法,常需结合分组模式与填充方式 使用。AES/CTR/NoPadding便是典型的组合模式。
AES:基础加密算法,是对称加密算法的核心,通过固定长度的密钥(支持 128 位、1 92 位、256 位)对数据进行加密和解密(加密解密使用同一密钥)。它将数据按固定 块大小(128 位,即 16 字节)拆分处理,是整个加密流程的 “运算核心”。
CTR:分组加密,全称为 “Counter Mode(计数器模式)属于 AES 的分组工作模式,用于解决 “固定密钥下相同明文生成相同密文” 的安全问题。
这种模式的关键优势是支持并行计算(计数器值可独立生成,无需等待前一块处理完成),因此加密效率高,适合大文件或实时数据传输场景(如视频流加密)。为每个数据块生成一个唯一的 “计数器值(Counter)”,该值会按固定规则递增(如从初始值开始每次 + 1);将计数器值与密钥通过 AES 算法加密,得到 “密钥流”;用密钥流与明文块进行 “异或运算” 得到密文(解密时用相同计数器值生成密钥流,与密文异或得到明文)。
NoPadding:“NoPadding” 表示 “不进行填充”。由于 AES 算法要求明文必须按固定块大小(128 位)拆分,若明文长度不是块大小的整数倍,通常需用填充方式(如 PKCS#7)补充至完整块。但 “CTR 模式” 本质是通过密钥流与明文异或加密,无需依赖完整块结构,因此可直接处理任意长度明文,无需填充 —— 这也是 “CTR+NoPadding” 组合常见的原因。
1.4 注意事项****
计数器唯一性:CTR 模式中,同一密钥下若计数器值重复,会导致密钥流重复,攻击者可通过异或运算破解明文。因此需确保计数器初始值(Nonce)+ 递增规则的唯一性(如 Nonce 结合随机数 + 固定递增步长);需严格保证计数器唯一性,否则安全性低于 CBC 模式(CBC 依赖前一块密文,抗篡改能力更强)
Nonce 管理:Nonce(初始计数器值)需作为密文的一部分传输(或双方约定),但无需保密;若 Nonce 泄露,只要计数器递增规则唯一,仍可保证安全;
数据完整性:CTR 模式仅提供加密功能,不验证数据完整性(如密文被篡改无法察觉),需配合额外机制(如 HMAC 哈希校验)使用。
1.5 解决方案****
初始化密匙****
private static async deriveKey(id: string): Promise<cryptoFramework.SymKey | null> {
try {
// 创建 SHA256 消息摘要
const messageDigest = cryptoFramework.createMd(SHA256);
// 将ID转换为Uint8Array
const textEncoder = new util.TextEncoder();
const data: Uint8Array = textEncoder.encodeInto(id);
// 更新消息摘要数据
await messageDigest.update({ data });
// 计算消息摘要值
const digestData = await messageDigest.digest();
// 验证摘要长度
if (digestData.data.length !== 32) {
console.error(`${TAG} deriveKey error: SHA-256 的长度: ${digestData.data.length}`);
return null;
}
// 创建 AES-256 密钥生成器
const symKeyGenerator = cryptoFramework.createSymKeyGenerator('AES256');
// 使用完整32字节作为密钥
const keyMaterial: cryptoFramework.DataBlob = {
data: digestData.data
};
// 生成密钥
return await symKeyGenerator.convertKey(keyMaterial);
} catch (error) {
console.error(`${TAG} error: ${JSON.stringify(error)}, id:${id}`);
return null;
}
}
生成随机字节数组****
// 生成随机字节数组
private static async generateRandomBytes(length: number): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const rand = cryptoFramework.createRandom();
rand.generateRandom(length, (err, dataBlob) => {
if (err) {
reject(err);
} else {
resolve(dataBlob.data);
}
});
});
}
加密文本****
static async encrypt(text: string, key: cryptoFramework.SymKey | null = AESEncryptUtils.didKey): Promise<string | null> {
if (!key) return null;
try {
//生成随机IV
const iv: Uint8Array = await AESEncryptUtils.generateRandomBytes(IV_LENGTH_BYTES);
// 配置加密参数
const ivParams: cryptoFramework.IvParamsSpec = {
algName: 'IvParamsSpec',
iv: { data: iv }
};
// 初始化加密器
const cipher = cryptoFramework.createCipher(ENCRYPT_ALGO);
await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, key, ivParams);
// 执行加密
const textEncoder = new util.TextEncoder();
const input: cryptoFramework.DataBlob = {
data: textEncoder.encodeInto(text)
};
const encryptedData = await cipher.doFinal(input);
// 组合 IV + 密文
const payload = new Uint8Array([
...iv,
...encryptedData.data
]);
// Base64 编码
return AESEncryptUtils.toUrlSafeBase64(payload);
} catch (error) {
console.error(`${TAG} error: ${JSON.stringify(error)}, text:${text}`);
return null;
}
}
文本解密
static async decrypt(encryptedText: string, key: cryptoFramework.SymKey | null = AESEncryptUtils.didKey): Promise<string | null> {
if (!key) return null;
try {
//Base64 解码
const payload: Uint8Array = AESEncryptUtils.fromUrlSafeBase64(encryptedText);
// 分离 IV 和密文
if (payload.length < IV_LENGTH_BYTES) {
throw new Error(TAG+' payload Uint8Array 长度错误');
}
const iv = payload.subarray(0, IV_LENGTH_BYTES);
const ciphertext = payload.subarray(IV_LENGTH_BYTES);
// 配置解密参数
const ivParams: cryptoFramework.IvParamsSpec = {
algName: 'IvParamsSpec',
iv: { data: iv }
};
// 初始化解密器
const cipher = cryptoFramework.createCipher(ENCRYPT_ALGO);
await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, key, ivParams);
// 执行解密
const decryptData = await cipher.doFinal({ data: ciphertext });
// 转换为字符串
const textDecoder = new util.TextDecoder();
return textDecoder.decodeWithStream(decryptData.data);
} catch (error) {
console.error(`${TAG} error: ${JSON.stringify(error)}, encryptedText:${encryptedText}`);
return null;
}
}
1. Tips:最好是在App冷启动的时候就启动密匙的初始化,如果要使用RSA、SM2进行非对称加密的时候,必须创建两个cipher对象来分别进行加密和解密操作,建议对每次update和doFinal的结果都判断是否为null,并在结果不为null时取出其中的数据进行拼接,形成完整的密文/明文。RSA、SM2非对称加解密不支持update操作。
简短流程:
1派生密匙:
1.1 使用cryptoFramework.createMd('SHA256')创建消息摘要对象。
1.2使用TextEncoder将字符串id转换为Uint8Array(相当于UTF-8字节数组)。
1.3 调用md.update()方法更新数据,注意参数类型为DataBlob(即{ data: Uint8Array })。
1.4 调用md.digest()方法得到摘要结果,返回DataBlob(包含data属性,为Uint8Array)
1.5使用cryptoFramework.createSymKeyGenerator('AES256')创建AES-256密匙生成器
1.6 使用完整32字节作为密匙,调用密匙生成器的convertKey(密匙)生成派生密匙
2.加密
2.1通过AESEncryptUtils.generateRandomBytes(16) 生成随机IV
2.2配置加密参数
2.3使用cryptoFramework.createCipher创建加密器
2.4创建TextEncoder对象,使用加密器的doFinal方法执行加密
2.5通过Uint8Array组合IV和密文
其中涉及到Base64和字节数组的转换:通过Base64Helper对象的encodeToString和decode等方法来实现Unit8Array和base64的互相转换,
完成Base64解码并分离密文就可以通过解密器来执行解密了,注意解密完之后的数据是DataBlod格式的数据,仍需通过TextDecoder对象来进行转换