侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

dh秘钥交换算法实践

Administrator
2020-08-31 / 0 评论 / 0 点赞 / 52 阅读 / 8858 字

一、场景

出于对于服务器与客户端数据传输安全的需要,将整个请求的入参出参按照约定方式加密,这里选用 AES ,入选理由是安全性高、效率高。唯一的问题是需要不通过网络传输秘钥,且加密双方能动态约定秘钥,来完成加密/解密。于是乎这成了本文的话题。

二、秘钥交换算法

这个算法简称 DH(Diffie-Hellman Key Exchange/Agreement Algorithm) ,算法的原理自行百度吧,这不是本文讨论的话题。

要知道的是,我们可以基于这个 DH 算法解决上面的场景问题,即按照某种方式给加密双方一个约定的秘钥。

三、实现

这里我就直接上代码了,有兴趣的直接粘贴复制就完事了。

  • DHProperties 定义一个属性文件来约束DH
@Data
@Component
@ConfigurationProperties(prefix = "info.dh")
public class DHProperties {
/**
     * 加密开关,true表示开启加密,默认是true
     */
    private Boolean open = true;
    /**
     * redis 存储协商中的前缀
     */
    private String conferPrefix;
    /**
     * 协商中过期时间,单位(秒)
     */
    private Long conferOverdueTime;
    /**
     * redis 存储协商完成的前缀
     */
    private String keyPrefix;
    /**
     * 协商结果过期时间,单位(秒)
     */
    private Long overdueTime;
}
  • Locks 定义一个基于 redis 的分布式锁
@Slf4j
@Component
public class Locks {
   @Autowired
    StringRedisTemplate redisTemplate;

    public <T> void consumer(String key, T t, Consumer<T> fun) {
        log.info("获取分布式锁: {}", key);
        try {
            if (!redisTemplate.opsForValue().setIfAbsent(key, "1")) throw new RuntimeException("获取分布式锁异常");
            fun.accept(t);
        } finally {
            redisTemplate.delete(key);
        }
    }
 }
  • DHService 秘钥算法的具体实现类

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flashwhale.cloud.gateway.common.DHProperties;
import com.flashwhale.cloud.gateway.utlis.Locks;
import com.flashwhale.cloud.gateway.utlis.dh.ClientDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/***
 * 基于dh的协商实现 key的方案
 */
@Component
public class DHService {
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ObjectMapper objectMapper;
    @Autowired
    DHProperties dhProperties;
    @Autowired
    Locks locks;

    /**
     * 协商中
     * 客户端用来获取协商信息
     *
     * @return 返回协商中的信息到客户端
     */
    public Map<String, String> getBaseData() {
        DH dh = new DH();
        Map<String, String> baseData = dh.init();
        String uuid = UUID.randomUUID().toString();
        redisTemplate.opsForValue().set(dhProperties.getConferPrefix() + uuid, serialization(baseData),
                dhProperties.getConferOverdueTime(), TimeUnit.SECONDS);
        //服务端的server_number是不能直接暴露给客户端的 我们给客户端的应该是processed_server_number
        String processedServerNum = baseData.get("processed_server_number");
        baseData.remove("processed_server_number");
        baseData.put("server_number", processedServerNum);
        baseData.put("uuid", uuid);
        return baseData;
    }

    /**
     * 即将协商完成
     * 客户端回发 协商信息 到服务端, 服务端生成协商秘钥
     *
     * @param clientDTO 客户端传过来的参数
     */
    public void postClientData(ClientDTO clientDTO) {
        if (null == clientDTO
                || !StringUtils.hasText(clientDTO.getClientNumber())
                || !StringUtils.hasText(clientDTO.getUuid())) return;
        locks.consumer(clientDTO.getUuid(), clientDTO.getClientNumber(), (x) -> {
            // 需要根据客户端传来的 uuid 取出上一个接口中协商好的server_number和p
            String json = redisTemplate.opsForValue().get(dhProperties.getConferPrefix() + clientDTO.getUuid());
            if (!StringUtils.hasText(json)) return;
            Map<String, String> ret = deserialization(json);
            //删除这个id
            redisTemplate.delete(clientDTO.getUuid());
            String serverNum = ret.get("server_number");
            String p = ret.get("p");
            DH dh = new DH();
            String key = dh.computeShareKey(clientDTO.getClientNumber(), serverNum, p);
            System.out.println(key);
            redisTemplate.opsForValue().set(dhProperties.getKeyPrefix() + clientDTO.getUuid(), key, dhProperties.getOverdueTime(), TimeUnit.SECONDS);
        });
    }

    /**
     * 获取协商好的key
     *
     * @param uuid 协商会话标记
     * @return 返回协商的key  如果为null 表示没有协商key
     */
    public String getKey(String uuid) {
        if (!redisTemplate.hasKey(dhProperties.getKeyPrefix() + uuid)) return null;
        return redisTemplate.opsForValue().get(dhProperties.getKeyPrefix() + uuid);
    }

    /**
     * 反序列化方法
     *
     * @param json json字符串
     * @return 返回一个map ,由于使用的是 StringRedisTemplate 存储 ,通常这里是一个 Map<String, String>
     */
    HashMap deserialization(String json) {
        try {
            return objectMapper.readValue(json, HashMap.class);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("反序列化失败");
        }
    }

    /**
     * 序列化方法
     *
     * @param dataMap 要序列化的字符串, 通常这里是一个 Map<String, String>
     * @return 返回一个json字符串
     */
    String serialization(Map<String, String> dataMap) {
        try {
            return objectMapper.writeValueAsString(dataMap);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("序列化失败");
        }
    }

}

  • ClientDTO 在提供接口前构造一个类用于封装客户端入参
@Data
public class ClientDTO implements Serializable {
    /**
     * 客户端生成的 clientNumber
     */
    String clientNumber;
    /**
     * 第一步返回给客户端的 redis缓存标记
     */
    String uuid;
}
  • 构建用于客户端请求服务器的 DH 服务接口
/**
 * 给客户端提供的协商keyapi
 */
@RequestMapping("dh")
@RestController
public class DHController {

    @Autowired
    DHService dhService;

    /**
     * 给客户端提供一个可以使用的 server_number 相关信息
     */
    @GetMapping("basedata")
    Map<String, String> dhBaseData() {
        return dhService.getBaseData();
    }

    /***
     * 传入客户端生成的 clientNumber 来生成协商的 key
     */
    @PostMapping("clientdata")
    void dhClientData(@RequestBody ClientDTO clientDTO) {
        dhService.postClientData(clientDTO);
    }

}

接口说明:

这里我们提供了2个接口

  • basedata 用于客户端找服务器第一次获取协商数据
  • clientdata 用户客户端将自己预创建的 key 发送到服务器端(以便服务器算出最终秘钥)

四、测试

编写一个客户端来测试,预期结果是不将秘钥通过网络传输,各自算出相同的秘钥。

@Component
public class DHClient {
    private static int mClientNum = (new Random()).nextInt(89999) + 10000;
    @Autowired
    RestTemplate restTemplate;

    public String getKey() {
        Map<String, String> resMap = restTemplate.getForObject("http://localhost:8080/dh/basedata", Map.class);
        String p = resMap.get("p");
        String g = resMap.get("g");
        String serverNumber = resMap.get("server_number");
        String uuid = resMap.get("uuid");
        String processClientNumber = processNum(g, mClientNum + "", p);
        System.out.println("客户端生成的 clientNumber 是: " + processClientNumber);

        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.put("clientNumber", Collections.singletonList(processClientNumber));
        paramMap.put("uuid", Collections.singletonList(uuid));
        restTemplate.postForObject("http://localhost:8080/dh/clientdata", paramMap, Void.class);
        String mClientKey = processNum(serverNumber, mClientNum + "", p);
        System.out.println("客户端协商的key 是: " + mClientKey);
        test(mClientKey);
        return mClientKey;
    }

    static String processNum(String g, String e, String p) {
        BigInteger mP = new BigInteger(p);
        BigInteger mG = new BigInteger(g);
        return mG.modPow(new BigInteger(e), mP).toString();
    }

    void test(String key) {
        System.out.println("客户端加密内容:" + AESUtil.encrypt("你好 我是测试内容 nihao,woshicesneir 1234567890", key));
    }
}

这里要感谢 ti-dh 提供开源项目

个人公众号

0

评论区