你有没有用过 WhatsApp、Signal 或 Telegram 这样的即时通讯软件?它们提供的一项关键安全功能是端到端加密 (E2EE) 。这确保了消息直接在发送者和接收者之间加密,使得服务器或任何第三方都无法访问消息内容。
理解端到端加密
端到端加密 (End-to-End Encryption, E2EE) 是一种密码学方法,它保证只有通信的参与者才能解密消息。在 E2EE 系统中,消息在发送者的设备上加密,并且仅在接收者的设备上解密,任何中间服务器或第三方都无法访问明文内容。
端到端加密的应用范围
E2EE 可以应用于各种形式的通信,包括:
虽然根据数据格式的不同,具体的加密过程可能会有所不同,但其基本原理保持一致。
E2EE 的性能考量
实现 E2EE 会由于加密和解密过程所需的时间而导致消息延迟略有增加。为了增强安全性,这种额外的开销通常是可以接受的。
使用 Signal 协议的端到端加密流程
本节概述了使用广泛认可的 Signal 协议 实现 E2EE 的过程。
参与者:
Alice: 消息发送者。
Bob: 消息接收者。
流程概览:
密钥生成: Alice 和 Bob 各自独立地生成自己的身份密钥、预共享密钥和签名预共享密钥。
密钥交换: Alice 获取 Bob 的身份密钥和预共享密钥,以及一个他的一次性预共享密钥。。
会话建立: Alice 使用 Bob 的公钥信息与他建立安全会话。
消息加密: Alice 使用与 Bob 建立的会话加密她的消息。
消息传输: Alice 将加密的消息发送给 Bob。
消息解密: 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; } } 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);final identityKeyPair = generateIdentityKeyPair();final registrationId = generateRegistrationId(false );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' ;Future<Map <String , String >> getKeyBundleFromServer(String bobId) async { ... } const String remoteUserId = 'bob_user_id' ; final selfId = 'userId' ;final aliceStore = _getStore(selfId);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 );final preKey = bundleData['preKey' ]!;final preKeyRecord = PreKeyRecord.fromBuffer(base64Decode(preKey));final preKeyId = preKeyRecord.preKeyId;final preKeyPublic = preKeyRecord.getKeyPair().publicKey;final signedPreKey = SignedPreKeyRecord.fromSerialized( base64Decode(bundleData['signedPreKey' ]!), ); final signedPreKeyId = signedPreKey.id;final signedPreKeyPublic = signedPreKey.getKeyPair().publicKey;final signedPreKeySignature = signedPreKey.signature;final identityKeyPublic = IdentityKey.fromBytes(base64Decode(bundleData['identityKey' ]!), 0 ); final bundle = PreKeyBundle( int .parse(bundleData['registrationId' ]!), 1 , preKeyId, preKeyPublic, signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKeyPublic, ); final sessionBuilder = SessionBuilder.fromSignalStore( aliceStore, remoteAddress, ); 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' ; 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)), ); final encryptedMessage = base64Encode(ciphertext.serialize());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' ; final selfId = 'userId' ;final bobStore = _getStore(selfId);class ReceivedMessage { final String encryptedMessage; final int type; ReceivedMessage(this .encryptedMessage, this .type); } final message = ReceivedMessage( 'AgAAAAEAAAAAAAD...' , 1 , ); final encryptedMessage = message.encryptedMessage;final type = message.type;final remoteAddress = SignalProtocolAddress(aliceUserId, 1 );final sessionCipher = SessionCipher.fromStore( bobStore, remoteAddress, ); try { if (type == 3 ) { final preKeySignalMessage = PreKeySignalMessage.fromBuffer(base64Decode(encryptedMessage)); final decryptedMessage = await sessionCipher.decrypt(preKeySignalMessage); print ("解密后的消息 (PreKey): ${utf8.decode(decryptedMessage)} " ); } else if (type == 2 ) { 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 库提供了实现此协议的完整工具集。本文中的代码示例展示了基本实现,但在实际应用中,您需要根据自身的用户认证系统、服务器架构和安全策略进行适当集成。正确实施后,您的聊天应用将能够提供与主流安全通讯应用相当的加密保护水平。
参考
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !