基于以太坊智能合约的加密聊天系统

admin
admin 2021年07月17日
  • 在其它设备中阅读本文章

一、需要实现的功能

  1. 聊天功能,可以给任何地址账户发送聊天信息,同时也可以接收,且可以查看聊天历史
  2. 信息加密,所有聊天的信息只有聊天双方可以看到,其他人不可看
  3. 对称加密密钥管理,用于聊天信息的加密与解密,需要存储到区块链上

二、合约设计

注:如无特殊说明,序号、编号、索引等是从 0 开始的

1、对称密钥管理合约接口:

a、密钥存储(地址A、加密的会话密钥SSK1、加密的会话密钥SSK2)
    首次聊天需要设置双方通信的对称加密密钥,且需要经过双方的公钥加密然后存储到本合约
    SSK1是调用发起地址用公钥加密的聊天密钥,调用发起地址可以通过私钥解开这个聊天密钥,SSK2同理
    聊天双方只需要有一方设定即可,且只能设置一次(如果聊天中途修改,之前的聊天信息就因为密钥改变而无法解密)
b、密钥查询(地址A)(加密的会话密钥SSK)
    查询调用发起地址用公钥加密后的与地址A通信的聊天密钥

用来存储双方用来加密聊天信息的对称密钥,且为了私密性,用公钥对聊天密钥加密,保证除了聊天双方谁也不知道聊天密钥

2、聊天记录合约接口:
信息发送与接收

a、发送(地址B、加密的聊天信息SM)
    前提:信息发送方和接收方存在聊天密钥
    调用此接口地址A为信息发送方、地址B为接收方
b、接收(地址A、信息序号n)
    调用此接口地址B为信息接收方,地址A为发送方,信息序号表示对方发送的第n条信息,
    返回地址A发过来的第n条信息
c、信息数量(地址A)
    调用此接口地址B为信息接收方,地址A为发送方
    返回地址A发过来的信息条数

好友列表

d、好友列表()
    返回调用此接口地址A的所有好友地址
e、好友(序号n)
    返回调用此接口地址A的第n个好友地址
f、好友数量()
    返回调用此接口地址A的总好友数量

聊天历史

g、会话(地址A、地址B)
    返回地址A和地址B的会话(即发送信息的方向记录,如果A向B发送计为true,否则false)
h、聊天信息查询(地址A、地址B、信息序号n)
    限制地址A或者地址B才能发起调用
    返回地址A向地址B发送的第n条信息

不提供查询有新信息地址集合原因:如果用数组存储地址集合,那么当将一个地址从地址集合里删除时很麻烦; 如果转换成字典存储将会增加一些辅助变量,增加合约存储的压力,查询时还要转换成数组返回,浪费计算资源; 另外还需要发送交易来清除新信息地址集合,要发交易清除!!!

以上接口除了发送信息需要发送交易以外,别的接口可以直接调用,立刻返回数据。合约只存储必要的数据,减少不必要的 gas 消耗,可调用的接口比较简洁,如果前端需要特殊复杂的接口可以使用适配器重新包装组合这些接口

三、简单交互流程

以地址 A1 和 A2 为例

  1. 生成加密的聊天密钥,地址 A1 或者地址 A2 随机生成一个聊天密钥 SK,分别用地址 A1 和 A2 的公钥加密得到加密的聊天密钥 SSK1 和 SSK2
  2. 调用密钥管理合约的密钥存储接口,把聊天密钥 SSK1 和 SSK2 存储到合约里,这样双方可以通过密钥查询接口查询与对方通信的密钥
  3. 地址 A1 调用聊天记录合约的发送接口,把加密后的信息发送到地址 A2
  4. 地址 A2 可以直接调用接收接口获取到地址 A1 发送来的信息
  5. 地址 A2 调用别的接口可以查询到聊天记录和好友列表,如果有新的好友还可以去查聊天密钥
  6. 同理,其他的地址也可以如此聊天

四、客户端设计

通过以太坊的 web3js 库来调用合约来实现聊天系统

初始化

  1. 配置以太坊账户,如果已经存在就读取,否则新建一个
  2. 好友列表和聊天会话构建,如果本地存在,就从本地恢复,否则连接区块链下载数据恢复,大致流程:
    先调用 d 接口获取到所有好友列表
    遍历好友列表使用 g 接口查询会话记录,同时使用 h 接口恢复聊天信息

新的好友和信息接收(需要定时重复调用)
可以通过本地与区块链上的信息数量和好友数量对比来判断是否有新信息

  1. 新好友获取,新的好友数量使用 f 接口,再根据序号(第一个收到或者发送信息的好友序号为 0, 以此类推)调用 e 接口查询好友地址
  2. 新信息获取,遍历好友地址,调用 c 接口查询信息数量,再根据序号调用 b 接口接收信息,同时需要获取聊天密钥解密信息

信息发送

  1. 配置聊天密钥,先查询是否已经存在,如果没有就生成一个并存储到对称密钥管理
  2. 用聊天密钥加密信息调用 a 接口发送信息(需要以交易的形式发送)

五、合约

// sk_manage.sol
pragma solidity ^0.6.6;


contract SK {
    mapping(address=>mapping(address=>bytes)) private ssks;
    
    function Store(address B,bytes memory ssk1,bytes memory ssk2) public {
       address A = msg.sender;
       require(ssk1.length > 0 && ssk2.length > 0);
       require(ssks[A][B].length == 0 && ssks[B][A].length == 0);
       ssks[A][B] = ssk1;
       ssks[B][A] = ssk2;
    }

    
    function Search(address B) public view returns (bytes memory){
       address A = msg.sender;
       return ssks[A][B];
    }
}


// chat_manage.sol
pragma solidity ^0.6.6;


contract Chat {
    mapping(address=>mapping(address=>bytes[])) private sms;        //地址A向地址B发送的信息集合
    
    mapping(address=>address[]) private friendsAddrs;               //某一个地址的好友地址集合
    mapping(address=>mapping(address=>bool)) private isFriends;     //地址A和地址B是不是好友
    
    mapping(address=>mapping(address=>bool[])) private sessions;    //地址A和地址B的会话记录(表示发送方向,且A<B)
    
    function Send(address B,bytes memory sm) public {
       address A = msg.sender;
       require(sm.length > 0);
       require(A != B);
       sms[A][B].push(sm);
       
       if (!(isFriends[A][B]||isFriends[B][A])){
           friendsAddrs[A].push(B);
           friendsAddrs[B].push(A);
           isFriends[A][B] = true;
       }
       
       if (uint256(A) < uint256(B)){
           sessions[A][B].push(true);
       }else{
           sessions[A][B].push(false);
       }
    }
    
    function Recv(address A, uint64 index) public view returns (bytes memory){
       address B = msg.sender;
       require(index < sms[A][B].length);
       return sms[A][B][index];
    }
    
    function MsgNum(address A) public view returns (uint64) {
        address B = msg.sender;
        return uint64(sms[A][B].length);
    }
    
    function FriendAddrs() public view returns (address[] memory) {
       address A = msg.sender;
       return friendsAddrs[A];
    }
    
    function FriendNum() public view returns (uint64) {
        address A = msg.sender;
        return uint64(friendsAddrs[A].length);
    }
    
    function FriendAddrByIndex(uint64 index) public view returns (address) {
       address A = msg.sender;
       require(index < friendsAddrs[A].length);
       return friendsAddrs[A][index];
    }
    
    function Session(address A, address B) public view returns (bool[] memory) {
        require(uint256(A) < uint256(B));
        return sessions[A][B];
    }
    
    function ReadMsg(address A, address B, uint64 index) public view returns (bytes memory) {
        require(msg.sender == A || msg.sender == B);
        require(uint256(A) < uint256(B));
        return sms[A][B][index];
    }
}