浅谈之前,我们不妨先了解几个知识点:加密技术、哈希摘要。
加密技术 分为 对称加密 与 非对称加密 两类。
对称加密即加密密钥与解密密钥相同,形象的描述为,你有你家房门的钥匙,你既可以用它锁住房门,也可以用它打开房门。常见的算法有DES、AES。
而非对称加密则采用两把钥匙,公钥和私钥。使用公钥加密的信息只能通过私钥解密,反之,使用私钥加密的信息也只能通过公钥解密。这分别对应两个过程:数据加密传输与数字签名。本文仅谈论数据的加密传输。常见的算法有RSA。
哈希摘要 即指通过散列函数为一段数据生成固定长度的摘要,而不论数据的大小。生成的摘要一般难以推导出原数据。常见的散列函数有MD5、SHA-1、SHA-2、SHA-3(Keccak)。
一、数据加密传输策略的设计
从加密技术中,我们可以得到两种简单的加密传输的方式:使用对称加密或使用非对称加密。其各有优劣。
对称加密 | 非对称加密 | |
---|---|---|
加密效率 | 计算量小,加密效率高 | 计算复杂,加密效率低 |
密钥传输 | 不宜明文传输,以免泄漏密钥造成数据泄漏 | 可传输公钥,使用私钥解密以获得数据 |
因此,结合使用对称加密与非对称加密,接收方先用发送方的非对称加密的公钥加密自己的对称加密的密钥,发送方再用其私钥解密出对称加密的密钥,双方通过对称加密的密钥传输数据,才是更好的选择。这里我们以客户端(数据接收方)和服务端(数据发送方)为例:
- 服务端使用非对称加密生成公钥(publicKey)和私钥(privateKey),客户端使用对称加密生成密钥(key)
- 客户端获取publicKey,使用其加密key,得到key的密文(keyCiphertext)
- 服务端使用privateKey解密keyCiphertext得到key
- 服务端使用key加密数据(data)得到数据的密文(dataCiphertext)
- 客户端获取dataCiphertext,使用key解密得到data
至此,就完成了数据发送方(服务端)向数据接收方(客户端)的加密数据传输
二、密码的加密存储策略
不论是应用程序亦或是网页,都可能涉及到用户的登录,其背后也必然牵扯到数据库对用户密码的存储与验证。直接存储密码确实简单易行,但若发生数据泄漏则将可能产生严重后果,因此用户密码的存储也应当加密。
结合哈希摘要,我们不难想到一种方法:
- 将用户密码通过哈希函数加密生成摘要,并存储
- 用户登录时,对输入的密码使用相同的哈希函数加密生成摘要
- 对比两次摘要,即可验证密码
但实际上,若用户密码设计的简单(如个人生日等),很容易通过反查询(即先对多个数据一 一对应的生成摘要,再依次对比摘要以确定原数据)的方式破解用户密码。因此,加盐值(salt值)的概念应运而生。所谓加盐值,指的是一串随机生成的数据,将其以任意方式添加到原来的数据中,以提升数据复杂度的一种方法,就像是往一杯干净的水加粗盐而变得浑浊一样。因此,新的密码加密存储方式我们可以这样设计:
- 随机生成一串字符串(即盐值),将其以任意方式添加到用户密码中,生成摘要,同时存储摘要和盐值
- 用户登录时,取出盐值,将其以相同的方式添加到输入的密码中,生成摘要
- 对比两次摘要,即可验证密码
三、示例代码
代码中所采用的对称加密算法为AES,采用的非对称加密算法为RSA。数据的发送方为服务器端Java程序,数据的接收方为Android应用,所采用的字节数据编码方式为Base64。
数据发送方
//-----------------AES------------------
private byte[] aesKeyBytes = null;// AES密钥字节数组形式
private Key aesKey = null;// AES密钥
/**
* 获得使用Base64编码的未加密AES密钥字符串.
*
* @return AES密钥字符串形式
*/
public String getAESKeyNoEncrypt() {
return new String(Base64.getEncoder().encode(aesKeyBytes), StandardCharsets.UTF_8);
}
/**
* 使用AES密钥加密数据.
*
* @param data 待加密的数据
* @return 加密后的密文,使用Base64编码
*/
public String aesEncryptData(String data) {
try {
if (aesKey == null) {
throw new NullPointerException("AES密钥为空");
}
if (data == null) {
throw new NullPointerException("待加密数据为空");
}
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, aesKey);
return new String(Base64.getEncoder().encode(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e("AES加密数据失败", e);
return null;
}
}
/**
* 使用AES密钥解密数据.
*
* @param ciphertext 待解密的使用Base64编码的密文
* @return 解密后的数据
*/
public String aesDecryptData(String ciphertext) {
try {
if (aesKey == null) {
throw new NullPointerException("AES密钥为空");
}
if (ciphertext == null) {
throw new NullPointerException("待解密密文为空");
}
byte[] decodeCiphertextBytes = Base64.getDecoder().decode(ciphertext.getBytes(StandardCharsets.UTF_8));
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, aesKey);
return new String(cipher.doFinal(decodeCiphertextBytes), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e("AES解密数据失败", e);
return null;
}
}
//-----------------RSA------------------
// RSA允许你选择公钥的大小。512位的密钥被视为不安全的;768位的密钥
// 不用担心受到除了国家安全管理(NSA)外的其他事物的危害;1024位的
// 密钥几乎是安全的。
// ————来源于百度百科(RSA算法)
private static final int rsaKeySize = 1024;// RSA密钥长度
private static PrivateKey privateKey = null;// RSA私钥
private static String publicKeyStr = null;// RSA公钥字符串形式
/**
* 生成RSA密钥对.
*/
public static void generateRSAKeyPair() {
try {
KeyPairGenerator rsa = KeyPairGenerator.getInstance("RSA");
rsa.initialize(rsaKeySize);
KeyPair keyPair = rsa.generateKeyPair();
privateKey = keyPair.getPrivate();
byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
publicKeyStr = new String(Base64.getEncoder().encode(publicKeyBytes), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e("生成RSA密钥对失败", e);
}
}
/**
* 获得RSA公钥的字符串形式,使用Base64编码.
*
* @return RSA公钥字符串
*/
public static String getRSAPublicKeyStr() {
return publicKeyStr;
}
/**
* RSA私钥解密含密钥字符串以构造AES密钥.
*
* @param aesKeyStr 以Base64编码的RSA公钥加密后的AES密钥字符串
*/
private void decryptAESKey(String aesKeyStr) {
try {
byte[] decodeAESKeyBytes = Base64.getDecoder().decode(aesKeyStr);
int maxLength = rsaKeySize / 8;
int mod = decodeAESKeyBytes.length % maxLength;
int groupNum = decodeAESKeyBytes.length / maxLength;
if (mod != 0) {
groupNum++;
}
byte[][] dataSrc = new byte[groupNum][0];
for (int i = 0, start = 0; i < groupNum; i++, start += maxLength) {
if (i != groupNum - 1 || mod == 0) {
dataSrc[i] = Arrays.copyOfRange(decodeAESKeyBytes, start, start + maxLength);
} else {
dataSrc[i] = Arrays.copyOfRange(decodeAESKeyBytes, start, start + mod);
}
}
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[][] cache = new byte[dataSrc.length][0];
aesKeyBytes = new byte[0];
for (int i = 0, start = 0; i < dataSrc.length; i++) {
cache[i] = cipher.doFinal(dataSrc[i]);
aesKeyBytes = Arrays.copyOf(aesKeyBytes, aesKeyBytes.length + cache[i].length);
System.arraycopy(cache[i], 0, aesKeyBytes, start, cache[i].length);
start = cache[i].length;
}
aesKey = new SecretKeySpec(aesKeyBytes, "AES");
} catch (Exception e) {
myLog.e("解密数据获取AES密钥失败", e);
}
}
数据接收方
//******************加密传输*******************
private static String encryptAESKey = null;// 使用服务器端RSA公钥加密的AES密钥
//====================AES=====================
private static Key aesKey = null;// AES密钥
private static byte[] aesKeyBytes = null;// AES密钥字节数组
/**
* 生成AES密钥.
*/
public static void generateAESKey() {
try {
KeyGenerator aes = KeyGenerator.getInstance("AES");
aes.init(256);
aesKeyBytes = aes.generateKey().getEncoded();
aesKey = new SecretKeySpec(aesKeyBytes, "AES");
} catch (Exception e) {
myLog.e("生成AES密钥失败", e);
}
}
/**
* 使用AES密钥加密数据.
*
* @param data 待加密的数据
* @return 加密后的密文,使用Base64编码
*/
@SuppressLint("GetInstance")
private static String aesEncryptData(String data) {
try {
if (aesKey == null) {
throw new NullPointerException("AES密钥为空");
}
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, aesKey);
return new String(Base64.encode(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)), Base64.NO_WRAP), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e( "AES加密数据失败", e);
return null;
}
}
/**
* 使用AES密钥解密数据.
*
* @param ciphertext 待解密的使用Base64编码的密文
* @return 解密后的数据
*/
@SuppressLint("GetInstance")
public static String aesDecryptData(String ciphertext) {
try {
if (aesKey == null) {
throw new NullPointerException("AES密钥为空");
}
if (ciphertext == null) {
throw new NullPointerException("密文为空");
}
byte[] decodeCiphertextBytes = Base64.decode(ciphertext.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, aesKey);
return new String(cipher.doFinal(decodeCiphertextBytes), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e("AES解密数据失败", e);
return null;
}
}
//====================RSA=====================
private static PublicKey publicKey = null;// RSA公钥
/**
* 解码RSA公钥字符串以构造RSA公钥.
*/
private static void revertRSAPublicKey(String rsaPublicKeyStr) {
try {
byte[] rsaPublicKeyBytes = Base64.decode(rsaPublicKeyStr.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
publicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(rsaPublicKeyBytes));
encryptAESKey = getEncryptAESKey();
} catch (Exception e) {
myLog.e("还原RSA公钥失败", e);
}
}
/**
* 使用RSA公钥加密AES密钥.
*
* @return 以Base64编码的加密后的AES密钥字符串
*/
private static String getEncryptAESKey() {
try {
if (publicKey == null) {
throw new NullPointerException("RSA公钥为空");
}
if (aesKeyBytes == null) {
throw new NullPointerException("AES密钥为空");
}
int rsaKeySize = 1024;// RSA密钥长度
int maxLength = rsaKeySize / 8 - 11;
int mod = aesKeyBytes.length % maxLength;
int groupNum = aesKeyBytes.length / maxLength;
if (mod != 0) {
groupNum++;
}
byte[][] dataSrc = new byte[groupNum][0];
for (int i = 0, start = 0; i < groupNum; i++, start += maxLength) {
if (i != groupNum - 1 || mod == 0) {
dataSrc[i] = Arrays.copyOfRange(aesKeyBytes, start, start + maxLength);
} else {
dataSrc[i] = Arrays.copyOfRange(aesKeyBytes, start, start + mod);
}
}
byte[][] cache = new byte[dataSrc.length][0];
byte[] result = new byte[0];
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
for (int i = 0, start = 0; i < dataSrc.length; i++) {
cache[i] = cipher.doFinal(dataSrc[i]);
result = Arrays.copyOf(result, result.length + cache[i].length);
System.arraycopy(cache[i], 0, result, start, cache[i].length);
start = cache[i].length;
}
return new String(Base64.encode(result, Base64.NO_WRAP), StandardCharsets.UTF_8);
} catch (Exception e) {
myLog.e("使用RSA公钥加密AES密钥失败", e);
return null;
}
}
能力所限,不足之处烦请多多指教!