背景
最近在研究shadowsocks的源码,看到加密解密这块时,觉得自己从来没有接触过这块,想通过java来实现aes cfb/ofb加解密。
这里顺便提一下,shadowssocks网络加密、解密通信的过程,看图。不懂请留言。
基础知识
密码学常用术语
明文: 待加密数据。
密文: 明文经过加密后数据。
加密: 将明文转换为密文的过程。
加密算法: 将明文转换为密文的转换算法。
加密密钥: 通过加密算法进行加密操作的密钥。
解密: 将密文转换为明文的过程。
解密算法: 将密文转换为明文的转换算法。
解密密钥: 通过解密短发进行解密操作的密钥。
密码学分类
按密钥体制划分
a. 对称密码体制:也叫单钥或私钥密码体制,加密过程与解密过程使用同一套密钥。对应的算法就是对称加密算法,例如 DES , AES 。
b. 非对称密码体制:也叫双钥或公钥密码体制,加密过程与解密过程使用不同的密钥。对应的算法就是非对称加密算法,例如 RSA 。
按明文处理方式划分
a. 流密码:也称为序列密码,加密时每次加密一位或者一个字节的明文。例如 RC4 算法。
b. 分组密码:加密时将明文分成固定长度的组,用同一个密钥和算法对每一组进行加密输出也是固定长度的明文。当最后一组大小不满足指定的分组大小时,
有两种处理模式:
无填充模式,直接对剩余数据进行加密,此组加密后大小与剩余数据有关;
有填充模式,对于不满足指定长度分组的进行数据填充;如果恰巧最后一组数据与指定分组大小相同,那么直接添加一个指定大小的分组;填充的最后一个字节记录了填充的字节数。
分组密码工作模式简介
1 .电子密码本模–ECB
将明文的各个分组独立的使用相同的密钥进行加密,这种方式加密时各分组的加密独立进行互不干涉,因而可并行进行。同样因为各分组独立加密的缘故,相同的明文分组加密之后具有相同的密文。该模式容易暴露明文分组的统计规律和结构特征。不能防范替换攻击。
其实照实现来看, ECB 的过程只是把明文进行分组,然后分别加密,最后串在一起的过程。当消息长度超过一个分组时,不建议使用该模式。在每个分组中增加随机位 ( 如 128 位分组中 96 位为有效明文, 32 位的随机数 ) 则可稍微提高其安全性 , 但这样无疑造成了加密过程中数据的扩张。
优点 :
1.简单;
2.有利于并行计算;
3.误差不会被传送;
缺点 :
1.不能隐藏明文的模式;
2.可能对明文进行主动攻击;
2.密码分组链接模 — CBC
需要一个初始化向量 IV ,第一组明文与初始化向量进行异或运算后再加密,以后的每组明文都与前一组的密文进行异或运算后再加密。 IV 不需要保密,它可以明文形式与密文一起传送。
优点:
1.不容易主动攻击,安全性好于ECB,适合传输长度长的报文,是SSL、IPSec的标准。
缺点:
1.不利于并行计算;
2.误差传递;
3.需要初始化向量IV
3.密文反馈模式–CFB
需要一个初始化向量IV ,加密后与第一个分组明文进行异或运算产生第一组密文,然后对第一组密文加密后再与第二组明文进行异或运算缠身第二组密文,一次类推,直到加密完毕。
优点:
1.隐藏了明文模式;
2.分组密码转化为流模式;
3.可以及时加密传送小于分组的数据;
缺点 :
1.不利于并行计算;
2.误差传送:一个明文单元损坏影响多个单元;
3.唯一的IV;
4.输出反馈模式–OFB
需要一个初始化向量IV ,加密后得到第一次加密数据,此加密数据与第一个分组明文进行异或运算产生第一组密文,然后对第一次加密数据进行第二次加密,得到第二次加密数据,第二次加密数据再与第二组明文进行异或运算产生第二组密文,一次类推,直到加密完毕。
优点 :
1.隐藏了明文模式;
2.分组密码转化为流模式;
3.可以及时加密传送小于分组的数据;
缺点 :
1.不利于并行计算;
对明文的主动攻击是可能的;
误差传送:一个明文单元损坏影响多个单元;
5.计数器模式–CTR
使用计数器,计数器初始值加密后与第一组明文进行异或运算产生第一组密文,
计数器增加,然后,加密后与下一组明文进行异或运算产生下一组密文,以此类推,直到加密完毕
优点 :
可并行计算;
安全性至少与CBC 模式一样好;
加密与解仅涉及密码算法的加密;
缺点 :
没有错误传播,不易确保数据完整性;
分组密码填充方式简介
PKCS5 : 填充字符串由一个值为 5 的字节序列组成,每个字节填充该字节序列的长度。 明确定义 Block 的大小是 8 位
PKCS7 : 填充字符串由一个值为 7 的字节序列组成,每个字节填充该字节序列的长度。 对于块的大小是不确定的,可以在 1-255 之间
ISO10126: 填充字符串由一个字节序列组成,此字节序列的最后一个字节填充字节序列的长度,其余字节填充随机数据。
Java实现AES加解密
对于cfb和ofb的加密方法,在初始化的时候,需要指定向量和密匙。在java里,SecretKeySpec表示密匙,IvParameterSpec表示向量。注意,这两个类的构造函数的参数是字节数组。下面是一个简单封装的加解密类的实现。
import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class SecretAES extends AbstractSecret { private Cipher encrypt; private Cipher decrypt; public SecretAES(String cipherType, byte[] key, byte[] iv) { this.cipherType = cipherType; this.keyBytes = key; this.ivBytes = iv; SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); try{ this.encrypt = Cipher.getInstance(this.cipherType); this.encrypt.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); this.decrypt = Cipher.getInstance(this.cipherType); this.decrypt.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); } catch(Exception e){ e.printStackTrace(); } } @Override public byte[] encode(byte[] data) { try { return this.encrypt.doFinal(data); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } @Override public byte[] decode(byte[] data) { try { return this.decrypt.doFinal(data); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (BadPaddingException e) { e.printStackTrace(); } return null; } }
我们在做具体加解密的时候,一般会把一个段字符串作为密匙。下面的代码就是通过一个字符串,指定密匙和向量的长度(这个长度表示生产字节数组的长度),来产生密匙的字节数组和向量的字节数组。
public static byte[][] getKeyAndIV(String password, int keyLen, int ivLen){ byte[] passwordBytes = password.getBytes(); ByteArrayOutputStream arrayOutput = new ByteArrayOutputStream(); int i = 0; while(arrayOutput.size() < (keyLen + ivLen)){ if(i > 0){ byte[] originalBytes = arrayOutput.toByteArray(); byte[] newBytes = Arrays.copyOfRange(originalBytes, (i - 1) * 16, 16 * i); ByteArrayOutputStream arrayOutput2 = new ByteArrayOutputStream(); try { arrayOutput2.write(newBytes); arrayOutput2.write(passwordBytes); } catch (IOException e) { e.printStackTrace(); } byte[] passwordMD5 = DigestUtils.encodeMD5(arrayOutput2.toByteArray()); try { arrayOutput.write(passwordMD5); } catch (IOException e) { e.printStackTrace(); } } else{ byte[] passwordMD5 = DigestUtils.encodeMD5(passwordBytes); try { arrayOutput.write(passwordMD5); } catch (IOException e) { e.printStackTrace(); } } i++; } byte[] bytes = arrayOutput.toByteArray(); byte[] keyBytes = Arrays.copyOfRange(bytes, 0, keyLen); byte[] viBytes = Arrays.copyOfRange(bytes, keyLen, keyLen + ivLen); return new byte[][]{keyBytes, viBytes}; }
密匙的长度可以是16,24,32,向量的长度必须是16。
下面是加密解密的例子:
public static void run_test(SecretAES aes){ System.out.println(aes.getCipherType()); int BLOCK_SIZE = 16384; int rounds = 1 * 1024; String plain = RandomStringUtils.random(BLOCK_SIZE * rounds, "1234567890qazwsxedcrfvtgbyhnujmkiolp"); byte[] data = plain.getBytes(); long start = System.currentTimeMillis(); byte[] encodeData = aes.encode(data); assert encodeData.length == data.length; byte[] retData = aes.decode(encodeData); long end = System.currentTimeMillis(); System.out.println(data.length / (end - start) * 1000+ " bytes/ms"); assert plain.equals(new String(retData)); } public static void test_AES_OFB_NoPadding_16_16(){ String cipherType = "AES/OFB/NoPadding"; int keyLen = 16; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); } public static void test_AES_OFB_NoPadding_24_16(){ String cipherType = "AES/OFB/NoPadding"; int keyLen = 24; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); } public static void test_AES_OFB_NoPadding_32_16(){ String cipherType = "AES/OFB/NoPadding"; int keyLen = 16; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); } public static void test_AES_CFB_NoPadding_16_16(){ String cipherType = "AES/CFB/NoPadding"; int keyLen = 16; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); } public static void test_AES_CFB_NoPadding_24_16(){ String cipherType = "AES/CFB/NoPadding"; int keyLen = 24; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); } public static void test_AES_CFB_NoPadding_32_16(){ String cipherType = "AES/CFB/NoPadding"; int keyLen = 32; int viLen = 16; byte[][] keyAndVi = Encryptor.getKeyAndIV(StringUtils.repeat("k", 24), keyLen, viLen); SecretAES aes = new SecretAES(cipherType, keyAndVi[0], keyAndVi[1]); run_test(aes); }
还可以加密解密文件:
public static void jiemi() throws IOException { byte[][] keyAndVi = Encryptor.getKeyAndIV("123456", 32, 16); SecretAES aes = new SecretAES("AES/CFB/NoPadding", keyAndVi[0], keyAndVi[1]); File readFile = new File("C:/Users/1/Desktop/Backbone2.pptx"); FileInputStream fileIn = new FileInputStream(readFile); File writeFile = new File("C:/Users/1/Desktop/Backbone3.pptx"); FileOutputStream fileOut = new FileOutputStream(writeFile); byte[] readBytes = new byte[2048]; int readedCount = -1; while ((readedCount = fileIn.read(readBytes)) != -1) { byte[] encodeBytes = aes.decode(Arrays.copyOf(readBytes, readedCount)); fileOut.write(encodeBytes); } fileOut.flush(); fileIn.close(); fileOut.close(); }
如果使用24,32长度的密匙时,需要去官网下载Java Cryptography Extension (JCE) ,一定要和jre(jdk)的版本对应。这个是因为美国的对高科技的东西有出口限制。
这里贴一个java8的jce下载地址。其他版本的java,自己google吧。
http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
希望对君有所帮助。
参考文献:
http://aub.iteye.com/blog/1129339