前言
每次我们使用微信,有时候会想自己发的消息会不会被其他人监听到。特别是给自己的女朋友,发私密消息。还有就是微信管理员,会不会看你的私密消息。如果管理员想看你的,那么你也没办法。那有没一种方法,就是微信管理员也看不到你的私密消息内容。
上面说的管理员,实际就是服务端。而我们使用IM的人就是客户端,也就是要实现一个只有两个客户端能看到消息内容,即使是服务端也看不到。那么就更能保证用户的消息的隐私和信息安全。
上面说的要实现这个功能就是端到端加密,我们一起看看IM如何实现端到端加密。
端到端加密(End-to-End Encryption, E2EE)是一种安全通信的方式,其中只有通信的两个端点(通常是两个终端设备,例如用户的手机或计算机)能够理解或解密消息,而在通信路径的中间节点,包括服务提供者,都无法直接访问或理解消息内容。(Whatsapp支持)
实现思路
我们知道将消息加密,那么客户端A发送到服务端,服务端再转发到客户端B链路都是加密发送。那么不就是都是加密传输了,如果服务端也不知道密钥。那么服务端也就是看不到消息,解密后内容了。消息内容用对称加密AES,进行加解密。
这个前提,一个方案那么密钥发送线下传递。比如小明和小红,线下面对面。大家一起协商下密钥多少。大家都用这个密钥加解密,那么就可以愉快的进行私密聊天。
那万一,小明和小红是网友呢。也就是说彼此不能见面,那么怎么传递密钥。
上面的AES加密过程是需要将密钥加密后发送给对方,也就是一方需要加密,而另一方只需要解密。发明一种密码加密用加密密钥,解密用解密密钥。发送者只需要加密密钥(公钥),解密者只需要解密密钥(私钥)。那么我将公钥发给对方,让他用AES密钥用公钥密码的公钥加密后发给我,而我再用私钥解密。
我们这边需要使用到是非对称加密。
非对称加密解决密钥配送问题(key distribution problem,密钥分发问题)。
这边涉及到两个密码学知识对称密码AES和非对称加密RSA,如果大家对这两个密码不是很熟悉。可以参考下笔者密码学总结,实现开放接口验签和加密这篇文章。
整体思路,就是客户端A生成RSA公私钥,客户端A将公钥发给客户端B。客户端B收到公钥,生成AES密钥,同时将AES密钥用收到的公钥进行加密。客户端B将加密后的AES密钥发给客户端A .客户端A收到RSA加密后的密钥。客户端A,用私钥解密,获得了AES密钥对。
在AES密钥传输过程中,因为除了客户端A其他人都不知道RSA私钥。也就无法知道AES密钥了。
代码实现
1.客户端A生成RSA公私钥,将公钥发给客户端B
public void openE2EE(long userId) {
final ChatChannelDTO chatChannelDTO = CHAT_CHANNEL_MAP.get(userId);
final String[] keyPair = JiDigitUtil.genKeyPair(JiDigitUtil.RSA_ALGORITHM);
String publicKeyBase64 = keyPair[0];
String privateKeyBase64 = keyPair[1];
chatChannelDTO.setPublicKey(publicKeyBase64);
chatChannelDTO.setPrivateKey(privateKeyBase64);
chatChannelDTO.setEncryptType(CommonStatusEnum.DISABLE.getStatus());
privateMessage(publicKeyBase64, ChatMessageTypeEnum.RSA_PUBLIC_KEY.getCode(), userId);
}
2.客户端B收到公钥,生成AES密钥,同时将AES密钥用收到的公钥进行加密
case RSA_PUBLIC_KEY:
if (Objects.equals(clientInfo.getDeviceType(), DeviceTypeEnum.MOBILE.getCode())) {
// 收到RSA公钥,那么生成E2EE 密钥发给对方。作为通信密钥
final String publicKey = chatSendMessage.getMessageContent();
final String secretKey = JiDigitUtil.genSecretKey(JiDigitUtil.AES_ALGORITHM);
chatChannelDTO.setSecretKey(secretKey);
chatChannelDTO.setEncryptType(CommonStatusEnum.ENABLE.getStatus());
//E2EE密钥用 RSA公钥加密,发给对方
final String encryptRsa = JiDigitUtil.encryptRsa(secretKey, publicKey);
jiChatClient.privateMessage(encryptRsa, ChatMessageTypeEnum.END_TO_END_KEY.getCode(), chatSendMessage.getMessageFrom());
} else {
log.info("密钥连接请求");
}
break;
3.客户端A收到RSA加密后的密钥。客户端A,用私钥解密,获得了AES密钥对。
if (Objects.equals(clientInfo.getDeviceType(), DeviceTypeEnum.MOBILE.getCode())) {
// 收到E2EE 密钥
final String encryptRsa = chatSendMessage.getMessageContent();
final String secretKey = JiDigitUtil.decryptRsa(encryptRsa, chatChannelDTO.getPrivateKey());
chatChannelDTO.setSecretKey(secretKey);
chatChannelDTO.setEncryptType(CommonStatusEnum.ENABLE.getStatus());
} else {
log.info("密钥连接请求");
}
break;
4.客户端A用收到的AES密钥加密要发送的消息内容
/**
* 发送私聊消息
*
* @param msg 消息内容
* @param messageType 消息类型 1:文字 2:图片 3:语音 4:视频 5:文件 6:RSA公钥 7:端到端密钥
* @param userId 发送到用户id
* @author jisl on 2024/1/29 10:34
**/
public void privateMessage(String msg, Integer messageType, long userId) {
if (!CHAT_CHANNEL_MAP.containsKey(userId)) {
throw new ServiceException("和他还不是好友:" + userId);
}
final ChatChannelDTO chatChannelDTO = CHAT_CHANNEL_MAP.get(userId);
final ChatSendMessage chatMessage = ChatSendMessage.builder()
.messageFrom(clientInfo.getUserId()).messageTo(userId).messageType(messageType)
.messageContent(msg).channelKey(chatChannelDTO.getChannelKey()).encryptType(chatChannelDTO.getEncryptType())
.build();
clientInfo.fillMessage(chatMessage);
if (Objects.equals(ChatMessageTypeEnum.TEXT.getCode(), messageType) && Objects.equals(chatChannelDTO.getEncryptType(), CommonStatusEnum.ENABLE.getStatus())) {
//E2EE加密会话
chatMessage.setMessageContent(JiDigitUtil.encryptAes(msg, chatChannelDTO.getSecretKey(), chatMessage.getNonce()));
}
chatMessage.setCode(CommandCodeEnum.PRIVATE_MESSAGE.getCode());
MESSAGES_QUEUE.add(chatMessage);
CompletableFuture.runAsync(this::syncSendMsg);
}
5.客户端B收到加密后的密文,进行解密
case TEXT:
if (chatSendMessage.getEncryptType().equals(CommonStatusEnum.ENABLE.getStatus())) {
//密文消息
if (Objects.equals(clientInfo.getDeviceType(), DeviceTypeEnum.MOBILE.getCode())) {
if (Objects.equals(chatChannelDTO.getEncryptType(), CommonStatusEnum.ENABLE.getStatus())) {
final String decryptContent = JiDigitUtil.decryptAes(chatSendMessage.getMessageContent(), chatChannelDTO.getSecretKey(), chatSendMessage.getNonce());
log.info("解密后的明文:{}", decryptContent);
} else {
log.warn("当前手机客户端与用户{}的E2EE还没开启,请手动开启", chatSendMessage.getMessageFrom());
}
} else {
log.info("端到端加密请到手机端查看");
}
}
break;
运行效果
客户端A
2024-02-01 15:28:07.966 [,] DEBUG 21512 --- [jichat-work-1-1] c.ji.jichat.client.client.JiChatClient : 客户端手动发消息成功=BAr5IpW9idUlQ2JDz4ZSwQ==
服务端
Message received: {"channelKey":"cOvWLMNlsma_cOkLiKpPVqd","code":2002,"createTime":1706772572042,"encryptType":1,"messageContent":"GdexpxEtKr28X6wxeEDURw==","messageFrom":1749622404973465600,"messageId":132,"messageTo":1747905091907751936,"messageType":101,"nonce":"9DdXnA7isEKj8ENe","userKey":"1747905091907751936_1"}
客户端B
2024-02-01 15:29:33.992 [,] INFO 21512 --- [jichat-work-1-1] c.j.j.c.netty.handler.BizClientHandler : 收到服务端消息:ChatSendMessage(super=Message(userKey=1747905091907751936_1, code=2002, nonce=9DdXnA7isEKj8ENe), messageFrom=1749622404973465600, messageTo=1747905091907751936, encryptType=1, messageType=101, messageContent=GdexpxEtKr28X6wxeEDURw==, channelKey=cOvWLMNlsma_cOkLiKpPVqd, messageId=132, createTime=Thu Feb 01 15:29:32 GMT+08:00 2024)
2024-02-01 15:29:33.992 [,] INFO 21512 --- [jichat-work-1-1] c.j.j.c.netty.handler.BizClientHandler : 解密后的明文:你也好
GitHub源码地址
总结
以上是运用了非对称加密,来解决客户端之间的密钥分发问题。以上还留有几个问题
(1)多客户端如何实现加密,比如一个用户可以是电脑和手机同时在线。那么密钥分配就需要发个两个客户端。目前看了主流的IM简单粗暴,就是仅支持手机端端到端加密。大家可以思考下,如果需要要满足多客户端那么要如何实现。
(2)密钥失效问题,如果用户卸载APP或者换个手机,那么密钥丢失了。这时候要如何失效密钥更换。