@mijadesign/api-crypto
v0.1.0-beta.0
Published
Cross-runtime AES-256-GCM payload protocol for mobile clients
Keywords
Readme
@mijadesign/api-crypto
跨运行时的 Payload AES-256-GCM 协议实现,兼容微信小程序、React Native 和 H5。
本包只负责协议和密码学操作,不负责获取、缓存或维护 kid/key,也不依赖 Taro、Axios、Fetch 或 React Native。调用方必须注入当前请求使用的 kid/key 和安全随机数实现。
第三方密码库 @noble/ciphers 是运行时依赖,构建产物不会内联该依赖。最终应用由自身构建工具决定如何将依赖放入终端产物。
协议
- AES-256-GCM;
- 12 字节随机 IV;
- 16 字节认证 Tag;
- Payload 格式为
{"ct":"<base64url>"}; - AAD 格式为
v1|direction|METHOD|path|kid; - GET/HEAD 只请求响应加密,其他请求加密 JSON Body。
运行时适配
- 微信小程序注入
Taro.getRandomValues; - React Native 注入经平台验证的安全随机数;
- H5 注入
globalThis.crypto.getRandomValues。
各调用方负责把自身网络库的 Header、Body 和 Response 映射为本包的无框架类型。
安装
pnpm add @mijadesign/api-crypto@noble/ciphers 是本包的运行时依赖,会由包管理器安装;本包构建产物不会内联它。
基本用法
调用方必须自行提供当前有效的 kid/key。Key 必须是 32 字节 AES-256 密钥的 Base64URL 表示,不能使用普通字符串直接填入。
import {
decryptCrypto,
encryptCrypto,
type cryptoKey,
type RandomBytes,
} from '@mijadesign/api-crypto';
const key: cryptoKey = {
kid: currentKid,
keyB64url: currentKeyB64url,
};
const randomBytes: RandomBytes = async (length) => {
// 使用当前运行时提供的安全随机数实现,不要使用 Math.random()
return platformRandomBytes(length);
};
const prepared = await encryptCrypto({
key,
randomBytes,
method: 'POST',
url: '/mjsbtcloud/api/example/profile',
body: { name: '张三' },
headers: {
Authorization: 'Bearer <access-token>',
},
});
// 由调用方把 prepared.url、prepared.method、prepared.headers、prepared.body
// 映射到 Taro.request、Axios 或 fetch。
const rawResponse = await sendRequest(prepared);
const response = decryptCrypto(
{
status: rawResponse.status,
headers: rawResponse.headers,
body: rawResponse.body,
},
prepared.context,
);
const data = response.body;encryptCrypto 对 GET/HEAD 请求只添加响应加密 Header,不修改请求 Body;其他请求会将 Body 序列化并替换为 {"ct":"..."}。解密必须在业务错误归一化之前执行,Axios 的 error.response 分支也需要经过同样的解密处理。
各运行时随机数示例
微信小程序
import Taro from '@tarojs/taro';
const randomBytes = async (length: number) => {
const result = await Taro.getRandomValues({ length });
const bytes = new Uint8Array(result.randomValues);
if (bytes.length !== length) throw new Error('Taro.getRandomValues returned a wrong byte length');
return bytes;
};Web
const randomBytes = (length: number) => {
const bytes = new Uint8Array(length);
globalThis.crypto.getRandomValues(bytes);
return bytes;
};H5 必须运行在支持安全随机数的 HTTPS Secure Context 中。
React Native
RN 不应假设所有 Hermes 版本都提供 globalThis.crypto。请注入经过 iOS/Android 真机验证的安全随机数实现,例如由 Native 安全能力或经过验证的随机数模块提供:
const randomBytes = (length: number) => nativeSecureRandomBytes(length);响应处理规则
- 响应包含任意一个
X-Enc-*Header 时,必须校验完整的 Version、Kid、IV 并解密; - 加密请求收到成功的明文 2xx 响应会抛出
PLAINTEXT_SUCCESS_RESPONSE; - 加密响应的认证失败会抛出
DECRYPT_FAILED; - 明文非 2xx 响应且没有加密 Header 时,会原样返回给调用方处理登录失效或业务错误;
- 不要捕获错误后自动切换到明文请求;
- 不要记录 Key、明文 Body、解密后的敏感响应或完整密文。
API 适配建议
公共包不依赖网络库,适配层只需要完成以下映射:
| 网络库 | 请求映射 | 响应映射 |
| --- | --- | --- |
| Taro.request | 使用 prepared.method/url/headers/body | statusCode/header/data 映射为 status/headers/body |
| Axios | 使用 prepared.method/url/headers/body | status/headers/data 映射为 status/headers/body |
| fetch | 使用 prepared.method/url/headers/body | status/headers/text或json 映射为 status/headers/body |
上传、FormData、SSE 和二进制流不应直接套用本包的 JSON Payload 协议,需要使用各自的传输协议。
