javax.crypto.Cipher线程安全问题

在项目中使用javax.crypto.Cipher进行AES加密时遇到线程安全问题,导致校验失败。分析发现Cipher实例在多线程环境下由于内部状态变化而不安全。解决方案包括使用ThreadLocal存储、每次操作时新建实例或在同步块中操作。

项目场景:

某个项目中,需要对参数进行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源码学习笔记 - 百度文库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值