• 14.11 Socket 基于时间加密通信


    在之前的代码中我们并没有对套接字进行加密,在未加密状态下我们所有的通信内容都是明文传输的,这种方式在学习时可以使用但在真正的开发环境中必须要对数据包进行加密,此处笔者将演示一种基于时间的加密方法,该加密方法的优势是数据包每次发送均不一致,但数据包内的内容是一致的,当抓包后会发现每次传输的数据包密文是随机变化的,但内容始终保持一致,也就是说两个拥有相同内容的数据被加密后,数据包密文不同,其主要运用了基于当前时间戳的通信机制。

    14.11.1 实现加盐函数

    加盐函数此处笔者采用基于时间的加盐方式,取出用户分钟数与秒数并生成随机数作为盐,通过三者的混合计算出一串解密密钥对,此方法的必须保证服务端与客户端时间同步,如果不同步则无法计算出正确的密钥对,解密也就无法继续了。

    代码中函数GenRandomString用于实现生成一个随机数,该函数接受一个随机数长度并返回一个字符串。接着GetPasswordSalt_OnSecGetPasswordSalt_OnMin函数分别用于根据当前秒与分钟生成一个随机的盐,函数GetXorKey则用于对特定一段字符串进行异或处理并生成一个Key,函数CRC32则用于对字符串计算得到一个哈希值。

    #include 
    #include 
    #include 
    #include 
    #include 
    
    #pragma comment(lib,"ws2_32.lib")
    
    using namespace std;
    
    typedef struct
    {
      char random[1024];
      char Buffer[4096];
    }SocketPackage;
    
    // 产生长度为length的随机字符串
    char* GenRandomString(int length)
    {
      int flag, i;
      char* string;
      srand((unsigned)time(NULL));
      if ((string = (char*)malloc(length)) == NULL)
      {
        return NULL;
      }
    
      for (i = 0; i < length - 1; i++)
      {
        flag = rand() % 3;
        switch (flag)
        {
        case 0:
          string[i] = 'A' + rand() % 26;
          break;
        case 1:
          string[i] = 'a' + rand() % 26;
          break;
        case 2:
          string[i] = '0' + rand() % 10;
          break;
        default:
          string[i] = 'x';
          break;
        }
      }
      string[length - 1] = '\0';
      return string;
    }
    
    // 通过秒数生成盐
    int GetPasswordSalt_OnSec()
    {
      time_t nowtime;
      struct tm* p;;
      time(&nowtime);
      p = localtime(&nowtime);
      if (p->tm_sec <= 10)
        return 2;
      else if (p->tm_sec > 10 && p->tm_sec <= 20)
        return 5;
      else if (p->tm_sec > 20 && p->tm_sec <= 30)
        return 8;
      else if (p->tm_sec > 30 && p->tm_sec <= 40)
        return 4;
      else if (p->tm_sec > 40 && p->tm_sec <= 50)
        return 9;
      else
        return 3;
    }
    
    // 通过分钟生成盐
    int GetPasswordSalt_OnMin()
    {
      time_t nowtime;
      struct tm* p;;
      time(&nowtime);
      p = localtime(&nowtime);
      return p->tm_min;
    }
    
    // 获取异或整数
    long GetXorKey(const char* StrPasswd)
    {
      char cCode[32] = { 0 };
      strcpy(cCode, StrPasswd);
      DWORD Xor_Key = 0;
      for (unsigned int x = 0; x < strlen(cCode); x++)
      {
        Xor_Key = Xor_Key + (GetPasswordSalt_OnSec() * GetPasswordSalt_OnMin()) + cCode[x];
      }
      return Xor_Key;
    }
    
    // 计算CRC32校验和
    DWORD CRC32(char* ptr, DWORD Size)
    {
      DWORD crcTable[256], crcTmp1;
    
      // 动态生成CRC-32表
      for (int i = 0; i < 256; i++)
      {
        crcTmp1 = i;
        for (int j = 8; j > 0; j--)
        {
          if (crcTmp1 & 1) crcTmp1 = (crcTmp1 >> 1) ^ 0xEDB88320L;
          else crcTmp1 >>= 1;
        }
        crcTable[i] = crcTmp1;
      }
      // 计算CRC32值
      DWORD crcTmp2 = 0xFFFFFFFF;
      while (Size--)
      {
        crcTmp2 = ((crcTmp2 >> 8) & 0x00FFFFFF) ^ crcTable[(crcTmp2 ^ (*ptr)) & 0xFF];
        ptr++;
      }
      return (crcTmp2 ^ 0xFFFFFFFF);
    }
    
    int main(int argc, char *argv[])
    {
      // 生成一个随机数作为盐
      char* uuid = GenRandomString(7);
      std::cout << "随机数: " << uuid << std::endl;
    
      int sec_key = GetPasswordSalt_OnSec();
      std::cout << "根据秒数生成盐: " << sec_key << std::endl;
    
      int min_key = GetPasswordSalt_OnMin();
      std::cout << "根据分钟生成盐: " << min_key << std::endl;
    
      // 传入随机数作为密钥对,生成最终密钥
      long key = GetXorKey(uuid);
      std::cout << "最终密钥: " << key << std::endl;
    
      int crc32 = CRC32(uuid, 10);
      std::cout << "crc32: " << hex << crc32 << std::endl;
    
      system("pause");
      return 0;
    }
    

    14.11.2 实现加密函数

    对于加密函数SendEncryptionPage的实现流程,首先在发送数据包之前调用GenRandomString()生成一个7位的随机数,并将随机数拷贝到pack.random结构内,接着调用异或函数GetXorKey(uuid)生成加密密钥,并依次循环对pack.Buffer中的数据进行逐字节加密。最后将加密数据包发送出去,并接着计算该数据包的CRC32值,并再次通过send()函数将其发送给客户端。

    // 加密数据包并发送
    bool SendEncryptionPage(SOCKET* ptr, char* send_data)
    {
        char buf[8192] = { 0 };
        SocketPackage pack;
    
        memset(buf, 0, 8192);
    
        // 生成随机数并拷贝到结构体
        char* uuid = GenRandomString(7);
        strcpy(pack.random, uuid);
        std::cout << "[客户端] 本次随机密钥对: " << uuid << std::endl;
    
        // 生成并拷贝加密数据
        strcpy(pack.Buffer, send_data);
    
        int key = GetXorKey(uuid);
        std::cout << " --> 生成随机 key = " << key << std::endl;
    
        for (int x = 0; x < strlen(pack.Buffer); x++)
        {
            pack.Buffer[x] = pack.Buffer[x] ^ key;
        }
    
        // 加密数据包并发送
        memcpy(buf, &pack, sizeof(SocketPackage));
        send(*ptr, buf, sizeof(buf), 0);
    
        // 计算CRC32校验和,并发送给服务端
        DWORD crc32 = CRC32(buf, 100);
        char send_crc32[1024] = { 0 };
        sprintf(send_crc32, "%x", crc32);
        std::cout << " --> 发送CRC32校验和 = " << send_crc32 << std::endl;
    
        // 发送CRC32计算结果
        send(*ptr, send_crc32, sizeof(send_crc32), 0);
        return true;
    }
    

    14.11.3 实现解密函数

    解密函数RecvDecryptPage的实现流程与加密函数需要对应,首先当收到加密后的数据包时,该数据包会被存入buf变量内存储,并强制类型转为结构体。接着调用GetXorKey函数生成随机数,该随机数是通过本机时间通过分钟与秒数生成的盐,并与用户密码进行异或得到。通过接收服务器端发过来的CRC32校验码,比对原始数据包有没有被修改过,该校验码是服务端通过数据包生成的,最后客户端计算收到的数据包CRC32是否与服务端一致,一致则继续执行异或循环对数据包进行逐字节解包。

    // 接收数据包并解密
    char* RecvDecryptPage(SOCKET *ptr)
    {
        char buf[8192] = { 0 };
    
        // 接收加密后的数据包
        memset(buf, 0, sizeof(buf));
        recv(*ptr, buf, sizeof(buf), 0);
        SocketPackage* pack = (SocketPackage*)buf;
    
        // 接收随机数并获取异或密钥
        int key = GetXorKey(pack->random);
        std::cout << "[服务端] 基于时间计算 key = " << key << std::endl;
    
        // 服务端验证网络CRC32数据包是否一致
        char recv_crc32[1024] = { 0 };
        recv(*ptr, recv_crc32, sizeof(recv_crc32), 0);
        std::cout << "  --> 收到客户端CRC32校验和 = " << recv_crc32 << std::endl;
    
        // 计算CRC32是否与发送值一致
        DWORD crc32 = CRC32(buf, 100);
        char this_crc32[1024] = { 0 };
        sprintf(this_crc32, "%x", crc32);
        std::cout << "  --> 计算本地数据包CRC32校验和 = " << this_crc32 << std::endl;
    
        if (strcmp(recv_crc32, this_crc32) == 0)
        {
            std::cout << "  --> 校验和一致" << std::endl;
    
            // 开始解密数据包
            for (int x = 0; x < strlen(pack->Buffer); x++)
            {
                pack->Buffer[x] = pack->Buffer[x] ^ key;
            }
    
            std::cout << "    --> 解密后的数据: " << pack->Buffer << std::endl;
            std::cout << std::endl;
            return pack->Buffer;
        }
    }
    

    14.11.4 数据加密收发

    当有了上述完整加解密函数的封装之后读者就可以通过使用套接字的方法来实现数据包的通信,当需要接收数据时可以直接调用RecvDecryptPage()函数并传入当前活动套接字,而如果需要发送数据则也只需要调用SendEncryptionPage()函数即可,由于函数已被封装所以在传输数据时与普通套接字函数的使用没有任何区别。

    针对服务端的主函数如下所示;

    int main(int argc, char* argv[])
    {
        WSADATA WSAData;
        SOCKET sock, msgsock;
        struct sockaddr_in ServerAddr;
    
        if (WSAStartup(MAKEWORD(2, 0), &WSAData) != SOCKET_ERROR)
        {
            ServerAddr.sin_family = AF_INET;
            ServerAddr.sin_port = htons(9999);
            ServerAddr.sin_addr.s_addr = INADDR_ANY;
    
            sock = socket(AF_INET, SOCK_STREAM, 0);
            bind(sock, (LPSOCKADDR)&ServerAddr, sizeof(ServerAddr));
            listen(sock, 10);
        }
    
        while (1)
        {
            msgsock = accept(sock, (LPSOCKADDR)0, (int*)0);
    
            // 接收数据并解密
            char * recv_data = RecvDecryptPage(&msgsock);
            std::cout << "获取包内数据: " << recv_data << std::endl;
    
            // 发送数据
            SendEncryptionPage(&msgsock, (char*)"ok");
            std::cout << std::endl;
    
            closesocket(msgsock);
        }
        closesocket(sock);
        WSACleanup();
    
        return 0;
    }
    

    针对客户端的主函数如下所示;

    int main(int argc, char* argv[])
    {
        while (1)
        {
            WSADATA WSAData;
            SOCKET sock;
            struct sockaddr_in ClientAddr;
    
            if (WSAStartup(MAKEWORD(2, 0), &WSAData) != SOCKET_ERROR)
            {
                ClientAddr.sin_family = AF_INET;
                ClientAddr.sin_port = htons(9999);
                ClientAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
                sock = socket(AF_INET, SOCK_STREAM, 0);
                int Ret = connect(sock, (LPSOCKADDR)&ClientAddr, sizeof(ClientAddr));
                if (Ret == 0)
                {
                    // 发送数据
                    char send_message[4096] = "hello lyshark";
                    SendEncryptionPage(&sock, send_message);
    
                    // 接收数据
                    char* recv_data = RecvDecryptPage(&sock);
                    std::cout << "接收数据包: " << recv_data << std::endl;
                    std::cout << std::endl;
                }
            }
            closesocket(sock);
            WSACleanup();
            Sleep(5000);
        }
        return 0;
    }
    

    读者可自行将上述代码片段组合起来,并分别运行服务端与客户端,当运行后读者可看到如下图所示的输出信息;

    本文作者: 王瑞
    本文链接: https://www.lyshark.com/post/f1f85090.html
    版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

  • 相关阅读:
    基于小波神经网络的数据分类算法matlab仿真
    一种自适应模拟退火粒子群优化算法-附代码
    可衡量的项目目标怎么设定?
    Redis:send of 37 bytes failed with errno=10054
    基于antd-vue v1.7.x版本所开发的组织架构树升级版,支持增删改查,滑动加载、远程搜索
    【原理篇】三、SpringBoot自动配置原理
    Python二级 每周练习题23
    【数据挖掘】使用移动平均预测道琼斯、纳斯达克、标准普尔指数——Python中的基本数据操作和可视化
    安卓生成公钥和md5签名
    Java 并发包 park/unpark原理
  • 原文地址:https://www.cnblogs.com/LyShark/p/17773320.html