加密填充
只针对 aes 和rsa 加密。rsa签名是另外一个填充方式。其他加密算法使用不多。
- 为什么需要填充?
- 为什么需要知道填充?
为什么需要填充?
RSA和AES虽然属于两种截然不同的加密类型,但它们都属于块密码的应用范畴。
1.AES的块大小是固定的16字节,RSA的块大小根据密钥长度和填充方式而定。由于AES每次只能处理固定长度的数据(即一个块大小),当数据大小不是块大小的整数倍时,就需要对原始数据进行填充,因此填充对AES来说是技术原理上的需求。因为无法保证数据一定是对齐的。
2.RSA则不太一样,RSA填充的主要目的是为了加强算法的安全性。RSA填充在安全上的作用体现在以下两个方面:一方面,填充后明文长度变长,对应的密文长度也变长了;另一方面,某些填充方式会在明文中加入伪随机信息,将给定的明文消息加密为不同的密文。选择不填充只是会默认补0而已。
为什么需要知道填充?
关于这个,对于AES来说意义不大。对应RSA 来说,这个就决定了明文的长度。
因为AES一般都是多次的,但是RSA不一定希望多次,毕竟计算量太大。
RSA 一次加密的明文长度理论上是:密钥长度 128 bytes/1024 bi(keyLen/8)。所以,会因为填充模式的要求,明文长度出现一定的限制。
另外 ,RSA OEAP的填充模式,在各个语言上实现有点不一样(Android keyStore和Java也不一样)。所以,如果一定要选择oeap填充,除非明确两方实现相同,不然最好自己处理。
填充
AES填充
AES 的CTR和GCM不能填充。首先这两种模式下,明文不参加密钥运算,只是参与最后的异或,所以对明文长度没有要求。
本文只讨论几种比较常记的填充
pkcs5和pkcs7.
这两种对于AES 没有区别,规则就是:
加密:
数据长度%16=n。
n>0
数据 n个n。
n==0;
数据 16个16.
解密:
len=data.length
n= data[len-1]
然后倒序便利n个,必须都是等于n,证明是正常填充,否则就是无填充。但是不绝对,如果原始数据刚好符合,就会造成数据丢失,所以必须协商清楚,不能根据数据处理。
RSA 填充
RSA android里面最大加密长度: keyLen/8,但是极限长度的情况下,数据需要比Modulus要小,不然也是会报错:DATA_TOO_LARGE_FOR_MODULUS。
if (BN_ucmp(f, rsa->n) >= 0) {
// usually the padding functions would catch this
OPENSSL_PUT_ERROR(RSA, RSA_R_DATA_TOO_LARGE_FOR_MODULUS);
goto err;
}
1,NOPADDING
生成一个长度固定的keyLen/8,从尾部开始往前拷贝原数据进去,其他地方默认就是0.这种模式,计算结果固定,再次因为前面都是0,无法跟原始数据的0区分开,不推荐这种模式。而且这种模式要注意原数据可能会比Modulus大,造成加密失败。
2,RSA_PKCS1_PADDING
RSA_PKCS1_PADDING即PKCS#1 v1.5,属于PKCS#1的一个早期版本。在该填充模式下,如果明文长度不够K字节,加密的时候会在明文中随机填充一些数据,结果是相同的明文每次加密后的密文都不一样。这个规则也是固定填充,数据都是固定的。
a.生成长度为k-mLen-3的字节串PS包含随机生成的非零的伪随机数。PS的长度最小为8个字节。
b.级联PS,消息M和其它的填充组成长度为k的消息EM
EM = 0x00 || 0x02 || PS || 0x00 || M
这种模式下 ,明文最长k-11(比如128长度就是117)。
3.OEAP
PKCS#1 v2.1: RSA密码学规范中关于 OAEP的模式的讲解如下:
RSAES-OAEP-ENCRYPT (( n, e), M, L )
可选: Hash 哈希函数(hLen代表哈希函数的输出字节数)
MGF 掩码生成函数
输入:
(n,e) 输入的RSA公钥(k代表RSA模数n的字节长度)
M 待加密的数据, 一个长度为mLen的字节串,并且mLen<=k-2hLen-2
L 可选的和消息关联的标签;如果L没有提供,默认的值是空串
输出:
C 加密输出,长度为k的字节串
错误: “消息太长”;“标签太长”
假设: RSA公钥(n,e)是有效的。
操作:
1.长度检查
a.如果L的长度超过了哈希函数的输入限制(SHA-1是2^61-1字节),输出"标签太长“并中止。
b.如果mLen>k-2hLen-2,输出“消息太长”并中止。
2.EME-OAEP编码
a.如果标签L没有提供,使得L为空串。使得lHash=Hash(L),一个长度为hLen的字节串。
b.生成一个字节串PS,由k-mLen-2hLen-2个字节零组成,PS的长度有可能是0。
c.连接lHash,PS,一个单字节值为0x01和消息M,形成一个长度为k-hLen-1的字节串: DB = lHash || PS || 0x01 || M
d.生成一个长度为hLen的随机字节串种子seed。
e.使得 dbMask = MGF(seed, k-hLen-1)
f.使得 maskedDB = DB ⊕ dbMask.
g.使得 seedMask = MGF(maskedDB, hLen)
h.使得 maskedSeed = seed ⊕ seedMask
i.级联值为0x00的单字节,maskedSeed,和maskedDB形成长度为k的消息EM: EM = 0x00 || maskedSeed || maskedDB
这种模式下,每次填充都是随机。明文长度限制,与选择的hash算法相关。
关于这种模式需要说明的:L。label部分,算法要求,会参与hash 计算,但是在Android keystore hash的L固定是空串,而且不允许修改。与其他语言协作的时候注意。
另外:mgf里面的hash算法其实可以与外层hash算法不一样,只要两边协商就行。注意这里有个点,如果是go的oeap填充,mgf1里面的hash函数要和外面的hash一致。所以一般都使用一样的hash。
贴一些工具代码:
import java.security.MessageDigest;
/**
* 掩模生成函数
* mask generator function, as described in PKCS1v2.
*/
public class MGF1
{
private MessageDigest digest;
/**
* Create a version of MGF1 for the given digest.
*
* @param digest
* digest to use as the basis of the function.
*/
public MGF1(MessageDigest digest)
{
this.digest = digest;
}
/**
* int to octet string.
*/
private void I2OSP(int i, byte[] sp)
{
sp[0] = (byte) (i >>> 24);
sp[1] = (byte) (i >>> 16);
sp[2] = (byte) (i >>> 8);
sp[3] = (byte) (i >>> 0);
}
/**
* Generate the mask.
*
* @param seed
* source of input bytes for initial digest state
* @param length
* length of mask to generate
*
* @return a byte array containing a MGF1 generated mask
*/
public byte[]
generateMask(byte[] seed, int length)
{
byte[] mask = new byte[length];
byte[] C = new byte[4];
int counter = 0;
int hLen = digest.getDigestLength();
digest.reset();
while (counter < (length / hLen))
{
I2OSP(counter, C);
digest.update(seed);
digest.update(C);//update 就是把之前的跟现在的连接在一起
System.arraycopy(digest.digest(), 0, mask, counter * hLen, hLen);
counter++;
}
if ((counter * hLen) < length)
{
I2OSP(counter, C);
digest.update(seed);
digest.update(C);
System.arraycopy(digest.digest(), 0, mask, counter * hLen,
mask.length - (counter * hLen));
}
return mask;
}
}
import android.support.annotation.IntDef;
import android.support.annotation.StringDef;
import android.text.TextUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* @author ricardo
* @date 2020/10/28.
* description:
*/
public class OAEPUtils {
/**
* @hide
*/
@IntDef(flag = true, value = {
BLOCK_SIZE_2048,
BLOCK_SIZE_4096
})
@Retention(RetentionPolicy.SOURCE)
public @interface BlockSize {
}
public static final int BLOCK_SIZE_2048 = 2048 / 8;
public static final int BLOCK_SIZE_4096 = 4096 / 8;
/**
* @hide
*/
@StringDef(value = {
SHA_1,
SHA_224,
SHA_256,
SHA_384,
SHA_512
})
@Retention(RetentionPolicy.SOURCE)
public @interface HashName {
}
public static final String SHA_1 = "SHA-1";
public static final String SHA_224 = "SHA-224";
public static final String SHA_256 = "SHA-256";
public static final String SHA_384 = "SHA-384";
public static final String SHA_512 = "SHA-512";
public static byte[] rsaOAEPMGF1SHA1PaddingEMEncode(byte[] sourceData, @HashName String hashName, String label, @BlockSize int blockSize) throws Exception {
byte[] EM = null;
if (sourceData == null || sourceData.length == 0) {
return sourceData;
}
if (blockSize != BLOCK_SIZE_2048 || blockSize != BLOCK_SIZE_4096) {
throw new Exception("block size error");
}
byte[] lHash = SHAUtils.encrypt(TextUtils.isEmpty(label) ? "" : label, hashName);
if (lHash == null) {
throw new Exception("hash error");
}
if (blockSize - 2 * lHash.length - 2 < sourceData.length) {
throw new Exception(" data block size error");
}
byte[] ps = new byte[blockSize
- sourceData.length - 2 * lHash.length - 2];
for (int i = 0; i < ps.length; i++) {
ps[i] = 0;
}
byte[] DB = new byte[blockSize
- lHash.length - 1];
System.arraycopy(lHash, 0, DB, 0, lHash.length);
System.arraycopy(ps, 0, DB, lHash.length, ps.length);
DB[lHash.length + ps.length] = 1;
System.arraycopy(sourceData, 0, DB, lHash.length + ps.length + 1, sourceData.length);
byte[] seed = SecureRandom.getSeed(lHash.length);
MGF1 mgf1 = new MGF1(MessageDigest.getInstance(SHA_1));
byte[] dbMask = mgf1.generateMask(seed, blockSize - lHash.length - 1);
// DB和dbMask做异或运算生成maskedDB
byte[] maskedDB = new BigInteger(DB).xor(new BigInteger(dbMask))
.toByteArray();
byte[] seedMask = mgf1.generateMask(maskedDB, lHash.length);
// seed和seedMask做异或运算生成maskedSeed
byte[] maskedSeed = new BigInteger(seed).xor(new BigInteger(seedMask))
.toByteArray();
EM = new byte[blockSize];
EM[0] = 0;
System.arraycopy(maskedSeed, 0, EM, 1, maskedSeed.length);
System.arraycopy(maskedDB, 0, EM, maskedSeed.length + 1,
maskedDB.length);
return EM;
}
public static byte[] rsaOAEPMGF1SHA1PaddingEMDecode(byte[] EM, @HashName String hashName, String label, @BlockSize int blockSize) throws Exception {
byte[] sourceData = null;
if (EM == null || EM.length == 0 || EM.length != blockSize) {
return sourceData;
}
if (blockSize != BLOCK_SIZE_2048 || blockSize != BLOCK_SIZE_4096) {
throw new Exception("block size error");
}
byte[] lHash = SHAUtils.encrypt(TextUtils.isEmpty(label) ? "" : label, hashName);
if (lHash == null) {
throw new Exception("hash error");
}
byte[] maskSeed = Arrays.copyOfRange(EM, 1, lHash.length + 1);
byte[] maskDB = Arrays.copyOfRange(EM, lHash.length + 1, EM.length);
MGF1 mgf1 = new MGF1(MessageDigest.getInstance(SHA_1));
byte[] seedMask = mgf1.generateMask(maskDB, lHash.length);
byte[] seed = new BigInteger(maskSeed).xor(new BigInteger(seedMask)).toByteArray();
byte[] dbMask = mgf1.generateMask(seed, blockSize - lHash.length - 1);
byte[] DB = new BigInteger(maskDB).xor(new BigInteger(dbMask)).toByteArray();
int index = -1;
for (int k = 0; k < lHash.length; k++) {
if (lHash[k] != DB[k]) {
throw new Exception("hash error");
}
}
for (int j = lHash.length; j < DB.length; j++) {
if (DB[j] == 1) {
index = j + 1;
break;
}
}
sourceData = Arrays.copyOfRange(DB, index, DB.length);
return sourceData;
}
}
- RSA-PSS
与上面的基本类似,主要是用于签名的。因为会对原文有再hash操作,所以不能用于加密。