rwadurian/backend/services/backup-service/IMPLEMENTATION_GUIDE.md

902 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string>('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<void> {
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<string>('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<string> {
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<BackupShareDto> {
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 集成测试通过
- [ ] 监控指标已配置
- [ ] 告警规则已配置
- [ ] 生产环境部署在异地机房