这个Java加密代码是线程安全的吗?

11

我想在一个高并发应用程序中使用以下代码,其中某些数据必须加密和解密。所以我需要知道这段代码的哪个部分,如果有的话,应该被同步,以避免不可预测的问题。

public class DesEncrypter {
    Cipher ecipher;
    Cipher dcipher;

    // 8-byte Salt
    byte[] salt = {
        (byte)0xA9, (byte)0x9B, (byte)0xC8, (byte)0x32,
        (byte)0x56, (byte)0x35, (byte)0xE3, (byte)0x03
    };

    int iterationCount = 19;

    DesEncrypter(String passPhrase) {
        try {
            // Create the key
            KeySpec keySpec = new PBEKeySpec(passPhrase.toCharArray(), salt, iterationCount);

            SecretKey key = SecretKeyFactory.getInstance( "PBEWithMD5AndDES").generateSecret(keySpec);
            ecipher = Cipher.getInstance(key.getAlgorithm());
            dcipher = Cipher.getInstance(key.getAlgorithm());

            // Prepare the parameter to the ciphers
            AlgorithmParameterSpec paramSpec = new PBEParameterSpec(salt, iterationCount);

            // Create the ciphers
            ecipher.init(Cipher.ENCRYPT_MODE, key, paramSpec);
            dcipher.init(Cipher.DECRYPT_MODE, key, paramSpec);
        } catch (...)
    }

    public String encrypt(String str) {
        try {
            // Encode the string into bytes using utf-8
            byte[] utf8 = str.getBytes("UTF8");
            // Encrypt
            byte[] enc = ecipher.doFinal(utf8);
            // Encode bytes to base64 to get a string
            return new sun.misc.BASE64Encoder().encode(enc);

        } catch (...)
    }

    public String decrypt(String str) {
        try {
            // Decode base64 to get bytes
            byte[] dec = new sun.misc.BASE64Decoder().decodeBuffer(str);
            // Decrypt
            byte[] utf8 = dcipher.doFinal(dec);
            // Decode using utf-8
            return new String(utf8, "UTF8");
        } catch (...)
    }
}

如果我在每次调用encrypt()和decrypt()方法时都创建一个新的加密器,那么我可以避免并发问题,只是我不确定每次获取一个新的加密器实例会不会有很大的开销。

   public String encrypt(String str) {
        try {
            // Encode the string into bytes using utf-8
            byte[] utf8 = str.getBytes("UTF8");
            // Encrypt
            //new cipher instance
            ecipher = Cipher.getInstance(key.getAlgorithm());

            byte[] enc = ecipher.doFinal(utf8);
            // Encode bytes to base64 to get a string
            return new sun.misc.BASE64Encoder().encode(enc);

        } catch (...)
3个回答

13

标准规则是 - 除非Javadoc明确说明Java库中的类是线程安全的,否则您应该假设它不是。

在这种情况下:

  • 各个类没有被记录为线程安全。
  • Cipher.getInstance(...)SecretKeyFactory.getInstance(...)方法已记录为返回新对象;即不是引用其他线程可能有引用的现有对象。

    更新 - javadoc说明如下:

    "返回一个新的SecretKeyFactory对象,该对象封装了第一个支持指定算法的提供者的SecretKeyFactorySpi实现。"

    此外,源代码清楚地确认了创建并返回新对象。

简而言之,这意味着您的DesEncryptor类当前不是线程安全的,但是您应该能够通过同步相关操作(例如encodedecode)使其线程安全,并且不公开这两个Cipher对象。如果使方法同步很可能会创建瓶颈,那么为每个线程创建一个单独的DesEncryptor实例。


抱歉在一个这么老的话题上发帖,但我怀疑SecretKeyFactory.getInstance()每次都返回一个新对象,因为它是一个工厂。此外,我正在进行测试,第一次调用需要2.5秒,而每个后续调用都需要0秒。 - andrewktmeikle
  1. 请查看更新。
  2. 第一次调用可能会触发类加载和初始化,这可能涉及从操作系统获取“熵”。在某些情况下,这可能需要花费相当长的时间。
- Stephen C
说得好,实际上作为单例并没有太多意义。但为什么只在第一次收集熵?每次调用getInstance时都需要进行,不是吗? - andrewktmeikle
我怀疑熵是被其他加载/初始化作为提供者的一部分的东西收集的。但这只是猜测。如果您想认真找出真正的原因,就需要阅读源代码,对其进行分析和调试等操作。 - Stephen C

3

只有在同时被多个线程使用时,才需要使事物具备线程安全性。由于这个类的每个实例都预计只会被单个线程使用,因此无需担心它是否具有线程安全性。

另外需要注意的是,硬编码盐、随机数或初始化向量(IV)从来不是一个好主意。


不知道,谢谢。我打算将盐和口令短语存储在严格的操作系统权限下只允许应用程序访问的单独文件中。 - user646584
使用一个固定的盐值(即使它是受保护的)基本上会削弱使用盐值的意义。 - David Gelhar
你是说盐应该在运行时随机获取吗?我有点困惑,很明显一旦数据被使用它加密,密码短语就不能改变,那么需要提前生成并存储在安全文件中的是什么,盐和密码短语,只是密码短语等? - user646584
1
使用加盐进行加密的目的在于,相同的明文和口令组合不会总是加密为相同的密文。这个经典的例子是Unix密码文件加密:使用盐,即使两个用户有相同的密码,加密后的值也是不同的(这使得批量字典攻击更加困难)。盐在加密密码时随机生成,并与加密值一起存储。 - David Gelhar
我明白了,但是将盐值与加密后的数值一起存储意味着在存储时无法对盐值进行加密,对吗?这没关系,因为随机盐值将加密与所有其他加密数据分离。如果发现静态盐值,那么所有内容都会被泄露。感谢提供信息。 - user646584
@user646584 @David 人们混淆“盐”和“IV”的术语并没有帮助。盐用于使查找散列密码的原像更加困难。IV用于确保密码中的各种安全属性,包括同一消息加密两次不会导致相同的密文。在这里,您需要的是一个IV,每个创建的消息都应该是唯一的。您不需要盐,因为您无法将密码存储为哈希值。 - Nick Johnson

2

Cipher对象不是线程安全的,因为它保留了有关加密过程的内部状态。这也适用于您的DesEncrypter类 - 每个线程都需要使用自己的DesEncrypter实例,除非您同步encodedecode方法。


看看我上面的例子...为什么不为每次调用encrypt()/decrypt()创建一个新的Cipher实例呢? - user646584
2
是的,但在您的示例中,您正在使用实例变量“ ecipher”和“ dcipher”来存储Cipher实例,因此如果2个线程同时在同一“ DesEncrypter”实例上调用encrypt(),它们将相互冲突(除非您同步encrypt()调用)。为了避免这种情况,您可以将“ ecipher”和“ dcipher”设置为局部变量(在encrypt()函数内部),而不是实例变量,这样每次调用encrypt()都会有自己的值。 - David Gelhar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接