美文网首页
dh秘钥交换算法实践

dh秘钥交换算法实践

作者: 安静的夜灬 | 来源:发表于2020-08-24 10:53 被阅读0次

    一、场景

    出于对于服务器与客户端数据传输安全的需要,将整个请求的入参出参按照约定方式加密,这里选用 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 提供开源项目

    欢迎关注我的个人公众号

    相关文章

      网友评论

          本文标题:dh秘钥交换算法实践

          本文链接:https://www.haomeiwen.com/subject/fhmgjktx.html