为聊天应用添加端到端加密 (E2EE)

Posted by Loscoy on 2025-03-20
Estimated Reading Time 12 Minutes
Words 2.8k In Total
Viewed Times

你有没有用过 WhatsApp、Signal 或 Telegram 这样的即时通讯软件?它们提供的一项关键安全功能是端到端加密 (E2EE)。这确保了消息直接在发送者和接收者之间加密,使得服务器或任何第三方都无法访问消息内容。

理解端到端加密

端到端加密 (End-to-End Encryption, E2EE) 是一种密码学方法,它保证只有通信的参与者才能解密消息。在 E2EE 系统中,消息在发送者的设备上加密,并且仅在接收者的设备上解密,任何中间服务器或第三方都无法访问明文内容。

端到端加密的应用范围

E2EE 可以应用于各种形式的通信,包括:

  • 文本消息
  • 图片
  • 语音消息
  • 视频消息

虽然根据数据格式的不同,具体的加密过程可能会有所不同,但其基本原理保持一致。

E2EE 的性能考量

实现 E2EE 会由于加密和解密过程所需的时间而导致消息延迟略有增加。为了增强安全性,这种额外的开销通常是可以接受的。

使用 Signal 协议的端到端加密流程

本节概述了使用广泛认可的 Signal 协议实现 E2EE 的过程。

参与者:

  1. Alice: 消息发送者。
  2. Bob: 消息接收者。

流程概览:

  1. 密钥生成: Alice 和 Bob 各自独立地生成自己的身份密钥、预共享密钥和签名预共享密钥。
  2. 密钥交换: Alice 获取 Bob 的身份密钥和预共享密钥,以及一个他的一次性预共享密钥。。
  3. 会话建立: Alice 使用 Bob 的公钥信息与他建立安全会话。
  4. 消息加密: Alice 使用与 Bob 建立的会话加密她的消息。
  5. 消息传输: Alice 将加密的消息发送给 Bob。
  6. 消息解密: Bob 使用他的私钥信息解密收到的消息。

依赖

1
2
3
4
dependencies:
libsignal_protocol_dart: ^0.7.1
hive_flutter: ^1.1.0
get_it: ^8.0.2

详细步骤和代码示例 (Dart 实现)

此实现使用 libsignal_protocol_dart 库和 hive 进行本地密钥存储。

1. 密钥生成

Alice 和 Bob 都需要生成以下密钥:

  • 身份密钥 (Identity Key): 通常在用户注册时生成,并且在整个账户生命周期内保持不变,用于验证用户身份和签名其他密钥。
  • 签名预密钥 (Signed Pre-key): 由身份密钥签名,以证明其合法性,用于建立初始加密会话,需要定期更新。
  • 预密钥 (Pre-key): 一次性使用的密钥对,使用一次后即丢弃,每个会话使用一个唯一的预密钥,用完即销毁,确保完美前向保密性,服务器通常会存储多个预密钥。服务器储存的预密钥不足时,客户端需要再次生成。

2. 实现本地密钥存储库 (使用 Hive)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import 'package:hive/hive.dart';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';
import 'package:get_it/get_it.dart';

abstract class IHiveService {
Future<Box> openBox(String boxName);
}

class HiveSignalProtocolStore implements SignalProtocolStore {
final Box<dynamic> box;
final String userId

HiveSignalProtocolStore(this.box, this.userId);

// 初始化方法
Future<void> initialize(
IdentityKeyPair identityKeyPair, int registrationId) async {
await box.put('identityKeyPair', identityKeyPair.serialize());
await box.put('registrationId', registrationId);
}

@override
Future<PreKeyRecord> loadPreKey(int preKeyId) async {
final data = box.get('preKey_$preKeyId');
return data != null ? PreKeyRecord.fromBuffer(data) : null;
}

@override
Future<void> storePreKey(int preKeyId, PreKeyRecord preKeyRecord) async {
await box.put('preKey_$preKeyId', preKeyRecord.serialize());
}

@override
Future<void> removePreKey(int preKeyId) async {
await box.delete('preKey_$preKeyId');
}

@override
Future<void> storeSignedPreKey(
int signedPreKeyId,
SignedPreKeyRecord record,
) async {
await box.put('signedPreKey_$signedPreKeyId', record.serialize());
}

@override
Future<SignedPreKeyRecord> loadSignedPreKey(int signedPreKeyId) async {
final data = box.get('signedPreKey_$signedPreKeyId');
return data != null ? SignedPreKeyRecord.fromBuffer(data) : null;
}

@override
Future<void> removeSignedPreKey(int signedPreKeyId) async {
await box.delete('signedPreKey_$signedPreKeyId');
}

@override
Future<SessionRecord> loadSession(SignalProtocolAddress address) async {
final data = box.get('session_${address.getName()}_${address.getDeviceId()}');
return data != null ? SessionRecord.fromBuffer(data) : SessionRecord();
}

@override
Future<List<int>> getSubDeviceSessions(String name) async {
final keys = box.keys.whereType<String>().where((key) => key.startsWith('session_$name')).toList();
return keys.map((key) => int.parse(key.split('_').last)).toList();
}

@override
Future<void> storeSession(SignalProtocolAddress address, SessionRecord record) async {
await box.put('session_${address.getName()}_${address.getDeviceId()}', record.serialize());
}

@override
Future<void> deleteSession(SignalProtocolAddress address) async {
await box.delete('session_${address.getName()}_${address.getDeviceId()}');
}

@override
Future<void> deleteAllSessions(String name) async {
final keysToRemove = box.keys.whereType<String>().where((key) => key.startsWith('session_$name')).toList();
await box.deleteAll(keysToRemove);
}

@override
Future<IdentityKeyPair> getIdentityKeyPair() async {
final data = box.get('identityKeyPair');
return data != null ? IdentityKeyPair.fromBuffer(data) : null;
}

@override
Future<int> getLocalRegistrationId() async {
return box.get('registrationId') as int?;
}

@override
Future<int> getPreKeyCount() async {
return box.keys.whereType<String>().where((key) => key.startsWith('preKey_')).length;
}

@override
Future<int> getSignedPreKeyCount() async {
return box.keys.whereType<String>().where((key) => key.startsWith('signedPreKey_')).length;
}
}

// 获取 Hive 存储的函数
Future<HiveSignalProtocolStore> _getStore(String userId) async {
final store = _stores[userId];
if (store != null) {
return store;
}

final box = await getIt<IHiveService>().openBox('signal_store_$userId');
final newStore = HiveSignalProtocolStore(box, userId);
_stores[userId] = newStore;
return newStore;
}

3. 生成密钥包(alice和Bob各自生成)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import 'dart:convert';
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

// 将密钥上传到服务器的函数
Future<void> uploadKeysToServer(IdentityKeyPair identityKeyPair, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> preKeys, int registrationId) async { ... }

final selfId = 'userId';

final store = _getStore(selfId);

// 生成新的身份密钥对和注册 ID
final identityKeyPair = generateIdentityKeyPair();
final registrationId = generateRegistrationId(false);

// 使用身份密钥对和注册 ID 初始化存储
await store.initialize(identityKeyPair, registrationId);

// 生成预共享密钥
// 此处仅为示例,实际应用中需要根据实际情况生成
final preKeys = generatePreKeys(0, 100);
// 存储预共享密钥
for (final preKey in preKeys) {
await store.storePreKey(preKey.id, preKey);
}

// 生成签名预共享密钥
final signedPreKey = generateSignedPreKey(identityKeyPair, 0);
// 存储签名预共享密钥
await store.storeSignedPreKey(signedPreKey.id, signedPreKey);

// 将密钥上传到服务器
await uploadKeysToServer(identityKeyPair, signedPreKey, preKeys, registrationId);

4. 建立会话

Alice 使用从服务器获得的 Bob 的身份密钥和预密钥来建立与 Bob 的会话。如果服务器中Bob的预密钥不足,服务器将不会返回PreKey,Alice需要计算以下 Diffie-Hellman (DH) 值(参考:Signal 协议文档 - 发送初始消息):

1
2
3
DH1 = DH(IK_A, SPK_B)
DH2 = DH(EK_A, IK_B)
DH3 = DH(EK_A, SPK_B)

然后,使用密钥派生函数 (KDF) 将这些 DH 值组合起来,生成共享密钥 (SK):

1
SK = KDF(DH1 || DH2 || DH3)

代码示例 (Alice 端):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import 'dart:convert';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

// 从服务器获取 Bob 的密钥包的函数
Future<Map<String, String>> getKeyBundleFromServer(String bobId) async { ... }

const String remoteUserId = 'bob_user_id'; // 替换为 Bob 的实际用户 ID

final selfId = 'userId';
final aliceStore = _getStore(selfId);

// 从服务器获取 Bob 的身份密钥和预共享密钥包
// final bundleData = await getKeyBundleFromServer(remoteUserId);
final bundleData = { // 用于演示的模拟数据
'registrationId': '12345',
'preKey': base64Encode(PreKeyRecord(0, KeyPair.generate()).serialize()),
'signedPreKey': base64Encode(SignedPreKeyRecord(0, KeyPair.generate(), Uint8List.fromList(utf8.encode('signature'))).serialize()),
'identityKey': base64Encode(KeyPair.generate().publicKey.serialize()),
};

final remoteAddress = SignalProtocolAddress(remoteUserId, 1);

// Bob 的预共享密钥
final preKey = bundleData['preKey']!;
final preKeyRecord = PreKeyRecord.fromBuffer(base64Decode(preKey));
final preKeyId = preKeyRecord.preKeyId;
final preKeyPublic = preKeyRecord.getKeyPair().publicKey;

// Bob 的签名预共享密钥
final signedPreKey = SignedPreKeyRecord.fromSerialized(
base64Decode(bundleData['signedPreKey']!),
);
final signedPreKeyId = signedPreKey.id;
final signedPreKeyPublic = signedPreKey.getKeyPair().publicKey;
final signedPreKeySignature = signedPreKey.signature;

// Bob 的身份密钥
final identityKeyPublic =
IdentityKey.fromBytes(base64Decode(bundleData['identityKey']!), 0);

// 构建 PreKeyBundle
final bundle = PreKeyBundle(
int.parse(bundleData['registrationId']!),
1,
preKeyId,
preKeyPublic,
signedPreKeyId,
signedPreKeyPublic,
signedPreKeySignature,
identityKeyPublic,
);

final sessionBuilder = SessionBuilder.fromSignalStore(
aliceStore,
remoteAddress,
);
// 使用 PreKeyBundle 建立会话
// 此会话将存储在 Alice 的本地数据库中
await sessionBuilder.processPreKeyBundle(bundle);

5. 消息加密

Alice 使用与 Bob 建立的会话来加密消息。

代码示例 (Alice 端):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import 'dart:convert';
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';

const String remoteUserId = 'bob_user_id'; // 替换为 Bob 的实际用户 ID
const String messageToSend = '你好 Bob!';

final selfId = 'userId';
final aliceStore = _getStore(selfId);

// 获取远程地址
final remoteAddress = SignalProtocolAddress(remoteUserId, 1);
// 获取会话加密器
final sessionCipher = SessionCipher.fromStore(
aliceStore,
remoteAddress,
);

// 加密消息
final ciphertext = await sessionCipher.encrypt(
Uint8List.fromList(utf8.encode(messageToSend)),
);

// 加密后的消息 (base64 编码,方便传输)
final encryptedMessage = base64Encode(ciphertext.serialize());

// 消息类型 (SignalMessage 或 PreKeySignalMessage)
final type = ciphertext.getType();


// 发送消息
await sendMessage(encryptedMessage, type);
print('加密后的消息: $encryptedMessage');
print('消息类型: $type');

6. 消息解密

Bob 使用他的私钥信息来解密从 Alice 收到的消息。

代码示例 (Bob 端):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import 'dart:convert';
import 'dart:typed_data';
import 'package:libsignal_protocol_dart/libsignal_protocol_dart.dart';


const String aliceUserId = 'alice_user_id'; // 替换为 Alice 的实际用户 ID

final selfId = 'userId';
final bobStore = _getStore(selfId);

class ReceivedMessage {
final String encryptedMessage;
final int type;

ReceivedMessage(this.encryptedMessage, this.type);
}

// 模拟接收来自 Alice 的消息
final message = ReceivedMessage(
'AgAAAAEAAAAAAAD...', // 替换为实际的加密消息
1, // 替换为实际的消息类型 (PreKeySignalMessage 或 SignalMessage)
);

final encryptedMessage = message.encryptedMessage;
final type = message.type;

// 获取远程地址
final remoteAddress = SignalProtocolAddress(aliceUserId, 1);
// 获取会话加密器
final sessionCipher = SessionCipher.fromStore(
bobStore,
remoteAddress,
);

// 解密消息
try {
if (type == 3) { // PreKeySignalMessage
final preKeySignalMessage = PreKeySignalMessage.fromBuffer(base64Decode(encryptedMessage));
final decryptedMessage = await sessionCipher.decrypt(preKeySignalMessage);
print("解密后的消息 (PreKey): ${utf8.decode(decryptedMessage)}");
} else if (type == 2) { // SignalMessage
final signalMessage = SignalMessage.fromSerialized(base64Decode(encryptedMessage));
final decryptedMessage = await sessionCipher.decryptFromSignal(signalMessage);
print("解密后的消息 (Signal): ${utf8.decode(decryptedMessage)}");
} else {
print("未知消息类型: $type");
}
} catch (e) {
print("解密错误: $e");
}

前向保密

Signal 协议通过使用一次性预共享密钥来实现前向保密。每次 Alice 与 Bob 建立会话时,都会使用 Bob 的一个新的未使用的一次性预共享密钥。这意味着,即使攻击者获得了 Bob 的长期私钥,他们也无法解密之前使用过的一次性预共享密钥加密的消息。

双棘轮算法

双棘轮算法,用于两个参与者之间基于共享秘密密钥交换加密消息。双方每次交换消息时都会生成新的密钥,以确保早期密钥不能从后期密钥推导出来。双方在消息中发送 Diffie-Hellman 公钥,Diffie-Hellman 计算结果也会混合到派生密钥中,以确保后期密钥不能从早期密钥推导出来。这些特性为早期或后期加密消息提供了一定的保护,以防一方密钥泄露。这些属性可在一方密钥泄露的情况下为较早或较晚的加密消息提供一定的保护。

总结

Signal 协议是一种先进的端到端加密解决方案,它通过巧妙地结合多种密钥类型(身份密钥、预共享密钥、签名预共享密钥和一次性预共享密钥)来确保通信安全。在 Dart 和 Flutter 应用中,libsignal_protocol_dart 库提供了实现此协议的完整工具集。本文中的代码示例展示了基本实现,但在实际应用中,您需要根据自身的用户认证系统、服务器架构和安全策略进行适当集成。正确实施后,您的聊天应用将能够提供与主流安全通讯应用相当的加密保护水平。

参考


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !