构建零知识健康应用:React 客户端加密完整指南
概述
零知识健康应用的核心原则是服务端零数据——用户的敏感健康信息在离开设备前就已加密,服务器永远无法访问明文数据。本文将详细介绍如何在 React 应用中实现这一架构。
核心概念
- 客户端加密:数据在用户设备上加密,服务器只存储密文
- 端到端加密(E2EE):只有用户持有解密密钥
- 零知识架构:服务器无法访问明文,但仍可提供计算服务
- 密钥主权:用户完全控制自己的加密密钥
技术架构
code
┌─────────────────────────────────────────────────────────────────┐
│ 用户设备(浏览器) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 健康数据 │ ──加密──▶│ 加密数据 │ ──上传──▶│ 本地存储 │ │
│ │ (明文) │ │ (密文) │ │ (密钥) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼ HTTPS
┌─────────────────────────────────────────────────────────────────┐
│ 服务器(无法解密) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 密文存储 │ │ 元数据索引 │ │ 计算服务 │ │
│ │ (无法读取)│ │ (可搜索) │ │ (无需解密)│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Code collapsed
1. Web Crypto API 基础
密钥生成与管理
code
// lib/crypto/keys.ts
/**
* 生成 AES-GCM 加密密钥
*/
export async function generateEncryptionKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256, // AES-256
},
true, // 可导出(用于备份)
['encrypt', 'decrypt']
);
}
/**
* 从密码派生密钥(PBKDF2)
*/
export async function deriveKeyFromPassword(
password: string,
salt: Uint8Array,
iterations: number = 100000
): Promise<CryptoKey> {
// 导入密码作为密钥材料
const keyMaterial = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey']
);
// 派生 AES-GCM 密钥
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: iterations,
hash: 'SHA-256',
},
keyMaterial,
{
name: 'AES-GCM',
length: 256,
},
true,
['encrypt', 'decrypt']
);
}
/**
* 生成 ECDH 密钥对(用于密钥交换)
*/
export async function generateKeyPair(): Promise<CryptoKeyPair> {
return await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256',
},
true,
['deriveKey', 'deriveBits']
);
}
/**
* 导出密钥为 JWK 格式
*/
export async function exportKeyToJWK(key: CryptoKey): Promise<JsonWebKey> {
return await crypto.subtle.exportKey('jwk', key);
}
/**
* 从 JWK 导入密钥
*/
export async function importKeyFromJWK(
jwk: JsonWebKey,
keyUsages: KeyUsage[]
): Promise<CryptoKey> {
return await crypto.subtle.importKey(
'jwk',
jwk,
{ name: 'AES-GCM' },
true,
keyUsages
);
}
Code collapsed
加密与解密
code
// lib/crypto/encryption.ts
export interface EncryptedData {
ciphertext: string; // Base64 编码的密文
iv: string; // 初始化向量
salt?: string; // 盐值(如果使用密码派生)
tag?: string; // 认证标签
}
/**
* AES-GCM 加密
*/
export async function encryptData(
plaintext: string,
key: CryptoKey
): Promise<EncryptedData> {
// 生成随机初始化向量(12 字节是 GCM 的推荐值)
const iv = crypto.getRandomValues(new Uint8Array(12));
// 加密数据
const ciphertext = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
new TextEncoder().encode(plaintext)
);
return {
ciphertext: bufferToBase64(ciphertext),
iv: bufferToBase64(iv),
};
}
/**
* AES-GCM 解密
*/
export async function decryptData(
encryptedData: EncryptedData,
key: CryptoKey
): Promise<string> {
const ciphertext = base64ToBuffer(encryptedData.ciphertext);
const iv = base64ToBuffer(encryptedData.iv);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
},
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
}
/**
* 加密 JSON 对象
*/
export async function encryptJSON<T>(
data: T,
key: CryptoKey
): Promise<EncryptedData> {
return await encryptData(JSON.stringify(data), key);
}
/**
* 解密为 JSON 对象
*/
export async function decryptJSON<T>(
encryptedData: EncryptedData,
key: CryptoKey
): Promise<T> {
const plaintext = await decryptData(encryptedData, key);
return JSON.parse(plaintext) as T;
}
// 辅助函数
function bufferToBase64(buffer: ArrayBuffer): string {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
function base64ToBuffer(base64: string): ArrayBuffer {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
Code collapsed
2. React Hooks 实现
加密存储 Hook
code
// hooks/useEncryptedStorage.ts
import { useState, useEffect, useCallback } from 'react';
import { encryptData, decryptData, EncryptedData } from '@/lib/crypto/encryption';
import { generateEncryptionKey, exportKeyToJWK, importKeyFromJWK } from '@/lib/crypto/keys';
interface StorageOptions {
sessionStorage?: boolean;
autoSync?: boolean;
}
export function useEncryptedStorage<T>(
key: string,
masterKey: CryptoKey | null,
options: StorageOptions = {}
) {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [isEncrypted, setIsEncrypted] = useState(false);
const storage = options.sessionStorage ? sessionStorage : localStorage;
// 加载并解密数据
useEffect(() => {
if (!masterKey) {
setIsLoading(false);
return;
}
async function load() {
try {
const encryptedStr = storage.getItem(`encrypted_${key}`);
if (!encryptedStr) {
setIsLoading(false);
return;
}
const encrypted: EncryptedData = JSON.parse(encryptedStr);
const decrypted = await decryptData(encrypted, masterKey);
setData(JSON.parse(decrypted));
setIsEncrypted(true);
} catch (err) {
setError(err as Error);
} finally {
setIsLoading(false);
}
}
load();
}, [key, masterKey, storage]);
// 保存加密数据
const save = useCallback(
async (newData: T) => {
if (!masterKey) {
throw new Error('主密钥未设置');
}
try {
const plaintext = JSON.stringify(newData);
const encrypted = await encryptData(plaintext, masterKey);
storage.setItem(`encrypted_${key}`, JSON.stringify(encrypted));
setData(newData);
setIsEncrypted(true);
} catch (err) {
setError(err as Error);
throw err;
}
},
[key, masterKey, storage]
);
// 清除数据
const clear = useCallback(() => {
storage.removeItem(`encrypted_${key}`);
setData(null);
setIsEncrypted(false);
}, [key, storage]);
return {
data,
isLoading,
error,
isEncrypted,
save,
clear,
hasKey: !!masterKey,
};
}
Code collapsed
密钥管理 Hook
code
// hooks/useEncryptionKeys.ts
import { useState, useCallback, useEffect } from 'react';
import {
generateEncryptionKey,
deriveKeyFromPassword,
exportKeyToJWK,
importKeyFromJWK,
} from '@/lib/crypto/keys';
const KEY_STORAGE_KEY = 'encryption_key_jwk';
export function useEncryptionKeys() {
const [masterKey, setMasterKey] = useState<CryptoKey | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
const [isLocked, setIsLocked] = useState(true);
// 从会话存储恢复密钥
useEffect(() => {
const jwkStr = sessionStorage.getItem(KEY_STORAGE_KEY);
if (jwkStr) {
importKeyFromJWK(JSON.parse(jwkStr), ['encrypt', 'decrypt'])
.then(setMasterKey)
.then(() => {
setIsLocked(false);
setIsInitialized(true);
})
.catch(() => {
sessionStorage.removeItem(KEY_STORAGE_KEY);
setIsInitialized(true);
});
} else {
setIsInitialized(true);
}
}, []);
// 创建新密钥
const createKey = useCallback(async () => {
const key = await generateEncryptionKey();
await setMasterKey(key);
setIsLocked(false);
// 存储到会话存储(浏览器关闭时清除)
const jwk = await exportKeyToJWK(key);
sessionStorage.setItem(KEY_STORAGE_KEY, JSON.stringify(jwk));
return key;
}, []);
// 从密码恢复密钥
const unlockWithPassword = useCallback(async (password: string, salt: string) => {
const saltBuffer = Uint8Array.from(atob(salt), c => c.charCodeAt(0));
const key = await deriveKeyFromPassword(password, saltBuffer);
setMasterKey(key);
setIsLocked(false);
const jwk = await exportKeyToJWK(key);
sessionStorage.setItem(KEY_STORAGE_KEY, JSON.stringify(jwk));
}, []);
// 锁定密钥
const lock = useCallback(() => {
setMasterKey(null);
setIsLocked(true);
sessionStorage.removeItem(KEY_STORAGE_KEY);
}, []);
// 导出密钥用于备份
const exportKey = useCallback(async () => {
if (!masterKey) return null;
return await exportKeyToJWK(masterKey);
}, [masterKey]);
// 导入密钥恢复
const importKey = useCallback(async (jwk: JsonWebKey) => {
const key = await importKeyFromJWK(jwk, ['encrypt', 'decrypt']);
setMasterKey(key);
setIsLocked(false);
sessionStorage.setItem(KEY_STORAGE_KEY, JSON.stringify(jwk));
}, []);
return {
masterKey,
isInitialized,
isLocked,
createKey,
unlockWithPassword,
lock,
exportKey,
importKey,
hasKey: !!masterKey,
};
}
Code collapsed
健康数据加密 Hook
code
// hooks/useEncryptedHealthData.ts
import { useState } from 'react';
import { useEncryptedStorage } from './useEncryptedStorage';
import { useEncryptionKeys } from './useEncryptionKeys';
export interface HealthData {
bloodPressure?: { systolic: number; diastolic: number; date: string };
heartRate?: { bpm: number; date: string };
weight?: { kg: number; date: string };
medications?: Array<{ name: string; dosage: string; frequency: string }>;
notes?: string;
}
export function useEncryptedHealthData() {
const keys = useEncryptionKeys();
const healthData = useEncryptedStorage<HealthData>(
'health_data',
keys.masterKey
);
const [isSaving, setIsSaving] = useState(false);
const updateHealthData = async (updates: Partial<HealthData>) => {
setIsSaving(true);
try {
const newData = { ...healthData.data, ...updates };
await healthData.save(newData);
} finally {
setIsSaving(false);
}
};
return {
...healthData,
...keys,
isSaving,
updateHealthData,
};
}
Code collapsed
3. UI 组件实现
加密状态指示器
code
// components/encryption/EncryptionStatus.tsx
'use client';
import { Lock, Unlock, Shield, ShieldAlert } from 'lucide-react';
interface EncryptionStatusProps {
isEncrypted: boolean;
hasKey: boolean;
algorithm?: string;
}
export function EncryptionStatus({
isEncrypted,
hasKey,
algorithm = 'AES-256-GCM',
}: EncryptionStatusProps) {
if (hasKey && isEncrypted) {
return (
<div className="flex items-center gap-2 px-3 py-2 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<Shield className="w-4 h-4 text-green-600 dark:text-green-400" />
<div className="text-sm">
<span className="font-medium text-green-700 dark:text-green-300">
数据已加密
</span>
<span className="text-green-600 dark:text-green-400 ml-2">
({algorithm})
</span>
</div>
</div>
);
}
if (!hasKey) {
return (
<div className="flex items-center gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<Unlock className="w-4 h-4 text-amber-600 dark:text-amber-400" />
<div className="text-sm text-amber-700 dark:text-amber-300">
请输入密码解密数据
</div>
</div>
);
}
return (
<div className="flex items-center gap-2 px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<ShieldAlert className="w-4 h-4 text-red-600 dark:text-red-400" />
<div className="text-sm text-red-700 dark:text-red-300">
数据未加密
</div>
</div>
);
}
Code collapsed
密码设置组件
code
// components/encryption/PasswordSetup.tsx
'use client';
import { useState } from 'react';
import { Lock, Eye, EyeOff } from 'lucide-react';
import { useEncryptionKeys } from '@/hooks/useEncryptionKeys';
export function PasswordSetup() {
const { createKey, unlockWithPassword, isLocked, hasKey } = useEncryptionKeys();
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [error, setError] = useState('');
const [isNewUser, setIsNewUser] = useState(!hasKey);
const [salt, setSalt] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (isNewUser) {
if (password.length < 12) {
setError('密码长度至少需要 12 个字符');
return;
}
if (password !== confirmPassword) {
setError('两次输入的密码不一致');
return;
}
// 生成盐值
const saltBuffer = crypto.getRandomValues(new Uint8Array(32));
const saltB64 = btoa(String.fromCharCode(...saltBuffer));
setSalt(saltB64);
// 创建并存储密钥
await createKey();
// 保存盐值到本地(实际应用中应存储到服务器)
localStorage.setItem('encryption_salt', saltB64);
} else {
const storedSalt = localStorage.getItem('encryption_salt');
if (!storedSalt) {
setError('未找到加密配置,请重新设置');
setIsNewUser(true);
return;
}
await unlockWithPassword(password, storedSalt);
}
};
if (!isLocked) return null;
return (
<div className="max-w-md mx-auto p-6 bg-white dark:bg-gray-800 rounded-xl shadow-lg">
<div className="flex items-center gap-3 mb-6">
<div className="p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<Lock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 className="text-xl font-semibold">
{isNewUser ? '设置加密密码' : '输入密码解锁'}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
您的健康数据将使用端到端加密保护
</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
密码
</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 pr-12 border rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="至少 12 个字符"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>
{isNewUser && (
<div>
<label className="block text-sm font-medium mb-2">
确认密码
</label>
<input
type={showPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="再次输入密码"
required
/>
</div>
)}
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600 dark:text-red-400">
{error}
</div>
)}
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg text-sm text-blue-700 dark:text-blue-300">
<strong>重要提示:</strong>密码无法恢复。请务必记住您的密码,否则将无法访问加密数据。
</div>
<button
type="submit"
className="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
>
{isNewUser ? '创建加密密钥' : '解锁'}
</button>
</form>
</div>
);
}
Code collapsed
健康数据输入表单
code
// components/health/HealthDataForm.tsx
'use client';
import { useState } from 'react';
import { useEncryptedHealthData } from '@/hooks/useEncryptedHealthData';
import { EncryptionStatus } from '../encryption/EncryptionStatus';
export function HealthDataForm() {
const { data, isEncrypted, hasKey, isSaving, updateHealthData, isLocked } =
useEncryptedHealthData();
const [formData, setFormData] = useState({
systolic: data?.bloodPressure?.systolic || '',
diastolic: data?.bloodPressure?.diastolic || '',
heartRate: data?.heartRate?.bpm || '',
weight: data?.weight?.kg || '',
notes: data?.notes || '',
});
if (isLocked) {
return (
<div className="text-center py-12">
<p className="text-gray-500">请先解锁以访问健康数据</p>
</div>
);
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await updateHealthData({
bloodPressure: {
systolic: Number(formData.systolic),
diastolic: Number(formData.diastolic),
date: new Date().toISOString(),
},
heartRate: formData.heartRate ? {
bpm: Number(formData.heartRate),
date: new Date().toISOString(),
} : undefined,
weight: formData.weight ? {
kg: Number(formData.weight),
date: new Date().toISOString(),
} : undefined,
notes: formData.notes || undefined,
});
};
return (
<div className="max-w-2xl mx-auto p-6 bg-white dark:bg-gray-800 rounded-xl shadow-lg">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">健康数据记录</h2>
<EncryptionStatus isEncrypted={isEncrypted} hasKey={hasKey} />
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* 血压 */}
<div>
<label className="block text-sm font-medium mb-2">血压 (mmHg)</label>
<div className="grid grid-cols-2 gap-4">
<div>
<input
type="number"
value={formData.systolic}
onChange={(e) => setFormData({ ...formData, systolic: e.target.value })}
placeholder="收缩压"
className="w-full px-4 py-3 border rounded-lg"
/>
<p className="text-xs text-gray-500 mt-1">收缩压(高压)</p>
</div>
<div>
<input
type="number"
value={formData.diastolic}
onChange={(e) => setFormData({ ...formData, diastolic: e.target.value })}
placeholder="舒张压"
className="w-full px-4 py-3 border rounded-lg"
/>
<p className="text-xs text-gray-500 mt-1">舒张压(低压)</p>
</div>
</div>
</div>
{/* 心率 */}
<div>
<label className="block text-sm font-medium mb-2">心率 (BPM)</label>
<input
type="number"
value={formData.heartRate}
onChange={(e) => setFormData({ ...formData, heartRate: e.target.value })}
placeholder="每分钟心跳次数"
className="w-full px-4 py-3 border rounded-lg"
/>
</div>
{/* 体重 */}
<div>
<label className="block text-sm font-medium mb-2">体重 (kg)</label>
<input
type="number"
step="0.1"
value={formData.weight}
onChange={(e) => setFormData({ ...formData, weight: e.target.value })}
placeholder="您的体重"
className="w-full px-4 py-3 border rounded-lg"
/>
</div>
{/* 备注 */}
<div>
<label className="block text-sm font-medium mb-2">备注</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="添加备注..."
rows={3}
className="w-full px-4 py-3 border rounded-lg resize-none"
/>
</div>
<button
type="submit"
disabled={isSaving}
className="w-full py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium rounded-lg transition-colors"
>
{isSaving ? '加密保存中...' : '加密保存'}
</button>
<p className="text-xs text-center text-gray-500">
数据将在您的设备上使用 AES-256-GCM 加密后再上传
</p>
</form>
</div>
);
}
Code collapsed
4. 安全最佳实践
密码强度验证
code
// lib/crypto/password-strength.ts
export interface PasswordStrengthResult {
score: number; // 0-4
feedback: string[];
isStrong: boolean;
}
export function checkPasswordStrength(password: string): PasswordStrengthResult {
const feedback: string[] = [];
let score = 0;
// 长度检查
if (password.length >= 12) score += 1;
else feedback.push('密码长度应至少为 12 个字符');
if (password.length >= 16) score += 1;
// 包含大写字母
if (/[A-Z]/.test(password)) score += 1;
else feedback.push('添加大写字母');
// 包含小写字母
if (/[a-z]/.test(password)) score += 1;
else feedback.push('添加小写字母');
// 包含数字
if (/[0-9]/.test(password)) score += 1;
else feedback.push('添加数字');
// 包含特殊字符
if (/[^A-Za-z0-9]/.test(password)) score += 1;
else feedback.push('添加特殊字符(!@#$%^&*)');
// 检查常见模式
if (/(.)\1{2,}/.test(password)) {
score -= 1;
feedback.push('避免重复字符');
}
// 检查常见密码
const commonPasswords = ['password', '123456', 'qwerty', 'admin'];
if (commonPasswords.some(p => password.toLowerCase().includes(p))) {
score = 0;
feedback.push('不要使用常见密码');
}
return {
score: Math.max(0, Math.min(4, score)),
feedback,
isStrong: score >= 3,
};
}
Code collapsed
安全随机数生成
code
// lib/crypto/secure-random.ts
/**
* 生成加密安全的随机字符串
*/
export function generateSecureRandom(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const randomValues = crypto.getRandomValues(new Uint8Array(length));
return Array.from(randomValues, b => chars[b % chars.length]).join('');
}
/**
* 生成加密盐值
*/
export function generateSalt(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(32));
}
/**
* 生成会话 ID
*/
export function generateSessionId(): string {
const timestamp = Date.now().toString(36);
const random = generateSecureRandom(16);
return `${timestamp}-${random}`;
}
Code collapsed
密钥轮换策略
code
// lib/crypto/key-rotation.ts
export interface KeyRotationResult {
success: boolean;
re encrypted: number;
failed: number;
}
export async function rotateEncryptionKey(
oldKey: CryptoKey,
newKey: CryptoKey
): Promise<KeyRotationResult> {
let re encrypted = 0;
let failed = 0;
// 获取所有加密的数据项
const keys = Object.keys(localStorage).filter(k => k.startsWith('encrypted_'));
for (const storageKey of keys) {
try {
const encryptedStr = localStorage.getItem(storageKey);
if (!encryptedStr) continue;
const encrypted = JSON.parse(encryptedStr);
// 使用旧密钥解密
const decrypted = await decryptData(encrypted, oldKey);
// 使用新密钥加密
const re encryptedData = await encryptData(decrypted, newKey);
// 保存
localStorage.setItem(storageKey, JSON.stringify(re encryptedData));
re encrypted++;
} catch (error) {
console.error(`密钥轮换失败: ${storageKey}`, error);
failed++;
}
}
return { success: failed === 0, re encrypted, failed };
}
Code collapsed
5. 合规性实现
HIPAA 合规检查清单
code
// lib/compliance/hipaa-checklist.ts
export const HIPAA_SECURITY_CHECKLIST = {
administrative: {
securityManagement: [
'实施安全意识和培训计划',
'定期进行安全风险评估',
'制定应急响应计划',
'指定安全官员',
],
policies: [
'制定访问控制政策',
'制定安全事件处理程序',
'制定业务连续性计划',
],
},
physical: {
accessControl: [
'限制对电子介质的物理访问',
'实施访问验证系统',
'维护访问控制记录',
],
deviceSecurity: [
'移动设备加密要求',
'设备丢失/被盗报告程序',
'设备处置政策',
],
},
technical: {
accessControl: [
'唯一用户标识符',
'基于角色的访问控制',
'自动注销功能',
'紧急访问程序',
],
auditControls: [
'记录所有访问活动',
'审计日志完整性保护',
'定期审计日志审查',
],
integrity: [
'数据完整性验证机制',
'防篡改措施',
'数字签名实施',
],
transmission: [
'端到端加密',
'传输完整性保护',
'网络安全防火墙',
],
},
};
Code collapsed
GDPR 合规功能
code
// app/api/user/data-export/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { decryptData } from '@/lib/crypto/encryption';
export async function GET(req: NextRequest) {
// 验证用户身份...
// 获取用户所有加密数据
const userData = {
profile: await getUserEncryptedProfile(userId),
healthData: await getUserEncryptedHealthData(userId),
metadata: await getUserMetadata(userId),
};
// 生成可读报告(仅限用户访问)
const report = generateDataReport(userData);
return NextResponse.json({
downloadUrl: await generateSecureDownloadLink(report),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
}
// app/api/user/data-delete/route.ts
export async function DELETE(req: NextRequest) {
// 验证用户身份...
// 删除本地加密数据
await deleteAllUserData(userId);
// 请求合作方删除数据
await notifyDataDeletionToThirdParties(userId);
return NextResponse.json({
message: '数据删除请求已处理',
deletedAt: new Date().toISOString(),
});
}
Code collapsed
6. 安全检查清单
密码学
- 使用业界认可的加密库(Web Crypto API)
- 加密算法:AES-256-GCM
- 密钥派生:PBKDF2,至少 100,000 次迭代
- 每次加密使用唯一 IV
- 密钥不存储在代码或版本控制中
密钥管理
- 密钥仅存储在用户设备(会话存储)
- 支持密钥备份和恢复
- 实施密钥轮换机制
- 安全的密钥销毁流程
数据保护
- 敏感数据始终加密存储
- 传输数据使用 HTTPS/TLS 1.3
- 实施数据最小化原则
- 支持被遗忘权(数据删除)
访问控制
- 强密码策略(最少 12 字符)
- 会话自动超时
- 多因素认证支持
- 异常访问检测
审计与监控
- 记录所有加密操作
- 监控异常访问模式
- 定期安全审计
- 事件响应计划
参考资料
免责声明:本文提供的代码示例仅供学习参考。在生产环境中处理健康数据前,请务必进行安全审计,并确保符合 HIPAA、GDPR 等相关法规要求。