项目场景:
某个项目中,需要对参数进行sign校验,其中用到了AES加密算法,于是使用了javax.crypto.Cipher进行加解密的运算。
问题描述
在压测中发现,偶尔会出现校验失败的情况。而生成sign的工具与被测试程序的算法是完全一致的,理论上不应该出现校验不通过,所以需要排查问题。
原因分析:
对被测试程序与测试工具分别进行了多线程测试,发现测试工具对同样的参数,每次生成的sign是一致的,而被测试程序偶尔会生成不同的sign,于是怀疑是并发导致的问题。
被测试程序与测试工具唯一的不同在于,被测试程序的Cipher是在初始时就实例化好的:
Cipher encryptCipher = Cipher.getInstance(ALGORITHM); cipher.init(mode, keySpec, new IvParameterSpec(iv));之后每次加密都直接使用该对象:
encryptCipher.doFinal(srcData.getBytes(StandardCharsets.UTF_8));而测试工具是每次获得一个新对象:
Cipher encryptCipher = Cipher.getInstance(ALGORITHM); cipher.init(mode, keySpec, new IvParameterSpec(iv)); encryptCipher.doFinal(srcData.getBytes(StandardCharsets.UTF_8));
通过查看源码与查阅资料可知, Cipher实例内部维护着自身的状态,在init或者doFinal时会改变自身状态,所以并非是线程安全的。
public final void init(int var1, Key var2, AlgorithmParameterSpec var3, SecureRandom var4) throws InvalidKeyException, InvalidAlgorithmParameterException {
this.initialized = false;
checkOpmode(var1);
if (this.spi != null) {
this.checkCryptoPerm(this.spi, var2, var3);
this.spi.engineInit(var1, var2, var3, var4);
} else {
this.chooseProvider(2, var1, var2, var3, (AlgorithmParameters)null, var4);
}
this.initialized = true;
this.opmode = var1;
if (!skipDebug && pdebug != null) {
pdebug.println("Cipher." + this.transformation + " " + getOpmodeString(var1) + " algorithm from: " + this.provider.getName());
}
}
protected void engineInit(int var1, Key var2, AlgorithmParameterSpec var3, SecureRandom var4) throws InvalidKeyException, InvalidAlgorithmParameterException {
checkKeySize(var2, this.fixedKeySize);
this.updateCalled = false;
this.core.init(var1, var2, var3, var4);
}
void init(int var1, Key var2, AlgorithmParameterSpec var3, SecureRandom var4) throws InvalidKeyException, InvalidAlgorithmParameterException {
this.decrypting = var1 == 2 || var1 == 4;
byte[] var5 = getKeyBytes(var2);
int var6 = -1;
byte[] var7 = null;
if (var3 != null) {
if (this.cipherMode == 7) {
if (!(var3 instanceof GCMParameterSpec)) {
throw new InvalidAlgorithmParameterException("Unsupported parameter: " + var3);
}
var6 = ((GCMParameterSpec)var3).getTLen();
if (var6 < 96 || var6 > 128 || (var6 & 7) != 0) {
throw new InvalidAlgorithmParameterException("Unsupported TLen value; must be one of {128, 120, 112, 104, 96}");
}
var6 >>= 3;
var7 = ((GCMParameterSpec)var3).getIV();
} else if (var3 instanceof IvParameterSpec) {
var7 = ((IvParameterSpec)var3).getIV();
if (var7 == null || var7.length != this.blockSize) {
throw new InvalidAlgorithmParameterException("Wrong IV length: must be " + this.blockSize + " bytes long");
}
} else {
if (!(var3 instanceof RC2ParameterSpec)) {
throw new InvalidAlgorithmParameterException("Unsupported parameter: " + var3);
}
var7 = ((RC2ParameterSpec)var3).getIV();
if (var7 != null && var7.length != this.blockSize) {
throw new InvalidAlgorithmParameterException("Wrong IV length: must be " + this.blockSize + " bytes long");
}
}
}
if (this.cipherMode == 0) {
if (var7 != null) {
throw new InvalidAlgorithmParameterException("ECB mode cannot use IV");
}
} else if (var7 == null) {
if (this.decrypting) {
throw new InvalidAlgorithmParameterException("Parameters missing");
}
if (var4 == null) {
var4 = SunJCE.getRandom();
}
if (this.cipherMode == 7) {
var7 = new byte[GaloisCounterMode.DEFAULT_IV_LEN];
} else {
var7 = new byte[this.blockSize];
}
var4.nextBytes(var7);
}
this.buffered = 0;
this.diffBlocksize = this.blockSize;
String var8 = var2.getAlgorithm();
if (this.cipherMode == 7) {
if (var6 == -1) {
var6 = GaloisCounterMode.DEFAULT_TAG_LEN;
}
if (this.decrypting) {
this.minBytes = var6;
} else {
this.requireReinit = Arrays.equals(var7, this.lastEncIv) && MessageDigest.isEqual(var5, this.lastEncKey);
if (this.requireReinit) {
throw new InvalidAlgorithmParameterException("Cannot reuse iv for GCM encryption");
}
this.lastEncIv = var7;
this.lastEncKey = var5;
}
((GaloisCounterMode)this.cipher).init(this.decrypting, var8, var5, var7, var6);
} else {
this.cipher.init(this.decrypting, var8, var5, var7);
}
this.requireReinit = false;
}
public final byte[] doFinal(byte[] var1) throws IllegalBlockSizeException, BadPaddingException {
this.checkCipherState();
if (var1 == null) {
throw new IllegalArgumentException("Null input buffer");
} else {
this.chooseFirstProvider();
return this.spi.engineDoFinal(var1, 0, var1.length);
}
}
protected byte[] engineDoFinal(byte[] var1, int var2, int var3) throws IllegalBlockSizeException, BadPaddingException {
byte[] var4 = this.core.doFinal(var1, var2, var3);
this.updateCalled = false;
return var4;
}
byte[] doFinal(byte[] var1, int var2, int var3) throws IllegalBlockSizeException, BadPaddingException {
try {
this.checkReinit();
byte[] var4 = new byte[this.getOutputSizeByOperation(var3, true)];
byte[] var5 = this.prepareInputBuffer(var1, var2, var3, var4, 0);
int var6 = var5 == var1 ? var2 : 0;
int var7 = var5 == var1 ? var3 : var5.length;
int var8 = this.fillOutputBuffer(var5, var6, var4, 0, var7, var1);
this.endDoFinal();
if (var8 < var4.length) {
byte[] var9 = Arrays.copyOf(var4, var8);
if (this.decrypting) {
Arrays.fill(var4, (byte)0);
}
return var9;
} else {
return var4;
}
} catch (ShortBufferException var10) {
throw new ProviderException("Unexpected exception", var10);
}
}
解决方案:
知道了原因,解决方案也就明确了,可以采用以下几种方法:
1. 存储为threadlocal
2. 在每次加密/解密时获取新实例
3. 包装在
synchronized块中
参考:
在项目中使用javax.crypto.Cipher进行AES加密时遇到线程安全问题,导致校验失败。分析发现Cipher实例在多线程环境下由于内部状态变化而不安全。解决方案包括使用ThreadLocal存储、每次操作时新建实例或在同步块中操作。
5590

被折叠的 条评论
为什么被折叠?



