公钥加密 VS. 对称加密
(1) 如果使用公钥加密
- 所有客户端使用同一个公钥加密数据
- 加密后的结果可以不同,因为加密过程通常会引入随机性(填充/Nonce)
- 但服务器都能用同一个私钥解密,因为公钥-私钥是配对的
➡️ 即使不同客户端加密同样的数据,加密后的结果通常不同! 但服务器解密后,原始数据是一样的。
(2) 如果使用对称加密
- 每个客户端有自己的对称密钥,用来加密数据
- 加密后的结果不同,因为密钥不同
➡️ 不同客户端加密相同数据,结果一定不同! 因为每个客户端的密钥都不同。
为什么加密后的结果不同?
即使用同一个密钥,每次加密的结果也可能不同,原因是:
- 填充(Padding):某些加密算法会在数据后面填充不同的字节,使每次加密结果不同。
- 初始化向量(IV/Nonce):许多加密算法(如 AES CBC、GCM 模式)会使用一个随机数(IV),保证每次加密都不一样。
- 随机填充:如 RSA/OAEP 加密会自动加一些随机填充数据,使相同内容加密后结果不同。
📌 1 字节(Byte)是什么?
1 字节(Byte)= 8 位(bit)
- 位(bit):计算机的最小存储单位,可以是
0
或1
。 - 字节(Byte):8 个 bit 组成一个字节。
数据类型 | 占用字节数 | 示例 |
---|---|---|
字符 (ASCII) | 1 字节 | 'A' (0x41 ),'a' (0x61 ) |
Unicode 字符(UTF-8) | 1~4 字节 | '中' (E4 B8 AD ,3字节) |
整数 (int) | 4 字节 | 1234567890 |
浮点数 (float) | 4 字节 | 3.14 |
📌 1 字节如何存储数据?
示例:存储字母 'A'
,它的 ASCII 码是 65
,用二进制表示是 01000001
(1 字节)。
📌 存储 "ABC"
:
'A' → 01000001 (0x41)
'B' → 01000010 (0x42)
'C' → 01000011 (0x43)
总共 3 字节。
📌 存储 "你好"
(UTF-8 编码):
'你' → 11100100 10111000 10101001 (0xE4 B8 A9, 3字节)
'好' → 11100101 10111010 10101101 (0xE5 BA AD, 3字节)
总共 6 字节(UTF-8 里中文一般占 3 字节)。
1. 填充(Padding)
适用场景: 对称加密(如 AES)、非对称加密(如 RSA)
作用: 让数据长度符合加密算法的要求,增加安全性
🔹 为什么要填充?
许多加密算法要求数据长度是固定的,比如 AES 需要 16 字节的块(Block)。如果你的数据长度不是 16 的倍数,就必须填充一些额外的字节来让它对齐。
🔹 常见的填充方式
- PKCS#7 填充(用于 AES CBC/ECB)
- 如果数据长度不是 16 字节的倍数,就在结尾补上若干个相同数值的字节,这个数值等于填充的字节数。
- 示例(AES 需要 16 字节):
明文: "HELLO" (5 字节)
需要补 11 字节:\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B
这样填充后,变成:
"HELLO\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B\x0B"
- 解密时,程序知道最后的 \x0B 是填充数据,会自动去掉。
- OAEP 填充(用于 RSA)
- RSA 不能直接加密太长的数据,所以会用 填充+随机数 让数据变长,并增加安全性。
- OAEP(Optimal Asymmetric Encryption Padding)会用随机数填充数据,使得每次加密结果都不同,即使输入相同!
✅ 填充带来的变化: 即使你的原始数据相同,填充可能会引入不同的字节,导致加密结果不同。
🔹 1. PKCS#7 填充(最常用)
🟢 示例 1:短于 16 字节(补足)
假设你要加密 "HELLO"
,它只有 5 字节(少了 11 字节),需要填充:
明文: "HELLO" (5 字节)
填充: 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B (11 个字节)
最终数据(16 字节):
"H E L L O 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B"
0B = 11
(十六进制),表示填充了 11 个字节。
解密时,程序会检查最后一个字节的值 0B
,然后去掉 11 个 0B
,恢复 "HELLO"
。
🟢 示例 2:长于 16 字节(填充至 32)
假设 "HELLO WORLD! 123"
是 17 字节,超过 16,但不是 32:
明文: "HELLO WORLD! 123" (17 字节)
填充: 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F (15 字节)
最终数据(32 字节):
"H E L L O W O R L D ! 1 2 3 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F"
0F = 15
,表示填充了 15 个字节,补足到 32 字节。
解密时,检测到最后一个字节是 0F
,就知道要去掉 15 个 0F
,恢复 "HELLO WORLD! 123"
。
现代安全系统一般不会只依赖密钥,通常会有以下几种安全措施防止这种攻击:
🔒 1. 绑定设备
- 密钥通常存储在
AndroidKeyStore
,只能由该设备访问。 - 你即使知道密钥的内容,也没法直接复制到你的设备上使用,因为它可能是硬件绑定的。
🔒 2. 使用动态 Token
- 服务器可能会给客户端发送一个一次性 Token(比如 OAuth、JWT)。
- 即使你伪造别人的密钥加密数据,你可能无法获取 Token,服务器会拒绝你的请求。
🔒 3. 密钥派生 & 指纹认证
- 某些系统会把密钥和设备指纹(如 IMEI、设备ID)绑定。
- 服务器可能会检查请求中的设备信息,如果不匹配,服务器会拒绝。
签名校验就是用来检查一个请求或数据包是否被篡改,是否在传输过程中被更改过。
工作原理:
-
创建签名:
- 客户端或发送方通过某种加密算法(例如 HMAC)计算消息的“签名”。
- 计算签名时,客户端会使用私有的密钥和消息本身的内容一起计算,生成一段加密字符串(签名)。
这个签名是基于消息内容和密钥生成的,意味着如果消息内容发生了任何变化,签名也会变化。
String message = "City=Shanghai&Temperature=30";
String secretKey = "mySecretKey";
String signature = HMAC.calculateHMAC(message, secretKey); // 计算签名
发送签名:
- 客户端将生成的签名和数据一起发送给服务器。
- 例如,客户端可能会发送一个包含数据和签名的请求:
Data: City=Shanghai&Temperature=30
Signature: d13b2e4f5c29a8de75a3b1b68eaf7e57c7b4f9f95cc27b228c7d9fa38d0d4978
校验签名:
- 服务器收到请求后,会使用与客户端相同的算法和密钥(如果是对称加密的话)重新计算消息的签名。
- 然后,服务器将重新计算的签名与客户端发送的签名进行对比。
如果两者相同,说明消息在传输过程中没有被篡改,且是客户端发送的合法请求;如果不同,说明消息的内容可能被篡改过。
String calculatedSignature = HMAC.calculateHMAC("City=Shanghai&Temperature=30", "mySecretKey");
if (!calculatedSignature.equals(receivedSignature)) {
throw new SecurityException("Signature validation failed! Data might be tampered.");
}
签名的作用:
- 防止篡改: 如果数据在传输过程中被修改,签名就会不匹配,从而发现数据被篡改。
- 身份验证: 如果签名匹配,服务器可以确信数据是由客户端发送的,并且数据没有被篡改。
- 确保数据完整性: 签名能保证接收到的数据和发送的数据是一模一样的。
如果攻击者获得了客户端的密钥,并知道签名所用的加密算法和参数,那么他们就有可能伪造有效的请求。签名的安全性依赖于密钥的保密性,因此保护好密钥是防止伪造请求的关键。
1. 加密存储密钥
使用 Android Keystore 存储密钥
Android 提供了 Keystore
系统,它允许你存储加密密钥并对其进行加密操作,而不直接暴露密钥本身。使用 Keystore 可以确保密钥不会被泄露,因为密钥是存储在安全硬件中。
import android.security.keystore.KeyGenParameterSpec; // 导入密钥生成参数规范类,用于指定密钥的属性和使用方式
import android.security.keystore.KeyProperties; // 导入密钥属性类,用于定义加密算法和密钥用途
import android.util.Base64; // 导入 Base64 编解码工具类
import javax.crypto.Cipher; // 导入用于加解密操作的类
import javax.crypto.KeyGenerator; // 导入密钥生成器类,用于生成对称加密密钥
import javax.crypto.SecretKey; // 导入对称密钥类,代表加密和解密所用的密钥
import java.security.KeyStore; // 导入密钥库类,用于存储密钥
import java.security.NoSuchAlgorithmException; // 导入异常类,用于处理没有对应加密算法的错误
import java.security.cert.CertificateException; // 导入证书异常类,用于处理证书相关错误
public class KeyStoreExample {
private static final String KEY_ALIAS = "MyAppKeyAlias"; // 定义密钥的别名,用于在 KeyStore 中查找该密钥
private KeyStore keyStore; // 声明 KeyStore 实例变量,用于访问 Android KeyStore
// 构造方法,初始化 KeyStore 实例
public KeyStoreExample() throws Exception {
keyStore = KeyStore.getInstance("AndroidKeyStore"); // 获取 AndroidKeyStore 实例
keyStore.load(null); // 加载 KeyStore(null 表示加载默认配置)
}
// 创建密钥并保存到 Keystore 中
public void createKey() throws Exception {
// 创建 KeyGenerator 实例,用于生成密钥
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
// 初始化 KeyGenerator,并指定密钥用途和加密方式
keyGenerator.init(
new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) // 设置密钥别名和用途(加密、解密)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) // 设置块模式为 GCM(Galois/Counter Mode),用于加密
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) // 设置加密填充方式为无填充
.build() // 创建密钥生成参数配置对象
);
keyGenerator.generateKey(); // 根据配置生成密钥并存储在 KeyStore 中
}
// 使用 Keystore 中的密钥进行加密操作
public String encryptData(String inputData) throws Exception {
// 从 KeyStore 中获取存储的密钥
SecretKey key = (SecretKey) keyStore.getKey(KEY_ALIAS, null);
// 创建 Cipher 实例,用于执行加密操作
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // 使用 AES 加密算法,块模式为 GCM,且无填充
cipher.init(Cipher.ENCRYPT_MODE, key); // 初始化 Cipher 为加密模式,并使用 Keystore 中的密钥
// 获取初始化向量(IV),它是 GCM 模式中必需的
byte[] iv = cipher.getIV();
// 执行加密操作,输入数据被加密为字节数组
byte[] encryption = cipher.doFinal(inputData.getBytes()); // 将输入数据转换为字节数组并进行加密
// 将加密后的数据和 IV 合并,并用 Base64 编码方便传输
return Base64.encodeToString(encryption, Base64.DEFAULT) + ":" + Base64.encodeToString(iv, Base64.DEFAULT);
}
// 使用 Keystore 中的密钥进行解密操作
public String decryptData(String encryptedData) throws Exception {
// 将加密后的数据和 IV 从传输字符串中分离
String[] parts = encryptedData.split(":"); // 分割加密数据和 IV
byte[] encryption = Base64.decode(parts[0], Base64.DEFAULT); // 对加密数据进行 Base64 解码
byte[] iv = Base64.decode(parts[1], Base64.DEFAULT); // 对 IV 进行 Base64 解码
// 从 KeyStore 中获取存储的密钥
SecretKey key = (SecretKey) keyStore.getKey(KEY_ALIAS, null);
// 创建 Cipher 实例,用于执行解密操作
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); // 使用 AES 加密算法,块模式为 GCM,且无填充
// 初始化 Cipher 为解密模式,并使用获取的 IV
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv)); // 使用 IV 创建 GCMParameterSpec 以便解密
// 执行解密操作,恢复原始数据
byte[] original = cipher.doFinal(encryption); // 解密数据
// 将解密后的字节数组转换为字符串并返回
return new String(original); // 将字节数组转换为字符串并返回
}
}
代码解释:
KeyStore
是用于存储和管理密钥的 Android 系统组件。KeyGenParameterSpec.Builder
用来创建密钥生成的配置,确保密钥只能用于加密和解密操作。Cipher
用来执行加密和解密。加密后数据和 IV 会一起存储,解密时需要使用同样的 IV。
Cipher
是 Java 和 Android 中用于加密和解密数据的类。它是 javax.crypto.Cipher
类的一部分,提供了一种方法来执行加密和解密操作,支持多种加密算法和模式。
Cipher
类的作用:
- 加密:使用对称加密算法(例如 AES、DES 等)将明文数据转化为加密后的密文。
- 解密:使用相同的算法和密钥将加密后的密文还原为明文数据。
GCM(Galois/Counter Mode)是一种加密模式,它结合了加密和认证功能,用于确保数据的机密性和完整性。GCM 是基于计数器模式(CTR)的,并使用一个额外的认证标签来验证数据是否在传输过程中被篡改。
主要特点:
- 加密性(Confidentiality):像其他加密模式一样,GCM 用于加密数据,确保只有授权的接收者能够读取数据内容。
- 完整性(Integrity):GCM 还提供认证功能,确保加密的数据在传输过程中没有被篡改。认证标签用于验证数据的完整性。
工作原理:
GCM 模式结合了加密和认证的功能,具体步骤如下:
- 加密:使用计数器模式(CTR)对数据进行加密。CTR 模式每次使用一个不同的计数器值(IV + nonce)来生成密钥流,将明文与密钥流进行异或操作,得到密文。
- 认证:同时计算认证标签(Authentication Tag),这个标签是通过对加密数据和一些附加数据(如头信息等)进行特定的数学操作(Galois 域上的乘法)来生成的,确保数据在传输过程中没有被篡改。
2. 密钥管理
密钥的定期更换
定期更换密钥是防止密钥长期泄露的有效措施。密钥的更换可以通过引入新的密钥别名来完成。
示例代码:
public void rotateKey() throws Exception {
// 删除旧密钥(如果有的话)
if (keyStore.containsAlias(KEY_ALIAS)) {
keyStore.deleteEntry(KEY_ALIAS); // 删除旧密钥
}
// 创建新密钥
createKey();
}
通过 rotateKey()
方法,我们可以删除旧的密钥,并创建一个新的密钥。定期更换密钥可以降低密钥泄露后的风险。
3. 密钥保护的传输
使用 HTTPS 保护传输
在客户端和服务器之间传输密钥或密钥加密的数据时,使用 HTTPS 来加密整个通信通道,以防止密钥和其他敏感数据在传输过程中被中间人窃取。
// 使用 OkHttp 发起 HTTPS 请求
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://secure.example.com/endpoint")
.addHeader("Authorization", "Bearer your-token")
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 处理响应
}
@Override
public void onFailure(Call call, IOException e) {
// 处理失败
}
});
在客户端,OkHttp
默认会使用 HTTPS 协议来加密数据传输,确保中间人无法篡改或窃取传输的数据。
4. 结合时间戳和随机数
为进一步增加安全性,可以在每个请求中加入时间戳和随机数(nonce)来防止重放攻击。每个请求都必须包含唯一的时间戳和随机数,以确保每个请求都是唯一的。
public String generateRequestSignature(String data, long timestamp, String nonce) {
String input = data + timestamp + nonce;
return calculateSignature(input);
}
private String calculateSignature(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return Base64.encodeToString(hash, Base64.DEFAULT);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
代码解释:
- 使用时间戳和随机数作为加密数据的一部分。
- 使用
SHA-256
算法生成签名,确保请求的完整性和唯一性。
总结:
密钥保护的关键是存储安全、管理安全和传输安全:
- 加密存储密钥:使用 Android Keystore、硬件安全模块(HSM)等保护密钥。
- 密钥管理:定期轮换密钥,并确保每个密钥的生命周期受到有效管理。
- 加密传输:使用 HTTPS 协议确保密钥和加密数据在网络传输中不被泄露。
- 防止重放攻击:结合时间戳和随机数(nonce)等技术,防止攻击者伪造请求。
密钥保护
使用 HTTPS
使用短期有效的令牌(Token)
签名结合时间戳
Nonce(随机数)机制
双向认证(Mutual Authentication)