# Backup Service 实施指导 ## 1. 概述 ### 1.1 服务定位 `backup-service` 是 RWA Durian 平台的 MPC 备份分片存储服务,负责安全存储用户的 Backup Share (Party 2)。 **核心职责:** - 安全存储 MPC 2-of-3 的第三个分片 (Backup Share) - 提供分片的存取 API - 支持用户账户恢复时的分片验证和获取 - 审计日志记录 ### 1.2 安全要求 **关键安全原则:必须部署在与 identity-service 不同的物理服务器上!** ``` ┌──────────────────────────────────────────────────────────────────┐ │ MPC 分片物理隔离架构 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 物理服务器 A 物理服务器 B 用户设备 │ │ ┌─────────────────┐ ┌─────────────────┐ ┌────────────┐ │ │ │identity-service │ │ backup-service │ │Flutter App │ │ │ │Server Share (0) │ │Backup Share (2) │ │Client (1) │ │ │ └─────────────────┘ └─────────────────┘ └────────────┘ │ │ │ │ 任意单点被攻破,攻击者只能获得 1 个分片,无法重建私钥 │ └──────────────────────────────────────────────────────────────────┘ ``` 如果 Server Share 和 Backup Share 在同一台物理服务器,攻击者攻破该服务器后可获得 2 个分片,直接重建私钥,**MPC 安全性完全失效**! ### 1.3 技术栈 与 identity-service 保持一致: - **框架**: NestJS 10.x - **语言**: TypeScript 5.x - **数据库**: PostgreSQL 15+ (独立数据库实例) - **ORM**: Prisma 5.x - **缓存**: Redis (可选) - **消息队列**: Kafka (可选,用于接收 identity-service 的事件) --- ## 2. 目录结构 参考 identity-service 的 DDD + Hexagonal 架构: ``` backup-service/ ├── prisma/ │ ├── schema.prisma # 数据库模型 │ └── migrations/ # 数据库迁移 ├── src/ │ ├── api/ # 接口层 (Adapters - Driving) │ │ ├── controllers/ │ │ │ └── backup-share.controller.ts │ │ ├── dto/ │ │ │ ├── request/ │ │ │ │ ├── store-share.dto.ts │ │ │ │ └── retrieve-share.dto.ts │ │ │ └── response/ │ │ │ └── share-info.dto.ts │ │ └── api.module.ts │ │ │ ├── application/ # 应用层 (Use Cases) │ │ ├── commands/ │ │ │ ├── store-backup-share/ │ │ │ │ ├── store-backup-share.command.ts │ │ │ │ └── store-backup-share.handler.ts │ │ │ └── revoke-share/ │ │ │ ├── revoke-share.command.ts │ │ │ └── revoke-share.handler.ts │ │ ├── queries/ │ │ │ └── get-backup-share/ │ │ │ ├── get-backup-share.query.ts │ │ │ └── get-backup-share.handler.ts │ │ ├── services/ │ │ │ └── backup-share-application.service.ts │ │ └── application.module.ts │ │ │ ├── domain/ # 领域层 (Core Business Logic) │ │ ├── entities/ │ │ │ └── backup-share.entity.ts │ │ ├── repositories/ │ │ │ └── backup-share.repository.interface.ts │ │ ├── services/ │ │ │ └── share-encryption.domain-service.ts │ │ ├── value-objects/ │ │ │ ├── share-id.vo.ts │ │ │ └── encrypted-data.vo.ts │ │ └── domain.module.ts │ │ │ ├── infrastructure/ # 基础设施层 (Adapters - Driven) │ │ ├── persistence/ │ │ │ ├── prisma/ │ │ │ │ └── prisma.service.ts │ │ │ └── repositories/ │ │ │ └── backup-share.repository.impl.ts │ │ ├── crypto/ │ │ │ └── aes-encryption.service.ts │ │ └── infrastructure.module.ts │ │ │ ├── shared/ # 共享模块 │ │ ├── guards/ │ │ │ └── service-auth.guard.ts # 服务间认证 │ │ ├── filters/ │ │ │ └── global-exception.filter.ts │ │ └── interceptors/ │ │ └── audit-log.interceptor.ts │ │ │ ├── config/ │ │ └── index.ts │ ├── app.module.ts │ └── main.ts │ ├── test/ │ ├── unit/ │ └── e2e/ │ └── backup-share.e2e-spec.ts │ ├── .env.example ├── .env.development ├── package.json ├── tsconfig.json ├── nest-cli.json └── Dockerfile ``` --- ## 3. 数据库设计 ### 3.1 Prisma Schema ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // 备份分片存储 model BackupShare { shareId BigInt @id @default(autoincrement()) @map("share_id") // 用户标识 (来自 identity-service) userId BigInt @unique @map("user_id") accountSequence BigInt @unique @map("account_sequence") // MPC 密钥信息 publicKey String @unique @map("public_key") @db.VarChar(130) partyIndex Int @default(2) @map("party_index") // Backup = Party 2 threshold Int @default(2) totalParties Int @default(3) @map("total_parties") // 加密的分片数据 (AES-256-GCM 加密) encryptedShareData String @map("encrypted_share_data") @db.Text encryptionKeyId String @map("encryption_key_id") @db.VarChar(64) // 密钥轮换支持 // 状态管理 status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, ROTATED // 访问控制 accessCount Int @default(0) @map("access_count") // 访问次数限制 lastAccessedAt DateTime? @map("last_accessed_at") // 时间戳 createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") revokedAt DateTime? @map("revoked_at") // 索引 @@index([publicKey], name: "idx_backup_public_key") @@index([status], name: "idx_backup_status") @@index([createdAt], name: "idx_backup_created") @@map("backup_shares") } // 访问审计日志 model ShareAccessLog { logId BigInt @id @default(autoincrement()) @map("log_id") shareId BigInt @map("share_id") userId BigInt @map("user_id") action String @db.VarChar(20) // STORE, RETRIEVE, REVOKE, ROTATE sourceService String @map("source_service") @db.VarChar(50) // identity-service, recovery-service sourceIp String @map("source_ip") @db.VarChar(45) success Boolean @default(true) errorMessage String? @map("error_message") @db.Text createdAt DateTime @default(now()) @map("created_at") @@index([shareId], name: "idx_log_share") @@index([userId], name: "idx_log_user") @@index([action], name: "idx_log_action") @@index([createdAt], name: "idx_log_created") @@map("share_access_logs") } ``` --- ## 4. API 设计 ### 4.1 存储备份分片 **POST /backup-share/store** 由 identity-service 在创建账户时调用。 ```typescript // Request DTO class StoreBackupShareDto { @IsNotEmpty() userId: string; // identity-service 的用户ID @IsNotEmpty() accountSequence: number; // 账户序列号 @IsNotEmpty() publicKey: string; // MPC 公钥 @IsNotEmpty() encryptedShareData: string; // 已加密的分片数据 @IsOptional() threshold?: number; // 默认 2 @IsOptional() totalParties?: number; // 默认 3 } // Response { "success": true, "shareId": "123", "message": "Backup share stored successfully" } ``` ### 4.2 获取备份分片 **POST /backup-share/retrieve** 用于账户恢复场景,需要额外的身份验证。 ```typescript // Request DTO class RetrieveBackupShareDto { @IsNotEmpty() userId: string; @IsNotEmpty() publicKey: string; @IsNotEmpty() recoveryToken: string; // 恢复令牌 (由 identity-service 签发) @IsOptional() deviceId?: string; // 新设备ID } // Response { "success": true, "encryptedShareData": "...", "partyIndex": 2, "publicKey": "..." } ``` ### 4.3 撤销分片 **POST /backup-share/revoke** 用于密钥轮换或账户注销。 ```typescript // Request DTO class RevokeShareDto { @IsNotEmpty() userId: string; @IsNotEmpty() publicKey: string; @IsNotEmpty() reason: string; // ROTATION, ACCOUNT_CLOSED, SECURITY_BREACH } ``` --- ## 5. 服务间认证 ### 5.1 认证机制 backup-service 不对外公开,只接受来自内部服务的请求。使用 **服务间 JWT** 或 **mTLS** 进行认证。 ```typescript // src/shared/guards/service-auth.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as jwt from 'jsonwebtoken'; @Injectable() export class ServiceAuthGuard implements CanActivate { constructor(private configService: ConfigService) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const serviceToken = request.headers['x-service-token']; if (!serviceToken) { throw new UnauthorizedException('Missing service token'); } try { const secret = this.configService.get('SERVICE_JWT_SECRET'); const payload = jwt.verify(serviceToken, secret) as { service: string }; // 只允许特定服务访问 const allowedServices = ['identity-service', 'recovery-service']; if (!allowedServices.includes(payload.service)) { throw new UnauthorizedException('Service not authorized'); } request.sourceService = payload.service; return true; } catch (error) { throw new UnauthorizedException('Invalid service token'); } } } ``` ### 5.2 identity-service 调用示例 在 identity-service 中添加调用 backup-service 的客户端: ```typescript // identity-service/src/infrastructure/external/backup/backup-client.service.ts @Injectable() export class BackupClientService { private readonly backupServiceUrl: string; private readonly serviceToken: string; constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, ) { this.backupServiceUrl = this.configService.get('BACKUP_SERVICE_URL'); this.serviceToken = this.generateServiceToken(); } async storeBackupShare(params: { userId: string; accountSequence: number; publicKey: string; encryptedShareData: string; }): Promise { await firstValueFrom( this.httpService.post( `${this.backupServiceUrl}/backup-share/store`, params, { headers: { 'Content-Type': 'application/json', 'X-Service-Token': this.serviceToken, }, }, ), ); } private generateServiceToken(): string { const secret = this.configService.get('SERVICE_JWT_SECRET'); return jwt.sign( { service: 'identity-service', iat: Math.floor(Date.now() / 1000) }, secret, { expiresIn: '1h' }, ); } } ``` --- ## 6. 核心实现 ### 6.1 领域实体 ```typescript // src/domain/entities/backup-share.entity.ts export class BackupShare { private constructor( private readonly _shareId: bigint | null, private readonly _userId: bigint, private readonly _accountSequence: bigint, private readonly _publicKey: string, private readonly _partyIndex: number, private readonly _threshold: number, private readonly _totalParties: number, private _encryptedShareData: string, private _encryptionKeyId: string, private _status: BackupShareStatus, private _accessCount: number, private readonly _createdAt: Date, ) {} static create(params: { userId: bigint; accountSequence: bigint; publicKey: string; encryptedShareData: string; encryptionKeyId: string; }): BackupShare { return new BackupShare( null, params.userId, params.accountSequence, params.publicKey, 2, // Backup = Party 2 2, // threshold 3, // totalParties params.encryptedShareData, params.encryptionKeyId, BackupShareStatus.ACTIVE, 0, new Date(), ); } recordAccess(): void { if (this._status !== BackupShareStatus.ACTIVE) { throw new DomainError('Cannot access revoked share'); } this._accessCount++; } revoke(reason: string): void { if (this._status === BackupShareStatus.REVOKED) { throw new DomainError('Share already revoked'); } this._status = BackupShareStatus.REVOKED; } rotate(newEncryptedData: string, newKeyId: string): void { this._encryptedShareData = newEncryptedData; this._encryptionKeyId = newKeyId; this._status = BackupShareStatus.ACTIVE; } // Getters... get shareId(): bigint | null { return this._shareId; } get userId(): bigint { return this._userId; } get publicKey(): string { return this._publicKey; } get encryptedShareData(): string { return this._encryptedShareData; } get status(): BackupShareStatus { return this._status; } get accessCount(): number { return this._accessCount; } } export enum BackupShareStatus { ACTIVE = 'ACTIVE', REVOKED = 'REVOKED', ROTATED = 'ROTATED', } ``` ### 6.2 应用服务 ```typescript // src/application/services/backup-share-application.service.ts @Injectable() export class BackupShareApplicationService { private readonly logger = new Logger(BackupShareApplicationService.name); constructor( @Inject(BACKUP_SHARE_REPOSITORY) private readonly repository: BackupShareRepository, private readonly encryptionService: AesEncryptionService, private readonly auditService: AuditLogService, ) {} async storeBackupShare(command: StoreBackupShareCommand): Promise { this.logger.log(`Storing backup share for user: ${command.userId}`); // 检查是否已存在 const existing = await this.repository.findByUserId(BigInt(command.userId)); if (existing) { throw new ApplicationError('Backup share already exists for this user'); } // 二次加密 (identity-service 已加密一次,这里再加密一次) const { encrypted, keyId } = await this.encryptionService.encrypt( command.encryptedShareData, ); // 创建实体 const share = BackupShare.create({ userId: BigInt(command.userId), accountSequence: BigInt(command.accountSequence), publicKey: command.publicKey, encryptedShareData: encrypted, encryptionKeyId: keyId, }); // 保存 const saved = await this.repository.save(share); // 记录审计日志 await this.auditService.log({ shareId: saved.shareId!, userId: BigInt(command.userId), action: 'STORE', sourceService: command.sourceService, sourceIp: command.sourceIp, success: true, }); this.logger.log(`Backup share stored: shareId=${saved.shareId}`); return saved.shareId!.toString(); } async retrieveBackupShare(query: GetBackupShareQuery): Promise { this.logger.log(`Retrieving backup share for user: ${query.userId}`); const share = await this.repository.findByUserIdAndPublicKey( BigInt(query.userId), query.publicKey, ); if (!share) { throw new ApplicationError('Backup share not found'); } if (share.status !== BackupShareStatus.ACTIVE) { throw new ApplicationError('Backup share is not active'); } // 记录访问 share.recordAccess(); await this.repository.save(share); // 解密 const decrypted = await this.encryptionService.decrypt( share.encryptedShareData, share.encryptionKeyId, ); // 记录审计日志 await this.auditService.log({ shareId: share.shareId!, userId: BigInt(query.userId), action: 'RETRIEVE', sourceService: query.sourceService, sourceIp: query.sourceIp, success: true, }); return { encryptedShareData: decrypted, partyIndex: share.partyIndex, publicKey: share.publicKey, }; } } ``` --- ## 7. 环境配置 ### 7.1 .env.example ```bash # Database (必须是独立的数据库实例,不能与 identity-service 共享!) DATABASE_URL="postgresql://postgres:password@backup-db-server:5432/rwa_backup?schema=public" # Server APP_PORT=3002 APP_ENV="development" # Service Authentication SERVICE_JWT_SECRET="your-super-secret-service-jwt-key" ALLOWED_SERVICES="identity-service,recovery-service" # Encryption (用于二次加密备份分片) BACKUP_ENCRYPTION_KEY="your-256-bit-encryption-key-in-hex" BACKUP_ENCRYPTION_KEY_ID="key-v1" # Rate Limiting MAX_RETRIEVE_PER_DAY=3 # 每用户每天最多获取 3 次 MAX_STORE_PER_MINUTE=10 # 每分钟最多存储 10 个 # Audit AUDIT_LOG_RETENTION_DAYS=365 # 审计日志保留 365 天 # Monitoring PROMETHEUS_ENABLED=true PROMETHEUS_PORT=9102 ``` --- ## 8. 部署要求 ### 8.1 物理隔离 **强制要求:** ```yaml # docker-compose.yml (仅供参考架构,生产环境必须分开部署) # ❌ 错误示例 - 同一台机器 services: identity-service: ... backup-service: # 不能在这里! ... # ✅ 正确示例 - 分开部署 # 机器 A: docker-compose-main.yml services: identity-service: ... # 机器 B: docker-compose-backup.yml (异地机房) services: backup-service: ... ``` ### 8.2 网络安全 ```yaml # backup-service 的网络策略 - 只允许来自 identity-service 的入站连接 - 禁止任何公网访问 - 使用 VPN 或专线连接主机房和备份机房 ``` ### 8.3 数据库安全 ``` - 使用独立的 PostgreSQL 实例 - 启用 TLS 加密连接 - 定期备份到第三方存储 - 启用审计日志 ``` --- ## 9. 测试 ### 9.1 E2E 测试 ```typescript // test/e2e/backup-share.e2e-spec.ts describe('BackupShare (e2e)', () => { let app: INestApplication; let serviceToken: string; beforeAll(async () => { const moduleFixture = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); // 生成测试用的服务令牌 serviceToken = jwt.sign( { service: 'identity-service' }, process.env.SERVICE_JWT_SECRET, ); }); describe('POST /backup-share/store', () => { it('should store backup share successfully', async () => { const response = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({ userId: '12345', accountSequence: 1001, publicKey: '02' + 'a'.repeat(64), encryptedShareData: 'encrypted-share-data-base64', }) .expect(201); expect(response.body.success).toBe(true); expect(response.body.shareId).toBeDefined(); }); it('should reject duplicate share', async () => { // First store await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({ userId: '99999', accountSequence: 9999, publicKey: '02' + 'b'.repeat(64), encryptedShareData: 'data', }); // Duplicate await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({ userId: '99999', accountSequence: 9999, publicKey: '02' + 'b'.repeat(64), encryptedShareData: 'data', }) .expect(400); }); it('should reject unauthorized service', async () => { await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', 'invalid-token') .send({}) .expect(401); }); }); describe('POST /backup-share/retrieve', () => { it('should retrieve backup share with valid recovery token', async () => { // Setup: store a share first const storeResponse = await request(app.getHttpServer()) .post('/backup-share/store') .set('X-Service-Token', serviceToken) .send({ userId: '77777', accountSequence: 7777, publicKey: '02' + 'c'.repeat(64), encryptedShareData: 'test-encrypted-data', }); // Retrieve const response = await request(app.getHttpServer()) .post('/backup-share/retrieve') .set('X-Service-Token', serviceToken) .send({ userId: '77777', publicKey: '02' + 'c'.repeat(64), recoveryToken: 'valid-recovery-token', }) .expect(200); expect(response.body.encryptedShareData).toBeDefined(); expect(response.body.partyIndex).toBe(2); }); }); }); ``` --- ## 10. 与 identity-service 的集成 ### 10.1 修改 identity-service 在 identity-service 的 `autoCreateAccount` 中添加调用 backup-service: ```typescript // identity-service/src/application/services/user-application.service.ts // 在保存服务端分片后,调用 backup-service 存储备份分片 await this.mpcKeyShareRepository.saveServerShare({...}); // 新增: 调用 backup-service 存储备份分片 if (mpcResult.backupShareData) { try { await this.backupClient.storeBackupShare({ userId: account.userId.toString(), accountSequence: account.accountSequence.value, publicKey: mpcResult.publicKey, encryptedShareData: mpcResult.backupShareData, }); this.logger.log(`Backup share stored to backup-service for user: ${account.userId}`); } catch (error) { this.logger.error(`Failed to store backup share: ${error.message}`); // 根据业务需求决定是否回滚或继续 // 建议: 记录失败,后续通过补偿任务重试 } } ``` ### 10.2 配置更新 在 identity-service 的 `.env` 中添加: ```bash # Backup Service BACKUP_SERVICE_URL="http://backup-server:3002" SERVICE_JWT_SECRET="your-super-secret-service-jwt-key" ``` --- ## 11. 监控和告警 ### 11.1 关键指标 ``` - backup_share_store_total # 存储总数 - backup_share_retrieve_total # 获取总数 - backup_share_revoke_total # 撤销总数 - backup_share_store_latency_ms # 存储延迟 - backup_share_retrieve_latency_ms # 获取延迟 - backup_share_error_total # 错误总数 ``` ### 11.2 告警规则 ```yaml # 异常访问告警 - alert: HighBackupShareRetrieveRate expr: rate(backup_share_retrieve_total[5m]) > 10 for: 5m labels: severity: warning annotations: summary: "Backup share retrieve rate is high" # 服务不可用告警 - alert: BackupServiceDown expr: up{job="backup-service"} == 0 for: 1m labels: severity: critical ``` --- ## 12. 开发步骤 ### 12.1 初始化项目 ```bash # 1. 创建 NestJS 项目 cd backend/services/backup-service npx @nestjs/cli new . --skip-git # 2. 安装依赖 npm install @nestjs/config @prisma/client class-validator class-transformer npm install -D prisma # 3. 初始化 Prisma npx prisma init # 4. 复制 schema 并生成客户端 npx prisma generate npx prisma migrate dev --name init ``` ### 12.2 开发顺序 1. **基础设施层** - Prisma 配置、数据库连接 2. **领域层** - BackupShare 实体、Repository 接口 3. **应用层** - 存储/获取/撤销命令处理 4. **接口层** - REST API Controller 5. **安全层** - ServiceAuthGuard、审计日志 6. **测试** - 单元测试、E2E 测试 --- ## 13. 检查清单 开发完成后,确保以下事项: - [ ] 数据库部署在独立服务器 - [ ] 服务间认证已实现 - [ ] 分片数据已二次加密 - [ ] 审计日志已启用 - [ ] 访问频率限制已实现 - [ ] E2E 测试通过 - [ ] 与 identity-service 集成测试通过 - [ ] 监控指标已配置 - [ ] 告警规则已配置 - [ ] 生产环境部署在异地机房