【IM系列】IM如何实现端到端加密对话

前言

每次我们使用微信,有时候会想自己发的消息会不会被其他人监听到。特别是给自己的女朋友,发私密消息。还有就是微信管理员,会不会看你的私密消息。如果管理员想看你的,那么你也没办法。那有没一种方法,就是微信管理员也看不到你的私密消息内容。

上面说的管理员,实际就是服务端。而我们使用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源码地址

IM端到端实现

总结

以上是运用了非对称加密,来解决客户端之间的密钥分发问题。以上还留有几个问题
(1)多客户端如何实现加密,比如一个用户可以是电脑和手机同时在线。那么密钥分配就需要发个两个客户端。目前看了主流的IM简单粗暴,就是仅支持手机端端到端加密。大家可以思考下,如果需要要满足多客户端那么要如何实现。
(2)密钥失效问题,如果用户卸载APP或者换个手机,那么密钥丢失了。这时候要如何失效密钥更换。

  • 12
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值