当前位置: 代码迷 >> 综合 >> javax.crypto.Cipher线程安全问题
  详细解决方案

javax.crypto.Cipher线程安全问题

热度:45   发布时间:2023-12-22 23:54:45.0

项目场景:

某个项目中,需要对参数进行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块中

参考:

java - Cipher线程安全吗? - Thinbug 

javax.crypto.Cipher源码学习笔记 - 百度文库

  相关解决方案