加密算法分为单向加密和双向加密。
单向加密 包括 MD5 , SHA 等摘要算法。单向加密算法是不可逆的,也就是无法将加密后的数据恢复成原始数据,除非采取碰撞攻击和穷举的方式。像是银行账户密码的存储,一般采用的就是单向加密的方式。
双向加密 是可逆的,存在密文的密钥,持有密文的一方可以根据密钥解密得到原始明文,一般用于发送方和接收方都能通过密钥获取明文的情况。双向加密包括对称加密和非对称加密。对称加密包括 DES 加密, AES 加密等,非对称加密包括 RSA 加密, ECC 加密。
AES 算法全称 Advanced Encryption Standard ,是 DES 算法的替代者,也是当今最流行的对称加密算法之一。
要想学习AES算法,首先要弄清楚三个基本的概念:密钥、填充、模式。
密钥是 AES 算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥。
AES支持三种长度的密钥:
128位,192位,256位
平时大家所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用。从安全性来看,AES256安全性最高。从性能来看,AES128性能最高。本质原因是它们的加密处理轮数不同。
要想了解填充的概念,我们先要了解AES的分组加密特性。AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit。
这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。
但是这里涉及到一个问题:
假如一段明文长度是192bit,如果按每128bit一个明文块来拆分的话,第二个明文块只有64bit,不足128bit。这时候怎么办呢?就需要对明文块进行填充(Padding)。AES在不同的语言实现中有许多不同的填充算法,我们只举出集中典型的填充来介绍一下。
不做任何填充,但是要求明文必须是16字节的整数倍。
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}
如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。
比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}
需要注意的是,如果在AES加密的时候使用了某一种填充方式,解密的时候也必须采用同样的填充方式。
AES的工作模式,体现在把明文块加密成密文块的处理过程中。AES加密算法提供了五种不同的工作模式:
ECB、CBC、CTR、CFB、OFB
模式之间的主题思想是近似的,在处理细节上有一些差别。我们这一期只介绍各个模式的基本定义。
电码本模式 Electronic Codebook Book
密码分组链接模式 CipherBlock Chaining
计算器模式 Counter
密码反馈模式 CipherFeedBack
输出反馈模式 OutputFeedBack
如果在AES加密的时候使用了某一种工作模式,解密的时候也必须采用同样的工作模式。
AES加密主要包括两个步骤: 密钥扩展 和 明文加密 。
密钥扩展过程说明(密钥为16字节):
函数g的流程说明:
轮常量(Rcon)是一个字,最右边三个字节总为0。因此字与Rcon相异或,其结果只是与该字最左的那个字节相异或。每轮的轮常量不同,定位为Rcon[j] = (RC[j], 0, 0, 0)。(RC是一维数组)
RC生成函数:RC[1] = 1, RC[j] = 2 * RC[j – 1]。
因为16字节密钥的只进行10轮的扩展,所以最后生成的RC[j]的值按16进制表示为:
十轮的密钥扩展后,就能生成44个字大小的扩展密钥。扩展后的密钥将用于AES对明文的加密过程。
S盒是16×16个字节组成的矩阵,行列的索引值分别从0开始,到十六进制的F结束,每个字节的范围为(00-FF)。
进行字节代替的时候,把状态中的每个字节分为高4位和低4位。高4位作为行值,低4位作为列值,以这些行列值作为索引从S盒的对应位置取出元素作为输出,如下图所示:
S盒的构造方式如下:
(1) 按字节值得升序逐行初始化S盒。在行y列x的字节值是{yx}。
(2) 把S盒中的每个字节映射为它在有限域GF中的逆;{00}映射为它自身{00}。
(3) 把S盒中的每个字节的8个构成位记为(b7, b6, b5, b4, b3, b2, b1)。对S盒的每个字节的每个位做如下的变换:
ci指的是值为{63}的字节c的第i位。
解密过程逆字节代替使用的是逆S盒,构造方式为
字节d={05}。
逆向行移位将状态中后三行执行相反方向的移位操作,如第二行向右循环移动一个字节,其他行类似。
要注意,图示的矩阵的乘法和加法都是定义在GF(2^8)上的。
逆向列混淆原理如下:
轮密钥加后的分组再进行一次轮密钥加就能恢复原值.所以,只要经过密钥扩展和明文加密,就能将明文加密成密文,进行解密的时候,只需要进行逆向变换即可。
图[AES加密算法的流程]中还需要注意,明文输入到输入状态后,需要进行一轮的轮密钥加,对输入状态进行初始化。 前9轮的加密过程,都需要进行字节替代、行移位、列混淆和轮密钥加,但是第10轮则不再需要进行列混淆。
进行解密的时候,需要进行逆向字节替代,逆向行移位、逆向列混淆和轮密钥加。
通常采用同一个秘钥进行信息的加密和解密操作,称为单秘钥加密,也称为对称加密。
这里介绍其中一种对称加密算法 -- AES,采用唯一的key进行加密和解密
对称加密的优点:
算法公开,计算量小,加密速度快,加密效率高。
缺点:
双方使用相同的钥匙,安全性得不到保证。
使用对称加密需要注意的是秘钥的保密性,并且秘钥要求定期更换
写一个NSString分类,NSString+wxAES.h:
NSString+wxAES.m:
使用示例:
打印结果为:
首先准备一份明文和秘钥:
var plaintText = 'aaaaaaaaaaaaaaaa'; // 明文
var keyStr = 'bbbbbbbbbbbbbbbb'; // 一般key为一个字符串
参看官网文档,AES方法是支持AES-128、AES-192和AES-256的,加密过程中使用哪种加密方式取决于传入key的类型,否则就会按照AES-256的方式加密。
CryptoJS supports AES-128, AES-192, and AES-256. It will pick the variant by the size of the key you pass in. If you use a passphrase, then it will generate a 256-bit key.
由于Java就是按照128bit给的,但是由于是一个字符串,需要先在前端将其转为128bit的才行。
最开始以为使用CryptoJS.enc.Hex.parse就可以正确地将其转为128bit的key。但是不然...
经过多次尝试,需要使用CryptoJS.enc.Utf8.parse方法才可以将key转为128bit的。好吧,既然说了是多次尝试,那么就不知道原因了,后期再对其进行更深入的研究。
// 字符串类型的key用之前需要用uft8先parse一下才能用
var key = CryptoJS.enc.Utf8.parse(keyStr);
由于后端使用的是PKCS5Padding,但是在使用CryptoJS的时候发现根本没有这个偏移,查询后发现PKCS5Padding和PKCS7Padding是一样的东东,使用时默认就是按照PKCS7Padding进行偏移的。
// 加密
var encryptedData = CryptoJS.AES.encrypt(plaintText, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
由于CryptoJS生成的密文是一个对象,如果直接将其转为字符串是一个Base64编码过的,在encryptedData.ciphertext上的属性转为字符串才是后端需要的格式。
var encryptedBase64Str = encryptedData.toString();
// 输出:'RJcecVhTqCHHnlibzTypzuDvG8kjWC+ot8JuxWVdLgY=
console.log(encryptedBase64Str);
// 需要读取encryptedData上的ciphertext.toString()才能拿到跟Java一样的密文
var encryptedStr = encryptedData.ciphertext.toString();
// 输出:'44971e715853a821c79e589bcd3ca9cee0ef1bc923582fa8b7c26ec5655d2e06
console.log(encryptedStr);
由于加密后的密文为128位的字符串,那么解密时,需要将其转为Base64编码的格式。
那么就需要先使用方法CryptoJS.enc.Hex.parse转为十六进制,再使用CryptoJS.enc.Base64.stringify将其变为Base64编码的字符串,此时才可以传入CryptoJS.AES.decrypt方法中对其进行解密。
// 拿到字符串类型的密文需要先将其用Hex方法parse一下
var encryptedHexStr = CryptoJS.enc.Hex.parse(encryptedStr);
// 将密文转为Base64的字符串
// 只有Base64类型的字符串密文才能对其进行解密
var encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
使用转为Base64编码后的字符串即可传入CryptoJS.AES.decrypt方法中进行解密操作。
// 解密
var decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
经过CryptoJS解密后,依然是一个对象,将其变成明文就需要按照Utf8格式转为字符串。
// 解密后,需要按照Utf8的方式将明文转位字符串
var decryptedStr = decryptedData.toString(CryptoJS.enc.Utf8);
console.log(decryptedStr); // 'aaaaaaaaaaaaaaaa'
AES, 高级加密标准, 是采用区块加密的一种标准, 又称Rijndael加密法. 严格上来讲, AES和Rijndael又不是完全一样, AES的区块长度固定为128比特, 秘钥长度可以是128, 192或者256. Rijndael加密法可以支持更大范围的区块和密钥长度, Rijndael使用的密钥和区块长度均可以是128,192或256比特. AES是对称加密最流行的算法之一.
我们不去讨论具体的AES的实现, 因为其中要运用到大量的高等数学知识, 单纯的了解AES流程其实也没什么意义(没有数学基础难以理解), 所以我们今天着重来总结一些使用过程中的小点.
当然了分组密码的加密模式不仅仅是ECB和CBC这两种, 其他的我们暂不涉及.
上面说的AES是一种区块加密的标准, 那加密模式其实可以理解为处理不同区块的方式和联系.
ECB可以看做最简单的模式, 需要加密的数据按照区块的大小分为N个块, 并对每个块独立的进行加密
此种方法的缺点在于同样的明文块会被加密成相同的密文块, 因此, 在某些场合, 这种方法不能提供严格的数据保密性. 通过下面图示例子大家就很容易明白了
我们的项目中使用的就是这种模式, 在CBC模式中, 每个明文块与前一个块的加密结果进行异或后, 在进行加密, 所以每个块的加密都依赖前面块的加密结果的, 同时为了保证第一个块的加密, 在第一个块中需要引入初始化向量iv.
CBC是最常用的模式. 他的缺点是加密过程只能是串行的, 无法并行, 因为每个块的加密要依赖到前一个块的加密结果, 同时在加密的时候明文中的细微改变, 会导致后面所有的密文块都发生变化. 但此种模式也是有优点的, 在解密的过程中, 每个块的解密依赖上一个块的加密结果, 所以我们要解密一个块的时候, 只需要把他前面一个块也一起读取, 就可以完成本块的解密, 所以这个过程是可以并行操作的.
AES加密每个块blockSize是128比特, 那如果我们要加密的数据不是128比特的倍数, 就会存在最后一个分块不足128比特, 那这个块怎么处理, 就用到了填充模式. 下面是常用的填充模式.
PKCS7可用于填充的块大小为1-255比特, 填充方式也很容易理解, 使用需填充长度的数值paddingSize 所表示的ASCII码 paddingChar = chr(paddingSize)对数据进行冗余填充. (后面有解释)
PKCS5只能用来填充8字节的块
我们以AES(128)为例, 数据块长度为128比特, 16字节, 使用PKCS7填充时, 填充长度为1-16. 注意, 当加密长度是16整数倍时, 反而填充长度是最大的, 要填充16字节. 原因是 "PKCS7" 拆包时会按协议取最后一个字节所表征的数值长度作为数据填充长度, 如果因真实数据长度恰好为16的整数倍而不进行填充, 则拆包时会导致真实数据丢失.
举一个blockSize为8字节的例子
第二个块中不足8字节, 差4个字节, 所以用4个4来填充
严格来讲 PKCS5不能用于AES, 因为AES最小是128比特(16字节), 只有在使用DES此类blockSize为64比特算法时, 考虑使用PKCS5
我们的项目最开始加解密库使用了CryptoSwift, 后来发现有性能问题, 就改为使用IDZSwiftCommonCrypto.
这里咱们结合项目中边下边播边解密来提一个点, 具体的可以参考之前写的 边下边播的总结 . 因为播放器支持拖动, 所以我们在拖拽到一个点, 去网络拉取对应数据时, 应做好range的修正, 一般我们都会以range的start和end为基准, 向前后找到包含这个range的所有块范围. 打比方说我们需要的range时10-20, 这是我们应该修正range为0-31, 因为起点10在0-15中, 20 在16-31中. 这是常规的range修正.(第一步 找16倍数点).
但是在实际中, 我们请求一段数据时, 还涉及到解密器的初始化问题, 如果我们是请求的0-31的数据, 因为是从0开始, 所以我们的解密器只需要用key和初始的iv来进行初始化, 那如果经过了第一步的基本range修正后, 我们请求的数据不是从0开始, 那我们则还需要继续往前读取16个字节的数据, 举个例子, 经过第一步修正后的range为16-31, 那我们应该再往前读取16字节, 应该是要0-31 这32个字节数据, 拿到数据后,使用前16个字节(上一个块的密文)当做iv来初始化解密器.
还有一个要注意的点是, 数据解密的过程中, 还有可能会吞掉后面16个字节的数据, 我暂时没看源码, 不知道具体因为什么, 所以保险起见, 我们的range最好是再向后读取6个字节.
感谢阅读
参考资料
1. php的aes算法,加密时会存在空格,0,\0等方式进行补长,所以解密后需要进行trim操作,才能得到原数据串
2. aes加密后进行base64_encode,但是解密时,直接用aes进行解密,不需要先base64_decode.【这个操作很骚气】
function _decryptData($data,$password, $iv){
$decryptData=openssl_decrypt($data, 'aes-128-cbc', $password, OPENSSL_ZERO_PADDING, $iv);
$data =json_decode(trim($decryptData), true);
return $data;
}
function encryptData($data, $password, $iv){
$data = json_encode($data);//$data是一个数组,如果是字符串,请忽略此句.
$result = base64_encode(openssl_encrypt($data, 'aes-128-cbc', $password, OPENSSL_RAW_DATA, $iv));
return $result;
}
github地址: AES128Demo
demo主要功能及需要阐述的基本情况:
一、功能:
与后台、接口进行信息传递时文本的加密解密操作。采用的技术是AES128加密
二、基本情况:
1、base64加密用的是系统自带的方法。
2、填充方式是采用的是无填充 No padding 因为填充的话,后台语言不同填充方式不同,容易冲突
3、使用的时候请把.m文件打包成静态库
4、参考文件: AES128原理 AES128第三方加密解密
5、对demo有疑问或者有异议的感谢指教。谢谢大家
三、以下是UI效果图