1. 后端,web和小程序统一加密协议(都使用AES-GCM)
// 统一加密协议规范
const CRYPTO_PROTOCOL = {
version: '1.0',
algorithm: 'AES-256-GCM',
keyDerivation: 'PBKDF2',
keyLength: 256,
ivLength: 96,
tagLength: 128,
iterations: 100000
}
2. 后端统一加密服务(java等后端略)
const crypto = require('crypto')
class UnifiedCryptoService {
constructor() {
this.sessions = new Map()
}
generateSessionKey(userId, deviceId, platform) {
const sessionId = crypto.randomUUID()
const sessionKey = crypto.randomBytes(32)
const sessionInfo = {
sessionId,
userId,
deviceId,
platform,
key: sessionKey,
createdAt: Date.now(),
expiresAt: Date.now() + (24 * 60 * 60 * 1000),
lastUsed: Date.now()
}
this.sessions.set(sessionId, sessionInfo)
return {
sessionId,
expiresAt: sessionInfo.expiresAt
}
}
encrypt(sessionId, plaintext) {
const session = this.sessions.get(sessionId)
if (!session || session.expiresAt < Date.now()) {
throw new Error('会话不存在或已过期')
}
session.lastUsed = Date.now()
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipherGCM('aes-256-gcm', session.key)
cipher.setIVLength(12)
cipher.update(iv)
let encrypted = cipher.update(plaintext, 'utf8', 'hex')
encrypted += cipher.final('hex')
const tag = cipher.getAuthTag()
return {
iv: iv.toString('hex'),
data: encrypted,
tag: tag.toString('hex'),
algorithm: CRYPTO_PROTOCOL.algorithm
}
}
decrypt(sessionId, encryptedPacket) {
const session = this.sessions.get(sessionId)
if (!session || session.expiresAt < Date.now()) {
throw new Error('会话不存在或已过期')
}
session.lastUsed = Date.now()
const { iv, data, tag } = encryptedPacket
const decipher = crypto.createDecipherGCM('aes-256-gcm', session.key)
decipher.setIVLength(12)
decipher.update(Buffer.from(iv, 'hex'))
decipher.setAuthTag(Buffer.from(tag, 'hex'))
let decrypted = decipher.update(data, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}
}
3. Web 浏览器实现(Web Crypto API 隔离)
class WebCryptoClient {
constructor() {
this.localKey = null
this.keyId = null
}
async generateKey() {
try {
this.localKey = await window.crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
false,
['encrypt', 'decrypt']
)
this.keyId = await this.generateKeyId()
return this.keyId
} catch (error) {
throw new Error('本地密钥生成失败: ' + error.message)
}
}
async generateKeyId() {
const randomData = window.crypto.getRandomValues(new Uint8Array(32))
const hashBuffer = await window.crypto.subtle.digest('SHA-256', randomData)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
async encrypt(data) {
if (!this.localKey) {
throw new Error('密钥未初始化')
}
const iv = window.crypto.getRandomValues(new Uint8Array(12))
const encoder = new TextEncoder()
const encodedData = encoder.encode(JSON.stringify(data))
const encrypted = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
this.localKey,
encodedData
)
return {
keyId: this.keyId,
iv: Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join(''),
data: Array.from(new Uint8Array(encrypted)).map(b => b.toString(16).padStart(2, '0')).join(''),
algorithm: CRYPTO_PROTOCOL.algorithm,
platform: 'web'
}
}
async decrypt(encryptedPacket) {
if (!this.localKey) {
throw new Error('密钥未初始化')
}
if (encryptedPacket.keyId !== this.keyId) {
throw new Error('密钥ID不匹配')
}
const { iv, data } = encryptedPacket
const ivArray = new Uint8Array(iv.match(/.{2}/g).map(byte => parseInt(byte, 16)))
const dataArray = new Uint8Array(data.match(/.{2}/g).map(byte => parseInt(byte, 16)))
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivArray
},
this.localKey,
dataArray
)
const decoder = new TextDecoder()
const decryptedText = decoder.decode(decrypted)
return JSON.parse(decryptedText)
}
static isSupported() {
return typeof window !== 'undefined' &&
window.crypto &&
window.crypto.subtle &&
typeof window.crypto.subtle.generateKey === 'function'
}
destroy() {
this.localKey = null
this.keyId = null
}
}
4. 小程序实现(基于用户身份和设备特征的密钥隔离)
class MiniProgramCryptoClient {
constructor() {
this.platformKeyHandle = null
this.keyId = null
}
async generateKey() {
try {
const userAuthData = await this.getUserAuthData()
const deviceFingerprint = await this.getDeviceFingerprint()
this.platformKeyHandle = await this.createPlatformKeyHandle(
userAuthData,
deviceFingerprint
)
this.keyId = this.generateKeyId()
return this.keyId
} catch (error) {
throw new Error('平台密钥生成失败: ' + error.message)
}
}
async getUserAuthData() {
return new Promise((resolve) => {
wx.getUserProfile({
desc: '用于安全加密',
success: (res) => {
resolve({
encryptedData: res.encryptedData,
iv: res.iv,
signature: res.signature
})
},
fail: () => {
console.warn('用户授权失败,使用设备标识')
resolve(null)
}
})
})
}
async getDeviceFingerprint() {
const systemInfo = wx.getSystemInfoSync()
const networkInfo = await this.getNetworkInfo()
return {
model: systemInfo.model,
system: systemInfo.system,
version: systemInfo.version,
platform: systemInfo.platform,
brand: systemInfo.brand,
screenWidth: systemInfo.screenWidth,
screenHeight: systemInfo.screenHeight,
pixelRatio: systemInfo.pixelRatio,
networkType: networkInfo.networkType,
appId: wx.getAccountInfoSync().miniProgram.appId,
sessionStart: Date.now()
}
}
async getNetworkInfo() {
return new Promise((resolve) => {
wx.getNetworkType({
success: resolve,
fail: () => resolve({ networkType: 'unknown' })
})
})
}
generateKeyId() {
const CryptoJS = require('crypto-js')
const randomData = Date.now().toString() + Math.random().toString()
return CryptoJS.SHA256(randomData).toString()
}
async createPlatformKeyHandle(userAuthData, deviceFingerprint) {
const CryptoJS = require('crypto-js')
const keyMaterial = JSON.stringify({
userAuth: userAuthData,
device: deviceFingerprint,
timestamp: Date.now()
})
const keyHandle = CryptoJS.SHA256(keyMaterial + Math.random()).toString()
await this.storeKeyMaterialSecurely(keyHandle, keyMaterial)
return keyHandle
}
async storeKeyMaterialSecurely(keyHandle, keyMaterial) {
const CryptoJS = require('crypto-js')
try {
const deviceKey = this.getDeviceSpecificKey()
const layer1 = CryptoJS.AES.encrypt(keyMaterial, deviceKey).toString()
const timeKey = Date.now().toString() + Math.random().toString()
const layer2 = CryptoJS.AES.encrypt(layer1, timeKey).toString()
const fragment1 = layer2.substring(0, Math.floor(layer2.length / 2))
const fragment2 = layer2.substring(Math.floor(layer2.length / 2))
wx.setStorageSync(`key_frag1_${keyHandle}`, {
data: fragment1,
timestamp: Date.now(),
expires: Date.now() + (24 * 60 * 60 * 1000)
})
wx.setStorageSync(`key_frag2_${keyHandle}`, {
data: fragment2,
timeKey: timeKey,
timestamp: Date.now(),
expires: Date.now() + (24 * 60 * 60 * 1000)
})
} catch (error) {
throw new Error('密钥存储失败')
}
}
getDeviceSpecificKey() {
const systemInfo = wx.getSystemInfoSync()
const CryptoJS = require('crypto-js')
return CryptoJS.SHA256(
systemInfo.model +
systemInfo.system +
systemInfo.brand +
wx.getAccountInfoSync().miniProgram.appId
).toString().substring(0, 32)
}
async reconstructKeyMaterial(keyHandle) {
const CryptoJS = require('crypto-js')
try {
const frag1 = wx.getStorageSync(`key_frag1_${keyHandle}`)
const frag2 = wx.getStorageSync(`key_frag2_${keyHandle}`)
if (!frag1 || !frag2 || frag1.expires < Date.now() || frag2.expires < Date.now()) {
throw new Error('密钥已过期或不存在')
}
const layer2 = frag1.data + frag2.data
const layer1 = CryptoJS.AES.decrypt(layer2, frag2.timeKey).toString(CryptoJS.enc.Utf8)
const deviceKey = this.getDeviceSpecificKey()
const keyMaterial = CryptoJS.AES.decrypt(layer1, deviceKey).toString(CryptoJS.enc.Utf8)
return keyMaterial
} catch (error) {
throw new Error('密钥重构失败')
}
}
async deriveEncryptionKey(keyHandle) {
const CryptoJS = require('crypto-js')
const keyMaterial = await this.reconstructKeyMaterial(keyHandle)
const salt = CryptoJS.SHA256(keyMaterial).toString().substring(0, 16)
const key = CryptoJS.PBKDF2(keyMaterial, salt, {
keySize: 256/32,
iterations: CRYPTO_PROTOCOL.iterations
})
return key.toString()
}
async encrypt(data) {
if (!this.platformKeyHandle) {
throw new Error('密钥未初始化')
}
const CryptoJS = require('crypto-js')
try {
const encryptionKey = await this.deriveEncryptionKey(this.platformKeyHandle)
const iv = CryptoJS.lib.WordArray.random(96/8)
const encrypted = CryptoJS.AES.encrypt(
JSON.stringify(data),
encryptionKey,
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
return {
keyId: this.keyId,
iv: iv.toString(),
data: encrypted.ciphertext.toString(),
algorithm: 'AES-256-CBC',
platform: 'miniprogram'
}
} catch (error) {
throw new Error('加密失败: ' + error.message)
} finally {
if (typeof wx.triggerGC === 'function') {
wx.triggerGC()
}
}
}
async decrypt(encryptedPacket) {
if (!this.platformKeyHandle) {
throw new Error('密钥未初始化')
}
if (encryptedPacket.keyId !== this.keyId) {
throw new Error('密钥ID不匹配')
}
const CryptoJS = require('crypto-js')
try {
const { iv, data } = encryptedPacket
const decryptionKey = await this.deriveEncryptionKey(this.platformKeyHandle)
const decrypted = CryptoJS.AES.decrypt(
data,
decryptionKey,
{
iv: CryptoJS.enc.Hex.parse(iv),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
)
const decryptedText = decrypted.toString(CryptoJS.enc.Utf8)
return JSON.parse(decryptedText)
} catch (error) {
throw new Error('解密失败: ' + error.message)
} finally {
if (typeof wx.triggerGC === 'function') {
wx.triggerGC()
}
}
}
static isSupported() {
return typeof wx !== 'undefined' &&
wx.getSystemInfoSync &&
wx.setStorageSync &&
wx.getStorageSync
}
destroy() {
if (this.platformKeyHandle) {
try {
wx.removeStorageSync(`key_frag1_${this.platformKeyHandle}`)
wx.removeStorageSync(`key_frag2_${this.platformKeyHandle}`)
} catch (error) {
console.warn('清理本地密钥存储失败:', error)
}
}
this.platformKeyHandle = null
this.keyId = null
if (typeof wx.triggerGC === 'function') {
wx.triggerGC()
}
}
async refreshKey() {
this.destroy()
return await this.generateKey()
}
async validateKey() {
if (!this.platformKeyHandle || !this.keyId) {
return false
}
try {
await this.reconstructKeyMaterial(this.platformKeyHandle)
return true
} catch (error) {
return false
}
}
}