在Android直播开发之旅(14):使用RC4算法加解密音视频流一文中,我们了解了一种面向字节操作的对称加密算法–RC4,该算法实现简洁、加密速度快且安全性较高(
密钥可变长,为1~256字节
),通常用于加密流数据。本节将介绍另外一种对称加密–AES,通过分析它的实现原理并结合实战项目来了解其在文件加密方面的应用。
高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。AES 是一个迭代的、对称密钥分组的密码,AES算法加密强度大,执行效率高,使用简单。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用,是目前对称密钥加密中最流行的算法之一。AES的分组区块长度
固定为128位,密钥长度
则可以是128,192或256位。密钥长度越长,加密等级越高,但是效率会有所降低。AES算法常见参数配置:
参数类型 | AES128 | AES192 | AES256 |
---|---|---|---|
密码长度(bits) | 128 | 192 | 256 |
明文分组长度(bits) | 128 | 128 | 128 |
加密轮数 | 10 | 12 | 14 |
轮密钥长度(bits) | 128 | 128 | 128 |
扩展密钥长度(words) | 44 | 52 | 60 |
AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块(分组),每一个明文块长度128bit=16字节。这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。本文将以AES128为例,讲解AES的实现过程。
AES加密过程是在一个4×4的字节(即16字节)矩阵
上运作,这个矩阵又称为“体(state)”,其初值就是一个明文区块(矩阵中一个元素大小就是明文区块中的一个Byte)。加密时,各轮AES加密分为初始轮(Initial Round,1次)、普通轮(Rounds,N次)和最终轮(Final Rounds,1次),并且不同阶段的Round由多同的处理步骤,整个AES加密过程如下图所示(以AES128为例
):
其中,SubBytes
表示字节代替变换、ShiftRows
表示行移位变换、MixColumns
表示列混合变换、AddRoundKey
表示加轮密钥变换。
所谓字节替换,是指将明文块(16字节组成的4x4矩阵)中的每一个字节都替换成另外一个字节,其中替换的依据是一个被称为S盒(Subtitution Box)的16x16大小的二维常量数组。比如明文块当中a[2, 2] = 19
,那么经过被S盒替换后输出的目标b[2, 2] = S[1][9]=2D
,即a[2,2]的值被替换成S矩阵中行下标为1、列下标为9的元素。
所谓行移位,是指对进行字节替换后的矩阵进行行移位,其规则如下:
所谓列混合,是指将经过行移位得到的矩阵将其每一列和一个名为修补矩阵(fixed matrix)
的二维常量数组做矩阵相乘,得到对应的输出列。举例如下:
所谓加轮密钥,是指将输入的数组的每一个字节a[i,j]
与密钥对应位置的字节k[i,j]
进行异或依次,从而得到处理后的输出值b[i,j]
。这时唯一利用到密钥的一步,且128bit的密钥也同样被排列成4x4的矩阵。需要注意的是,由于加密的每一轮所用到的密钥并不是相同的,而每一轮的密钥通过**扩展密钥(KeyExpansions)**产生,扩展密钥算法计算过程为:
注:关于AES的加解密过程,可参照《A高级加密标准AES 徐娟》和《AES加密算法动画》来理解。另外,还有一个知识点就是
AES
的工作模式,AES加密算法提供了五种不同的工作模式:ECB、CBC、CTR、CFB、OFB,其中,ECB模式为默认工作模式,这种模式的每一个明文块的加密都是完全独立的,互不干涉的,因此实现起来比较简单且有利于并行计划,但是安全性稍微差一点。如果希望提高安全性,可以采用CBC模式,该模式在每一个明文块加密前会让明文块和一个值先做异或操作,但是缺点是无法并行计算,且由于增加了异或操作,性能上不如ECB模式。
public static byte[] getAutoCreateAESKey() throws Exception {
// 实例化一个AES加密算法的密钥生成器
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM_AES);
// 初始化密钥生成器,指定密钥位数为128位
keyGenerator.init(AES_KEY_LEN, new SecureRandom());
// 生成一个密钥
SecretKey secretKey = keyGenerator.generateKey();
return secretKey.getEncoded();
}
/** AES加密
*
* @param sourceFile 源文件
* @param encryptFile 加密文件
* @param password 密钥,128bit
* @throws Exception 抛出异常
*/
public static void aesEncryptFile(String sourceFile, String encryptFile, byte[] password) throws Exception {
// 创建AES密钥
SecretKeySpec key = new SecretKeySpec(password, "AES");
// 创建加密引擎(CBC模式)。Cipher类支持DES,DES3,AES和RSA加加密
// AES:算法名称
// CBC:工作模式
// PKCS5Padding:明文块不满足128bits时填充方式(默认),即在明文块末尾补足相应数量的字符,
// 且每个字节的值等于缺少的字符数。另外一种方式是ISO10126Padding,除最后一个字符值等于少的字符数
// 其他字符填充随机数。
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化加密器
cipher.init(Cipher.ENCRYPT_MODE, key,
new IvParameterSpec(new byte[cipher.getBlockSize()]));
// 原始文件流
FileInputStream inputStream = new FileInputStream(sourceFile);
// 加密文件流
FileOutputStream outputStream = new FileOutputStream(encryptFile);
// 以加密流写入文件
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
byte[] tmpArray = new byte[1024];
int len;
while((len = cipherInputStream.read(tmpArray)) != -1) {
outputStream.write(tmpArray, 0, len);
outputStream.flush();
}
cipherInputStream.close();
inputStream.close();
outputStream.close();
}
/** AES解密
*
* @param encryptFile 加密文件
* @param decryptFile 解密文件
* @param password 密钥,128bit
* @throws Exception 抛出异常
*/
public static void aesDecryptFile(String encryptFile, String decryptFile, byte[] password) throws Exception {
// 创建AES密钥,即根据一个字节数组构造一个SecreteKey
// 而这个SecreteKey是符合指定加密算法密钥规范
SecretKeySpec key = new SecretKeySpec(password, "AES");
// 创建解密引擎(CBC模式)
// Cipher类支持DES,DES3,AES和RSA加解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 初始化解密器
cipher.init(Cipher.DECRYPT_MODE, key,
new IvParameterSpec(new byte[cipher.getBlockSize()]));
// 加密文件流
FileInputStream fileInputStream = new FileInputStream(encryptFile);
// 解密文件流
FileOutputStream fileOutputStream = new FileOutputStream(decryptFile);
// 以解密流写出文件
CipherOutputStream cipherOutputStream =
new CipherOutputStream(fileOutputStream, cipher);
byte[] buffer = new byte[1024];
int len;
while((len = fileInputStream.read(buffer)) >= 0) {
cipherOutputStream.write(buffer, 0, len);
}
cipherOutputStream.close();
fileInputStream.close();
fileOutputStream.close();
}
AES、DES均为对称加密算法,它们的对比如下:
DES | AES | |
---|---|---|
安全性 | 密钥长度较短(56bits),不能抵抗密钥的穷举搜索攻击;轮密钥通过置换和循环移位进行扩展,存在弱密钥和半弱密钥 | 密钥长度至少128bits,安全性能超过3-DES;轮密钥通过非线性代换、线性混合和密钥加进行扩展 |
效率 | 快 | 快于3-DES |
灵活性 | 密钥长度不可变(56bits) | 密钥长度可变(128bits、192bits、256bits) |
复杂性 | 采用Feistel网络结构,每轮数据的变换不均匀,设计较为复杂 | 采用SP网络结构,每轮数据的变换均匀,设计简单 |
RSA加密算法是一种非对称加密算法
,该算法在公开密钥加密和电子商业中被广泛使用,只要其钥匙长度足够长(注:至少为500位长,推荐使用1024位
),世界上还没有任何可靠的攻击RSA算法的方式。所谓非对称加密算法,是指算法的加解密需要两个(一对)密钥,即公开密钥(publickey,简称公钥
)和私有密钥(privatekey,简称私钥
),其中公钥通常用于对数据进行加密且公开,而私钥用于对数据进行解密且私有。
RSA算法基于一个十分简单的数论事实:将两个大素数相乘十分容易,但是想要对其乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥
。在RSA算法加解密过程中,假设用户A生成一对RSA密钥,其中私钥由A自己保管,公钥将对外公开。用户B获得A公布的公钥后,它拿着公钥对数据进行加密然后传输给A,A获得B传输过来的加密数据后,就用自己保存的私钥进行解密,进而获得明文。整个加解密过程如下图所示:
1. RSA密钥对生成流程
(1)随意选择两个大的素数p、q
;
(2)计算两个大素数的乘机n = p * q
;
(3)φ(n) = (p-1)(q-1)
(欧拉函数);
(4)公钥e: 满足 1
(5)私钥d: 满足e*d mod φ(n)==1
;
(6)销毁p, q;
(7)公开发布n和公钥e;
2. 加、解密
(1)加密:假设明文m(m应为小于n的整数),m的e次幂取n的余数,得到密文c。
(2)解密:计算密文c的d次幂取n的余数,即得到明文m。
private static Map<Integer, String> keyMap = new HashMap<Integer, String>();
/**
* 随机生成密钥对
* @throws NoSuchAlgorithmException
*/
public static void genKeyPair() throws NoSuchAlgorithmException {
// KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
// 初始化密钥对生成器,密钥大小为96-1024位
keyPairGen.initialize(1024,new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 将公钥和私钥保存到Map
// 0表示公钥
// 1表示私钥
keyMap.put(0,Base64.getEncoder().encodeToString(publicKey.getEncoded()));
keyMap.put(1,Base64.getEncoder().encodeToString(privateKey.getEncoded()));
}
/**
* RSA公钥加密
*
* @param str
* 加密字符串
* @param publicKey
* 公钥
* @return 密文,经过Base64处理
* @throws Exception
* 加密过程中的异常信息
*/
public static String rsaEncrypt(String str, String publicKey ) throws Exception{
// 获取公钥
byte[] decoded = Base64.getDecoder().decode(publicKey);
RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(decoded));
//RSA加密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
return Base64.getEncoder().encodeToString((cipher.doFinal(str.getBytes("UTF-8")));
}
/**
* RSA私钥解密
*
* @param str
* 加密字符串
* @param privateKey
* 私钥
* @return 铭文
* @throws Exception
* 解密过程中的异常信息
*/
public static String rsaDecrypt(String str, String privateKey) throws Exception{
//64位解码加密后的字符串
byte[] inputByte = Base64.getDecoder().decode(str.getBytes("UTF-8"));
// base64编码的私钥
byte[] decoded = Base64.getDecoder().decode(privateKey);
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decoded));
//RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
return new String(cipher.doFinal(inputByte));
}
MD5信息摘要算法(MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息(文件)传输完整一致。MD5能把一个不管多长的文件都变成定长的且是不可逆的,它的实现原理大致为:MD5码以512位分组来处理输入的信息,且每一分组又被划分为16个32位子分组,经过了一系列的处理后,算法的输出由四个32位分组组成,将这四个32位分组级联后将生成一个128位散列值。
总体流程如下图所示:
代码实现:
/** 计算文件的md5值 */
public static String calculateMD5(File updateFile, int offset, int partSize) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
Log.e("FileUtils", "Exception while getting digest", e);
return null;
}
InputStream is;
try {
is = new FileInputStream(updateFile);
} catch (FileNotFoundException e) {
Log.e("FileUtils", "Exception while getting FileInputStream", e);
return null;
}
//DigestInputStream
final int buffSize = 8192;//单块大小
byte[] buffer = new byte[buffSize];
int read;
try {
if (offset > 0) {
is.skip(offset);
}
int byteCount = Math.min(buffSize, partSize), byteLen = 0;
while ((read = is.read(buffer, 0, byteCount)) > 0 && byteLen < partSize) {
digest.update(buffer, 0, read);
byteLen += read;
//检测最后一块,避免多读数据
if (byteLen + buffSize > partSize) {
byteCount = partSize - byteLen;
}
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 32 chars
output = String.format("%32s", output).replace(' ', '0');
return output;
} catch (IOException e) {
throw new RuntimeException("Unable to process file for MD5", e);
} finally {
try {
is.close();
} catch (IOException e) {
Log.e("FileUtils", "Exception on closing MD5 input stream", e);
}
}
}
Bse64是一种以64个可见字符集(又称“Base64编码表”
)对二进制数据进行编码的编码算法,这种编码不仅比较简短,同时也具有不可读性,即所编码的数据不会被人用肉眼所直接看到,因此常用于电子邮件加密
、数据简单加密
以及图片和文件网络传输
。Base64编码表如下图所示:
(1)Base64编码过程
在Base64编码过程中,每3个8位明文数据为一组(注:即每3个字节为一组
),取这3个字数据的ASCII码,然后以6位为一组组成1个新的数据。对于不足3字节的处理:(1)不足三字节后面填充0;(2)对于编码前的数据产生的6位,如果为0,则索引到的字符为‘A’;因不足3字节而填充的0,用’=’来替代。举例如下:
由此可知,“ABCD
”的base64编码为:“QUJDRA==
”。代码实现:
Base64.getEncoder().encode("ABCD".getBytes());
(2)Base64解码过程
Base64解码,即是base64编码的逆过程,即将base64编码数据根据编码表分别索引到编码值,然后每4个编码值一组组成一个24位的数据流,解码为3个字符(注:每个字符占8位
)。对于末尾位“=”的base64数据,最终取得的4字节数据,需要去掉“=”再进行转换。代码实现:
Base64.getDecoder().decode("QUJDRA==");