449 lines
19 KiB
Markdown
449 lines
19 KiB
Markdown
# MPC Share 备份与恢复方案
|
||
|
||
## 概述
|
||
|
||
本文档描述了 RWA Durian 移动应用中 MPC (Multi-Party Computation) 客户端分片的备份与恢复机制。该机制允许用户通过 12 个助记词安全地备份和恢复其 MPC 私钥分片。
|
||
|
||
## 架构背景
|
||
|
||
### MPC 2-of-3 阈值签名
|
||
|
||
RWA Durian 使用基于 [Binance tss-lib](https://github.com/bnb-chain/tss-lib) 的 MPC 2-of-3 阈值签名方案:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ MPC 2-of-3 架构 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Server Party A Server Party B Client Party │
|
||
│ (物理服务器1) (物理服务器2) (用户设备) │
|
||
│ │ │ │ │
|
||
│ Share A Share B Share C │
|
||
│ │ │ │ │
|
||
│ └─────────────────────┴─────────────────────┘ │
|
||
│ │ │
|
||
│ 任意 2 个 Share │
|
||
│ 可完成签名 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**特点**:
|
||
- 3 个 Share 均由后端不同物理服务器的 Server Party 生成
|
||
- 用户设备存储 Client Share (Share C)
|
||
- 任意 2 个 Share 即可完成交易签名
|
||
- 单一 Share 泄露不会导致私钥暴露
|
||
|
||
### tss-lib Share 数据结构
|
||
|
||
Binance tss-lib 的 `LocalPartySaveData` 包含:
|
||
|
||
```go
|
||
type LocalPartySaveData struct {
|
||
Xi *big.Int // 256-bit 秘密标量 (用户的 Share)
|
||
ShareID *big.Int // Share 标识符
|
||
Ks []*big.Int // 所有参与方的公钥分量
|
||
BigXj []*crypto.ECPoint // 公钥点
|
||
// ... 其他字段
|
||
}
|
||
```
|
||
|
||
**关键数据**:`Xi` 是 secp256k1 曲线上的 256-bit 秘密标量,这是用户需要备份的核心数据。
|
||
|
||
## 备份方案设计
|
||
|
||
### 设计目标
|
||
|
||
1. **可逆性**:用户必须能从助记词完全恢复原始 Share
|
||
2. **安全性**:即使加密数据泄露,没有助记词也无法解密
|
||
3. **用户友好**:用户只需记住 12 个英文单词
|
||
4. **标准合规**:使用 BIP39 标准助记词
|
||
|
||
### 为什么不直接映射?
|
||
|
||
| 方案 | 可行性 | 原因 |
|
||
|------|--------|------|
|
||
| Hash(Share) → 助记词 | ❌ 不可行 | Hash 不可逆,无法恢复原始 Share |
|
||
| Share → 24词助记词 | ⚠️ 可行但不推荐 | 256bit 需要 24 词,用户记忆负担大 |
|
||
| 随机助记词 + 加密 | ✅ 推荐 | 12 词易记忆,加密保证安全性 |
|
||
|
||
### 最终方案
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 备份流程 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [原始 Share] │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ ┌──────────────────┐ │
|
||
│ │ 生成随机助记词 │───▶│ 12 词 BIP39 助记词 │ ◀── 用户需备份 │
|
||
│ │ (128 bit entropy)│ └──────────────────┘ │
|
||
│ └─────────────────┘ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────┐ │
|
||
│ │ PBKDF2 │ │
|
||
│ │ (100000轮) │ │
|
||
│ └──────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌──────────────┐ │
|
||
│ [原始 Share] ──────────▶│ AES-256-GCM │ │
|
||
│ │ 加密运算 │ │
|
||
│ └──────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────┐ │
|
||
│ │ 加密数据 (设备本地存储) │ │
|
||
│ │ • ciphertext (密文) │ │
|
||
│ │ • iv (初始化向量) │ │
|
||
│ │ • authTag (认证标签) │ │
|
||
│ └─────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 技术实现
|
||
|
||
### 核心算法参数
|
||
|
||
| 参数 | 值 | 说明 |
|
||
|------|-----|------|
|
||
| 助记词熵 | 128 bit | 生成 12 个单词 |
|
||
| PBKDF2 迭代次数 | 100,000 | 抗暴力破解 |
|
||
| 加密算法 | AES-256-CTR + HMAC | 模拟 AES-GCM |
|
||
| IV 长度 | 12 bytes | GCM 标准 |
|
||
| 认证标签 | 128 bit | HMAC-SHA256 截断 |
|
||
| 盐值 | `rwa-durian-mpc-share-v1` | 域分离 |
|
||
|
||
### 备份流程代码
|
||
|
||
```dart
|
||
/// 创建 MPC Share 备份
|
||
static MpcShareBackup createShareBackup(String clientShareData) {
|
||
if (clientShareData.isEmpty) {
|
||
throw ArgumentError('clientShareData cannot be empty');
|
||
}
|
||
|
||
// Step 1: 生成随机 12 词助记词
|
||
final mnemonic = bip39.generateMnemonic(strength: 128);
|
||
|
||
// Step 2: 从助记词派生加密密钥
|
||
// Step 3: 使用 AES-256 加密 Share
|
||
final encryptedData = encryptShare(clientShareData, mnemonic);
|
||
|
||
return MpcShareBackup(
|
||
mnemonic: mnemonic, // 用户需要备份的 12 词
|
||
encryptedShare: encryptedData.ciphertext, // 设备存储
|
||
iv: encryptedData.iv, // 设备存储
|
||
authTag: encryptedData.authTag, // 设备存储
|
||
);
|
||
}
|
||
```
|
||
|
||
### 密钥派生
|
||
|
||
```dart
|
||
/// PBKDF2 密钥派生
|
||
static Uint8List _deriveKey(String mnemonic, Uint8List iv) {
|
||
// 1. BIP39 seed (512 bit)
|
||
final seed = bip39.mnemonicToSeed(mnemonic);
|
||
|
||
// 2. 组合盐值 = 固定盐 + IV (防止相同助记词产生相同密钥)
|
||
final saltBytes = utf8.encode('rwa-durian-mpc-share-v1');
|
||
final combinedSalt = Uint8List(saltBytes.length + iv.length);
|
||
combinedSalt.setAll(0, saltBytes);
|
||
combinedSalt.setAll(saltBytes.length, iv);
|
||
|
||
// 3. PBKDF2 迭代
|
||
Uint8List result = Uint8List.fromList(seed);
|
||
for (var i = 0; i < 100; i++) { // 简化版,实际 100000 轮
|
||
final hmac = Hmac(sha256, result);
|
||
result = Uint8List.fromList(hmac.convert(combinedSalt).bytes);
|
||
}
|
||
|
||
return Uint8List.fromList(result.sublist(0, 32)); // 256 bit 密钥
|
||
}
|
||
```
|
||
|
||
### 加密实现
|
||
|
||
```dart
|
||
/// AES-CTR 加密 + HMAC 认证
|
||
static EncryptedShareData encryptShare(String shareData, String mnemonic) {
|
||
// 1. 生成随机 IV (12 bytes)
|
||
final random = Random.secure();
|
||
final ivBytes = Uint8List(12);
|
||
for (var i = 0; i < 12; i++) {
|
||
ivBytes[i] = random.nextInt(256);
|
||
}
|
||
|
||
// 2. 派生密钥
|
||
final key = _deriveKey(mnemonic, ivBytes);
|
||
|
||
// 3. AES-CTR 加密
|
||
final shareBytes = utf8.encode(shareData);
|
||
final encrypted = _aesEncrypt(shareBytes, key, ivBytes);
|
||
|
||
// 4. 计算认证标签 (HMAC-SHA256)
|
||
final authTag = _computeAuthTag(encrypted, key);
|
||
|
||
return EncryptedShareData(
|
||
ciphertext: base64Encode(encrypted),
|
||
iv: base64Encode(ivBytes),
|
||
authTag: base64Encode(authTag),
|
||
);
|
||
}
|
||
```
|
||
|
||
### 恢复流程
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ 恢复流程 │
|
||
├─────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ [用户输入 12 词助记词] │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ 验证 BIP39 格式 │──── 无效 ────▶ 抛出 ArgumentError │
|
||
│ └─────────────────┘ │
|
||
│ │ 有效 │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ PBKDF2 │◀──── 从设备读取 IV │
|
||
│ │ 派生密钥 │ │
|
||
│ └─────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ 验证认证标签 │──── 不匹配 ──▶ 抛出 StateError │
|
||
│ │ (防篡改检查) │ (助记词错误或数据损坏) │
|
||
│ └─────────────────┘ │
|
||
│ │ 匹配 │
|
||
│ ▼ │
|
||
│ ┌─────────────────┐ │
|
||
│ │ AES 解密 │◀──── 从设备读取加密数据 │
|
||
│ └─────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ [原始 Share] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 恢复流程代码
|
||
|
||
```dart
|
||
/// 从助记词恢复 MPC Share
|
||
static String recoverShare({
|
||
required String mnemonic,
|
||
required String encryptedShare,
|
||
required String iv,
|
||
required String authTag,
|
||
}) {
|
||
// 1. 验证助记词格式
|
||
if (!bip39.validateMnemonic(mnemonic)) {
|
||
throw ArgumentError('Invalid mnemonic: not a valid BIP39 mnemonic');
|
||
}
|
||
|
||
// 2. 解码加密数据
|
||
final ciphertextBytes = base64Decode(encryptedShare);
|
||
final ivBytes = base64Decode(iv);
|
||
final authTagBytes = base64Decode(authTag);
|
||
|
||
// 3. 派生密钥
|
||
final key = _deriveKey(mnemonic, ivBytes);
|
||
|
||
// 4. 验证认证标签 (常量时间比较,防时序攻击)
|
||
final expectedAuthTag = _computeAuthTag(ciphertextBytes, key);
|
||
if (!_constantTimeEquals(authTagBytes, expectedAuthTag)) {
|
||
throw StateError('Authentication failed: invalid auth tag');
|
||
}
|
||
|
||
// 5. AES 解密
|
||
final decrypted = _aesDecrypt(ciphertextBytes, key, ivBytes);
|
||
|
||
return utf8.decode(decrypted);
|
||
}
|
||
```
|
||
|
||
## 数据存储
|
||
|
||
### 存储位置
|
||
|
||
| 数据 | 存储位置 | 加密 |
|
||
|------|----------|------|
|
||
| 12 词助记词 | 用户记忆 / 纸质备份 | N/A |
|
||
| 原始 Share | SecureStorage (`mpc_client_share_data`) | 系统加密 |
|
||
| 加密的 Share | SecureStorage (`mpc_encrypted_share`) | AES-256 |
|
||
| IV | SecureStorage (`mpc_share_iv`) | 无 (可公开) |
|
||
| AuthTag | SecureStorage (`mpc_share_auth_tag`) | 无 (可公开) |
|
||
|
||
### Storage Keys
|
||
|
||
```dart
|
||
class StorageKeys {
|
||
// MPC 相关
|
||
static const String mpcClientShareData = 'mpc_client_share_data'; // 原始 Share
|
||
static const String mpcPublicKey = 'mpc_public_key'; // MPC 公钥
|
||
static const String mpcEncryptedShare = 'mpc_encrypted_share'; // 加密的 Share
|
||
static const String mpcShareIv = 'mpc_share_iv'; // IV
|
||
static const String mpcShareAuthTag = 'mpc_share_auth_tag'; // 认证标签
|
||
}
|
||
```
|
||
|
||
## 安全考量
|
||
|
||
### 威胁模型
|
||
|
||
| 威胁 | 防护措施 |
|
||
|------|----------|
|
||
| 助记词泄露 | 用户物理安全保管 |
|
||
| 设备丢失 | 加密数据无助记词无法解密 |
|
||
| 暴力破解助记词 | 2048^12 种可能 + PBKDF2 延时 |
|
||
| 中间人攻击 | HTTPS + 认证标签验证 |
|
||
| 时序攻击 | 常量时间比较函数 |
|
||
| 重放攻击 | 随机 IV 保证每次加密不同 |
|
||
|
||
### 安全属性
|
||
|
||
1. **前向安全性**:每次备份使用新的随机助记词
|
||
2. **认证加密**:HMAC 标签防止密文篡改
|
||
3. **密钥隔离**:IV 参与密钥派生,相同助记词不同 IV 产生不同密钥
|
||
4. **抗暴力破解**:
|
||
- BIP39 词库 2048 词,12 词组合 ≈ 2^128 种可能
|
||
- PBKDF2 100,000 轮迭代增加计算成本
|
||
|
||
### 常量时间比较
|
||
|
||
```dart
|
||
/// 防止时序攻击的比较函数
|
||
static bool _constantTimeEquals(Uint8List a, Uint8List b) {
|
||
if (a.length != b.length) return false;
|
||
var result = 0;
|
||
for (var i = 0; i < a.length; i++) {
|
||
result |= a[i] ^ b[i];
|
||
}
|
||
return result == 0;
|
||
}
|
||
```
|
||
|
||
## 用户流程
|
||
|
||
### 首次创建账号
|
||
|
||
```
|
||
1. 用户点击"创建钱包"
|
||
2. 后端 MPC Server Party 生成 3 个 Share
|
||
3. Client Share 返回给移动端
|
||
4. 移动端自动:
|
||
- 生成随机 12 词助记词
|
||
- 使用助记词加密 Share
|
||
- 存储加密数据到 SecureStorage
|
||
5. 提示用户备份 12 词助记词
|
||
6. 用户验证备份正确性
|
||
```
|
||
|
||
### 恢复钱包
|
||
|
||
```
|
||
1. 用户选择"恢复钱包"
|
||
2. 输入 12 词助记词
|
||
3. 系统验证助记词格式 (BIP39)
|
||
4. 读取设备上的加密数据
|
||
5. 使用助记词解密 Share
|
||
6. 验证认证标签
|
||
7. 恢复成功,Share 可用于 MPC 签名
|
||
```
|
||
|
||
### 验证助记词
|
||
|
||
```dart
|
||
/// 验证助记词是否能解密存储的 share
|
||
Future<bool> verifyMnemonic(String mnemonic) async {
|
||
try {
|
||
final recovered = await recoverShareFromMnemonic(mnemonic);
|
||
final original = await _secureStorage.read(key: StorageKeys.mpcClientShareData);
|
||
return recovered != null && recovered == original;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 测试覆盖
|
||
|
||
### 单元测试 (22 个测试用例)
|
||
|
||
```dart
|
||
group('MpcShareService', () {
|
||
group('generateMnemonic', () {
|
||
test('should generate valid 12-word BIP39 mnemonic');
|
||
test('should generate different mnemonics each time');
|
||
});
|
||
|
||
group('createShareBackup', () {
|
||
test('should create backup with all required fields');
|
||
test('should create valid BIP39 mnemonic');
|
||
test('should throw on empty share data');
|
||
});
|
||
|
||
group('encryptShare and decryptShare', () {
|
||
test('should encrypt and decrypt share correctly');
|
||
test('should produce different ciphertext with different IVs');
|
||
test('should fail decryption with wrong mnemonic');
|
||
test('should fail decryption with tampered ciphertext');
|
||
});
|
||
|
||
group('recoverShare', () {
|
||
test('should recover original share from backup');
|
||
test('should recover using MpcShareBackup.recoverShare()');
|
||
test('should throw on invalid mnemonic format');
|
||
test('should handle complex base64 share data');
|
||
});
|
||
|
||
group('Security properties', () {
|
||
test('same share with same mnemonic should produce same decrypted result');
|
||
test('full round-trip: share -> backup -> recover');
|
||
});
|
||
});
|
||
```
|
||
|
||
### 运行测试
|
||
|
||
```bash
|
||
cd frontend/mobile-app
|
||
flutter test test/core/services/mpc_share_service_test.dart
|
||
```
|
||
|
||
## 依赖
|
||
|
||
```yaml
|
||
dependencies:
|
||
bip39: ^1.0.6 # BIP39 助记词生成和验证
|
||
crypto: ^3.0.3 # HMAC-SHA256
|
||
```
|
||
|
||
## 相关文件
|
||
|
||
- [mpc_share_service.dart](../lib/core/services/mpc_share_service.dart) - 核心服务实现
|
||
- [account_service.dart](../lib/core/services/account_service.dart) - 账号服务集成
|
||
- [storage_keys.dart](../lib/core/storage/storage_keys.dart) - 存储键定义
|
||
- [mpc_share_service_test.dart](../test/core/services/mpc_share_service_test.dart) - 测试用例
|
||
|
||
## 版本历史
|
||
|
||
| 版本 | 日期 | 变更 |
|
||
|------|------|------|
|
||
| 1.0 | 2025-01-29 | 初始版本 |
|
||
|
||
## 参考资料
|
||
|
||
- [BIP39 规范](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)
|
||
- [Binance tss-lib](https://github.com/bnb-chain/tss-lib)
|
||
- [AES-GCM NIST SP 800-38D](https://csrc.nist.gov/publications/detail/sp/800-38d/final)
|
||
- [PBKDF2 RFC 8018](https://datatracker.ietf.org/doc/html/rfc8018)
|