康心伴Logo
康心伴WellAlly
Data & Privacy

构建零知识健康应用:React 客户端加密完整指南

5 分钟阅读

构建零知识健康应用: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 等相关法规要求。

#

文章标签

react
encryption
zero-knowledge
healthtech
security
e2ee

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅