背景
当需要对数据的传输或表达有安全级别不高的可逆加密,但要求其加解密时间复杂度越低越好,这时使用常规的对称加解密(3DES、AES)或者非对称加解密(RSA、ECC)显然不合适。首先(1),加密的安全级别和加解密时间复杂度两者之间就是一对矛盾,那么如何做到破解起来不那么容易,加解密本身又非常高效的熊鱼兼得呢?另外(2),依稀记得上大学的时候老师说过这样的一句话:相对于古典加解密,现代加解密一般都是算法公开,密钥的管理才是其核心问题。那么,基于以上两点有没有什么好的套路去兼顾各方呢?即:我既不想加解密成本高,也不想去管理该死的密钥(常规的套路就是将密钥的生成或者管理揉到加密算法之中去),还要其破解起来不那么容易。那么,下面我举例一些常用套路。
1. 密码学三大原则
在正式介绍开始之前先说下密码学三大原则:扩散、混淆、雪崩效应
- 扩散 扩散就是让明文中的每一位影响密文中的许多位,或者说让密文中的每一位受明文中的许多位的影响,这样可以隐蔽明文的统计特性。
- 混淆 混淆就是将密文与密钥之间的统计关系变得尽可能复杂,使得对手即使获取了关于密文的一些统计特性,也无法推测密钥。使用复杂的非线性代替变换可以达到比较好的混淆效果,而简单的线性代替变换得到的混淆效果则不理想。
- 雪崩 雪崩效应(Avalanche effect)指加密算法(尤其是块密码和加密散列函数)的一种理想属性。雪崩效应就是一种不稳定的平衡状态也是加密算法的一种特征,它指明文或密钥的少量变化会引起密文的很大变化。雪崩效应是指当输入发生最微小的改变(例如,反转一个二进制位)时,也会导致输出的剧变(如,输出中一半的二进制位发生反转)。在高品质的块密码中,无论密钥或明文的任何细微变化都应当引起密文的剧烈改变。
2. 常用套路实例
2.1 利用多表位移法(维尼吉亚密码)生成看似随机的定长数字会议号
1、场景:
关于腾讯会议号的生成方法的推测网络上真实说啥都有,有的真的太重了,例如:[腾讯会议号设计的几种猜测]https://cloud.tencent.com/developer/article/2030928,不管怎么样,保障轻量高效和密码学三大原则都是应该充分被遵守的。
2、思路:
- 利用MD5的高散列性生成一定数量的KeyMap<Integer, Integer>(多表位移用)。
- 加密时根据一个满足一致性的规则(简单取模)获得1个对应的KeyMap,最后再根据该KeyMap进行移位,同时处理MAX_NUMBER越界循环复用。
- 解密即加密逆计算。
- 初始密钥为密钥数量的MD5值。初始密钥:用于加密工作密钥。工作密钥:用于加密敏感数据(密码、个人信息等)的密钥。初始密钥为密钥数量的MD5值,这使得我们不可以不去考虑该死的密钥管理问题了。
packageencode;importjava.nio.charset.StandardCharsets;importjava.security.MessageDigest;importjava.util.HashMap;importjava.util.Map;/**
* PseudoRandomNumberUtil
*
* @author chenx
*/publicclassPseudoRandomNumberUtil{privatestaticfinalint KEY_COUNT =128;privatestaticfinalint MAX_NUMBER =1000000000;privatestaticfinalint MASK_CODE = MAX_NUMBER / KEY_COUNT;privatestaticfinalMap<Integer,Integer> KEY_MAP =newHashMap<>();privatePseudoRandomNumberUtil(){}static{try{MessageDigest md5 =MessageDigest.getInstance("MD5");
md5.update(String.valueOf(KEY_COUNT).getBytes(StandardCharsets.UTF_8));byte[] keyData = md5.digest();for(int i =0; i < KEY_COUNT; i++){
md5.update(keyData);
keyData = md5.digest();
KEY_MAP.put(i,getKeyDataIntValue(keyData));}}catch(Exception e){thrownewRuntimeException("PseudoRandomNumberUtil init error! "+ e.getMessage());}}/**
* encode
*
* @param number
* @return
*/publicstaticintencode(int number){int index = number % KEY_COUNT;int key = KEY_MAP.get(index).intValue();int temp =(number / KEY_COUNT)+ key;if(temp >= MASK_CODE){
temp -= MASK_CODE;}return temp * KEY_COUNT + index;}/**
* decode
*
* @param number
* @return
*/publicstaticintdecode(int number){int index = number % KEY_COUNT;int key = KEY_MAP.get(index).intValue();int temp = number / KEY_COUNT;if(temp <= key){
temp += MASK_CODE;}
temp -= key;int result = temp * KEY_COUNT + index;if(result >= MAX_NUMBER){
result -= MAX_NUMBER;}return result;}/**
* getKeyDataIntValue
*
* @param keyData
* @return
*/privatestaticintgetKeyDataIntValue(byte[] keyData){int result =0;for(int i =0, len =4; i < len; i++){
result = result |(keyData[i]&0xff);
result = result <<((len - i)*8);}
result = result &Integer.MAX_VALUE;
result = result % MASK_CODE;return result;}publicstaticvoidmain(String[] args){for(int i =0; i <1000; i++){int encodedData =PseudoRandomNumberUtil.encode(i);int decodeData =PseudoRandomNumberUtil.decode(encodedData);System.out.println("rawData:"+ i +" encodedData:"+ encodedData +" decodeData:"+ decodeData);}}}
运行效果如下,可以看出散的还是非常开的。由于计算上都是非常简单的加/减/乘/除/取模,因此十分轻量和高效。
rawData:0 encodedData:777307648 decodeData:0
rawData:1 encodedData:673582081 decodeData:1
rawData:2 encodedData:462563842 decodeData:2
rawData:3 encodedData:572984835 decodeData:3
rawData:4 encodedData:375370756 decodeData:4
rawData:5 encodedData:404839941 decodeData:5
rawData:6 encodedData:936941574 decodeData:6
rawData:7 encodedData:490034183 decodeData:7
rawData:8 encodedData:606869000 decodeData:8
rawData:9 encodedData:968213001 decodeData:9
rawData:10 encodedData:753664522 decodeData:10
rawData:11 encodedData:151120907 decodeData:11
rawData:12 encodedData:509426188 decodeData:12
rawData:13 encodedData:518332429 decodeData:13
rawData:14 encodedData:667913230 decodeData:14
rawData:15 encodedData:787467791 decodeData:15
rawData:16 encodedData:947158544 decodeData:16
rawData:17 encodedData:333873681 decodeData:17
rawData:18 encodedData:212811794 decodeData:18
rawData:19 encodedData:379195923 decodeData:19
rawData:20 encodedData:701412884 decodeData:20
rawData:21 encodedData:660866069 decodeData:21
rawData:22 encodedData:96149526 decodeData:22
rawData:23 encodedData:186095127 decodeData:23
rawData:24 encodedData:64869400 decodeData:24
rawData:25 encodedData:461154841 decodeData:25
rawData:26 encodedData:475000346 decodeData:26
rawData:27 encodedData:601995291 decodeData:27
rawData:28 encodedData:754361372 decodeData:28
rawData:29 encodedData:938285085 decodeData:29
rawData:30 encodedData:513153054 decodeData:30
2.2 按位异或混淆数据
1、场景:
业务系统开发的时候常常使用数字型ID做为数据的主键,但是由于数字的易暴力穷举性,因此如果将其做为参数对外暴露,则有暴力穷举的安全隐患,这个时候则需要一种高效的编码方式(因为满大街的地方用的实在太多了)。当然相关的做法有很多,这里举例一种按位异或的方法,由于异或本身的高效性(目前CPU都对异或运算进行了内置优化)和特性(不进位的二进制加法),对同样的东西进行两次异或即可对数据进行还原。
2、思路:
- 将数据本身进行一次高离散度哈希(不推荐MD5,虽然MD5离散度好,但是效率相对不高,推荐Murmur哈希)
- 将第1步的数据哈希值与数据自身一起进行序列化(serialize(Long snowflakeId)方法)
- 用数据哈希值与数据自身进行按位异或运算得到混淆数据(obfuscate(byte[] bytes)方法)
- 数据本身的哈希值实际上起到一个密钥的作用,由于数据本身不固定,另外哈希算法也不对外直接暴露,因此这里我们也可以不必去考虑密钥管理问题。
importjava.io.*;/**
* IdWorker
*
* @author chenx
*/publicclassIdWorker{privatestaticfinalint HASH_CODE_LENGTH =4;privatestaticfinalint LONG_BYTE_LENGTH =8;privatestaticSnowFlakeIdWorker snowFlakeIdWorker =newSnowFlakeIdWorker(0,0);privateIdWorker(){}/**
* getStringId
*
* @return
* @throws IOException
*/publicstaticStringgetStringId()throwsIOException{byte[] bytes =serialize(snowFlakeIdWorker.nextId());obfuscate(bytes);returnBase58Util.encode(bytes);}/**
* getStringId
*
* @param longId
* @return
* @throws IOException
*/publicstaticStringgetStringId(Long longId)throwsIOException{byte[] bytes =serialize(longId);obfuscate(bytes);returnBase58Util.encode(bytes);}/**
* getLongId
*
* @return
*/publicstaticLonggetLongId(){return snowFlakeIdWorker.nextId();}/**
* getLongId
*
* @param stringId
* @return
* @throws IOException
*/publicstaticLonggetLongId(String stringId)throwsIOException{byte[] bytes =Base58Util.decode(stringId);obfuscate(bytes);returndeserialize(bytes);}/**
* serialize
*
* @param snowflakeId
* @return
* @throws IOException
*/privatestaticbyte[]serialize(Long snowflakeId)throwsIOException{try(ByteArrayOutputStream out =newByteArrayOutputStream();DataOutputStream dos =newDataOutputStream(out)){
dos.writeInt(getHashCode(snowflakeId));
dos.writeLong(snowflakeId);return out.toByteArray();}}/**
* deserialize
*
* @param bytes
* @return
* @throws IOException
*/privatestaticLongdeserialize(byte[] bytes)throwsIOException{try(ByteArrayInputStream in =newByteArrayInputStream(bytes);DataInputStream dis =newDataInputStream(in)){
dis.readInt();return dis.readLong();}}/**
* obfuscate(hashCode按位异或混淆)
*
* @param bytes
*/privatestaticvoidobfuscate(byte[] bytes){byte[] hashBytes =null;byte[] payloadBytes =null;if(bytes ==null){thrownewRuntimeException("bytes is null");}if(bytes.length <= HASH_CODE_LENGTH){thrownewRuntimeException("bytes.length <= "+ HASH_CODE_LENGTH);}int leftBytesSize = bytes.length - HASH_CODE_LENGTH;
hashBytes =newbyte[HASH_CODE_LENGTH];
payloadBytes =newbyte[leftBytesSize];System.arraycopy(bytes,0, hashBytes,0, HASH_CODE_LENGTH);System.arraycopy(bytes, HASH_CODE_LENGTH, payloadBytes,0, leftBytesSize);for(int i = HASH_CODE_LENGTH; i < bytes.length; i++){
bytes[i]=(byte)(hashBytes[i % HASH_CODE_LENGTH]^ payloadBytes[i - HASH_CODE_LENGTH]);}}/**
* getHashCode:使用murmur哈希算法的原因为其散列性较好
*
* @param snowflakeId
* @return
*/privatestaticintgetHashCode(Long snowflakeId){returnMurmurHashUtil.hash32(long2Bytes(snowflakeId));}/**
* long2Bytes
*
* @param num
* @return
*/privatestaticbyte[]long2Bytes(long num){byte[] byteNum =newbyte[LONG_BYTE_LENGTH];for(int ix =0; ix < LONG_BYTE_LENGTH;++ix){int offset =64-(ix +1)* LONG_BYTE_LENGTH;
byteNum[ix]=(byte)((num >> offset)&0xff);}return byteNum;}publicstaticvoidmain(String[] args)throwsIOException{for(int i =0; i <1000; i++){long longId =IdWorker.getLongId();String encodedLongId =IdWorker.getStringId(longId);long decodedLongId =IdWorker.getLongId(encodedLongId);System.out.println("longId:"+ longId +" , encodedLongId:"+ encodedLongId +" , decodedLongId:"+ decodedLongId);}}}
运行效果如下,同样可以看到使用murmur32进行哈希后再按位异或其结果还是散的比较开的,大家试下用其他的哈希方式,例如直接用String.hashCode,那么一定会看到其密码学三大原则评判上的不佳。虽然上述示例只是简单的对Long类型数据进行混淆编码,但是实际上该方法是一种通用的数据混淆方式,之前写了一个文件服务,为了防止文件下载地址的暴力穷举,对于文件索引数据(MetaDataIndex)的混淆就是使用的同样的方式,大家可以参考:https://github.com/bossfriday/bossfriday-nubybear/blob/master/cn.bossfriday.fileserver/src/main/java/cn/bossfriday/fileserver/engine/impl/v1/MetaDataHandler.java
longId:155205853429891072 , encodedLongId:51eYjUeFFvY2jALLq , decodedLongId:155205853429891072
longId:155205853446668288 , encodedLongId:3Wgm2Uhmsdwe7SaDf , decodedLongId:155205853446668288
longId:155205853446668289 , encodedLongId:2c73jdjfPKsitiDbP , decodedLongId:155205853446668289
longId:155205853446668290 , encodedLongId:4CURDUt6mQBE2LTH5 , decodedLongId:155205853446668290
longId:155205853446668291 , encodedLongId:4hrNARwhL537ShgQM , decodedLongId:155205853446668291
longId:155205853446668292 , encodedLongId:3cCxrqvL42Q9D3UNk , decodedLongId:155205853446668292
longId:155205853446668293 , encodedLongId:2xKY2dpMEP7ecDmHb , decodedLongId:155205853446668293
longId:155205853446668294 , encodedLongId:3ZaMM5okHucTDDNUY , decodedLongId:155205853446668294
longId:155205853446668295 , encodedLongId:4NCsDx5KnLYFykeVD , decodedLongId:155205853446668295
longId:155205853446668296 , encodedLongId:HvFSG6kSan2yGmmg , decodedLongId:155205853446668296
longId:155205853446668297 , encodedLongId:5Ad3PAWMrmCB4ya5H , decodedLongId:155205853446668297
longId:155205853450862592 , encodedLongId:5NbiVFkWihqyMru9V , decodedLongId:155205853450862592
longId:155205853450862593 , encodedLongId:2oqmVzAGYT53ZGB1Q , decodedLongId:155205853450862593
longId:155205853450862594 , encodedLongId:26Zq7uhzZavRmzA9t , decodedLongId:155205853450862594
longId:155205853450862595 , encodedLongId:3u8s2PwMb2H93WCBr , decodedLongId:155205853450862595
longId:155205853450862596 , encodedLongId:ARHkhBKd5dG1jaM9 , decodedLongId:155205853450862596
longId:155205853450862597 , encodedLongId:2u81176vAEN9gZv41 , decodedLongId:155205853450862597
longId:155205853450862598 , encodedLongId:w98z2TAQd526wZx8 , decodedLongId:155205853450862598
longId:155205853450862599 , encodedLongId:2GiztYWt59YMFWeKb , decodedLongId:155205853450862599
longId:155205853450862600 , encodedLongId:3vLEYNr8iaxccHH5j , decodedLongId:155205853450862600
longId:155205853450862601 , encodedLongId:2dzH8hoTmdFatGW7F , decodedLongId:155205853450862601
longId:155205853455056896 , encodedLongId:kftKoQrqZ6QZs9kQ , decodedLongId:155205853455056896
longId:155205853455056897 , encodedLongId:5jbp912w62xSSiBTW , decodedLongId:155205853455056897
3. 总结
密码学相关的东西可能多数开发同学了解甚少,我也是同样,不过做为程序猿多少还是有必要了解其基本的东西。从网络上搜索到的片段信息总结来看,一般把密码学归类为:古典密码学和现代密码学。
古典密码学基本要点为:替换法和移位法(当然为了增加其复杂度又衍生出:多表替换和多表位移):
- 单表代换 *是指英文字母在进行密码编码替换的时候,有一张对照表,也就是说,对于每一个字母,都是唯一对应。简单来说就是明文和密文一一对应。
- 单表位移 *本质也是一种“替换”的加密技术,需要加密的“原信息”中的所有字母都在字母表上向后(或向前)按照一个固定数目进行偏移后被替换为加密好的信息。例如,当偏移量是3的时候,所有的字母A将被替换成D,B变成E,以此类推。代表加密算法为:凯撒加密。
现代密码学中我们常常见到或者听到的手段有:
- 散列摘要 例如:MD5,SHA-1,SHA-256,SHA-512。其中MD5具备高散列性:目前标准的MD5算法理论碰撞概率是2的128次方分之一;因此MD5具备强抗碰撞性,即:已知原数据和其MD5值,想找到一个具有相同MD5值的数据(即伪造数据)是非常困难的。
- 对称加密 使用的加密和解密的方式是同一把密钥。常见的加密算法有: DES:Data Encryption Standard,数据加密标准,使用密钥的块加密算法。 AES:Advanced Encryption Standard,高级加密标准,又称Rijndael加密法,区块加密标准。 RC4:Rivest Cipher 4,流加密算法,密钥长度可变。
- 非对称加密 使用公钥加密,必须使用私钥机密;使用私钥加密,必须使用公钥解密。常见的加密算法有: RSA:RSA是一种公钥密码算法,它的名字是由它的三位开发者,即Ron Rivest、Adi Shamir 和 Leonard Adleman 的姓氏的首字母组成的。RSA可以被用于公钥密码和数字签名。RSA是被研究得最广泛的公钥算法,从提出到现在已近三十年,经历了各种攻击的考验,逐渐为人们接受,普遍认为是目前最优秀的公钥方案之一。 ECC:ECC加密算法的全称为“Elliptic curve cryptography”,中文名为“椭圆加密算法”,是一种基于椭圆曲线数学的公开密钥加密算法。
版权归原作者 BossFriday 所有, 如有侵权,请联系我们删除。