【问题标题】:Why the encryption of Java Cipher with the same string, key and SecureRandom always differ?为什么使用相同的字符串、密钥和 SecureRandom 的 Java Cipher 加密总是不同?
【发布时间】:2015-11-30 10:51:13
【问题描述】:

感谢大家的cmets。

但是,我必须解释一下这个问题。

我知道我们不应该使用修复随机生成器比较加密结果,因为它可能会降低安全性。但是,我只是想在测试中进行,我将在实际运行中使用随机的原始机制

情况是我需要通过以下步骤使用帐户/密码登录服务器:

  1. 从服务器获取信息:a.b.com/get_cipher.cgi。
  2. 接收响应,对其进行解析并获取一些信息以创建密码。
  3. 使用密码加密帐户/密码,然后组成以下 URL a.b.com/login.cgi?encrypted={encrypted_account_password}

这是一个复杂的过程,我无法请求服务器更改协议。我想测试整个登录过程。因此,我尝试提供假帐户/密码,并在不解密结果的情况下检查生成的 url 是否正确(如果解密结果,则意味着在这个测试用例中,我需要解密加密的结果,解析 url,并提取相关信息,与测试无关的信息太多了。另外,如果我在登录过程中做一些更改,我可能需要修改测试用例中的解密和解析过程。)

这意味着哈希函数不适合我。 (原来的登录过程没有使用任何哈希,所以我不想在测试用例中测试它。而且,即使我检查了哈希结果是正确的,它也不能证明登录过程 是正确的。)

=== 原题如下===

我有一个需要登录的程序。为了避免在网络上以纯文本形式传输密码,我需要对其进行加密。换句话说,登录过程包含一个加密阶段。

然后,我想为整个登录过程编写一个测试用例。如果使用相同的帐户和密码,我认为加密结果是相同的。

由于它可能在加密过程中使用 SecureRandom,我用 Mockito 写了一个假的 SecureRandom,代码如下:

private static final long RANDOM_SEED = 3670875202692512518L;
private Random generateRandomWithFixSeed() {
    Random random = new Random(RANDOM_SEED);
    return random;
}

private SecureRandom generateSecureRandomWithFixSeed() {
    final Random random = generateRandomWithFixSeed();
    final SecureRandom secureRandom = new SecureRandom();
    final SecureRandom spySecureRandom = Mockito.spy(secureRandom);

    Mockito.doAnswer(new Answer<Object>() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Object[] args = invocation.getArguments();
            byte[] bytes = (byte[]) args[0];
            random.nextBytes(bytes);
            return bytes;
        }
    })
            .when(spySecureRandom)
            .nextBytes(Matchers.<byte[]>anyObject());

    return spySecureRandom;
}

@Test
public void test_SecureRandom_WithFixSeed() {
    final SecureRandom secureRandom1 = generateSecureRandomWithFixSeed();
    final SecureRandom secureRandom2 = generateSecureRandomWithFixSeed();

    final byte[] bytes1 = new byte[20];
    final byte[] bytes2 = new byte[20];

    secureRandom1.nextBytes(bytes1);
    secureRandom2.nextBytes(bytes2);

    boolean isTheSameSeries = true;
    for(int i = 0; i < 20; i++) {
        isTheSameSeries &= (bytes1[i]==bytes2[i]);
    }

    assertThat(isTheSameSeries, is(true));
}

generateRandomWithFixSeed() 将使用相同的键新建一个 Random,以便生成相同的结果。 generateSecureRandomWithFixSeed() 使用 Makito 检测函数调用 nextBytes() 并始终回答随机结果。测试 test_SecureRandom_WithFixSeed() 还显示两个不同的 SecureRandom 实例生成相同的结果。

但是,如果我在密码中使用 generateSecureRandomWithFixSeed() 如下,它不能总是返回相同的结果。

    @Test
public void test_cipher() {
    final SecureRandom secureRandomWithFixSeed = generateSecureRandomWithFixSeed();

    final String pkcs = "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv7n+/uWHHVC7229QLEObeH0vUcOagavDukf/gkveqgZsszzGkZQaXfsrjdPiCnvjozCy1tbnLu5EInDy4w8B+a9gtK8KqsvlsfuaT9kRSMUS8CfgpWj8JcJwijmeZhjR52k0UBpWLfn3JmRYW8xjZW6dlOSnS0yqwREPU7myyqUzhk1vyyUG7wLpk7uK9Bxmup0tnbnD4MeqDboAXlQYnIFVV+CXywlAQfHHCfQRsGhsEtu4excZVw7FD1rjnro9bcWFY9cm/KdDBxZCYQoT/UW0OBbipoINycrmfMKt1r4mGE9/MdVoIEMBc54aI6sb2g5J2GtNCYfEu+1/gA99xY0+5B3ydH74cbqfHYOZIvu11Q7GnpZ6l8zTLlMuF/pvlSily76I45H0YZ3HcdQnf/GoKC942P6fNsynHEX01zASYM8dzyMxHQpNEx7fcXGi+uiBUD/Xdm2jwzr9ZEP5eEVlrpcAvr8c9S5ylE50lwR+Mp3vaZxPoLdSGZrfyXy4v97UZSnYARQBacmn6KgsIHIOKhYOxNgUG0jwCO/zrPvlbjiYTHQYLOCcldTULvXOdn51enJFGVjscGoZfRj6vZgyHVCUW4iip4iSbSKPcPbf0GMZuniS9qJ3Wybve0/xpppdOv1c40ez0NKQyQkEZRb+V0qfesatJKZd/hUGr+MCAwEAAQ==";
    final byte bytePKCS[] = Base64.base64ToByteArray(pkcs);
    final X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(bytePKCS);

    PublicKey pubKey = null;
    try {
        pubKey = KeyFactory.getInstance("RSA").generatePublic(pubKeySpec);
    } catch (InvalidKeySpecException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }

    final String targetResultText = "NZqTzuNli92vuXEQNchGeF6faN/NBHykhfqBFcWzBHZhbgljZaWAcAzasFSm/odZZ6vBD2jK7J4oi5BmDjxNdEjkXyv3OZ2sOTLCfudgPwXcXmmhOwWHDLY02OX0X3RwBHzqWczqAd4dwslo59Gp5CT59GWXenJPL8wvG90WH2XAKOmHg5uEZj55ZvErRQ6StPVzLkiNCMPOhga7FZWK/rSEpT6BHDy3CibDZ0PNRtAW4wiYAr0Cw6brqiqkN301Bz6DzrV5380KDHkw26GjM8URSTFekwvZ7FISQ72UaNHhjnh1WgMIPf/QDbrEh5b+rmdZjzc5bdjyONrQuoj0rzrWLN4z8lsrBnKFVo+zVyUeqr0IxqD2aHDLyz5OE4fb5IZJHEMfYr/R79Zfe8IuQ2tusA02ZlFzGRGBhAkb0VygXxJxPXkjbkPaLbZQZOsKLUoIDkcbNoUTxeS9+4LWVg1j5q3HR9OSvmsF5I/SszvVrnXdNaz1IKCfVYkwpIBQ+Y+xI/K360dWIHR/vn7TU4UsGmWtwVciq0jWLuBN/qRE6MV47TDRQu63GzeV00yAM/xUM33qWNXCV1tbGXNZw8jHpakgflTY0hcjOFHPpq2UfJCyxiSBtZ0b7hw9Rvhi8VwYc243jXO9CvGq+J6JYvchvKHjq2+YKn1UB2+gs20=";
    final String plainText = "a";
    String resultText = "";
    try {
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, pubKey, secureRandomWithFixSeed);
        final byte[] result = cipher.doFinal(plainText.getBytes());
        resultText = Base64.byteArrayToBase64(result);
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (NoSuchPaddingException e) {
        e.printStackTrace();
    } catch (IllegalBlockSizeException e) {
        e.printStackTrace();
    } catch (BadPaddingException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    }
    assertThat(resultText, is(targetResultText));
}

【问题讨论】:

  • 只是几个想法...您可以尝试调用 cipher.getIV() 来查看是否存在变化的初始化向量。您可能还不想通过将加密字节与常量进行比较来进行测试,而是使用 DECRYPT_MODE 将从加密字节解密的值与原始值进行比较。

标签: java encryption secure-random


【解决方案1】:

你不应该做你想做的事。这不是能够比较两个加密值以确定它们是否相同的加密点。我相信你可以让它工作,但你实际上会禁用功能并降低你加密的所有内容的安全性,以使它们显示相同。

如果您希望能够在不解密密码的情况下比较两个值,那么您真正想要的是hash function。具体来说,看看至少使用 SHA1 或 SHA-256(更好)。

How to hash some string with sha256 in Java?

按照设计,哈希是单向的(例如,如果没有 Rainbow Table 之类的东西,您将无法反转密码)。但是,它旨在完全按照您的描述使用,将新值与旧值进行比较。

如果您真的想使用加密值,您应该解密密码值并将其与纯文本进行比较。但是,hashing follows best practices

【讨论】:

  • 我不想处理加密结果,但我想测试它。我更准确地描述了我的情况。我需要通过以下步骤使用帐户/密码登录服务器: 1. 从服务器获取信息:a.b.com/get_cipher.cgi 2. 接收并解析响应并创建密码。 3. 使用密码加密账户/密码,然后组成以下 URL a.b.com/login.cgi?encrypted={encrypted_account_password} 这是一个复杂的过程。我想测试一下。我想用步骤 1 的准备响应编写一个测试用例,并检查步骤 3 的结果 url。
【解决方案2】:

对您的解释的回答相当简单,只有散列函数(不需要密码):

1 在服务器和客户端之间共享一个秘密。这必须在之前完成。任何字符串都可以完成这项工作(您可以将其视为密码)。

客户端有P,服务器有P

1bis 更好的安全性:对 P 进行散列,在双方:Client 和 Server 都有 Ph

2 连接:

2a 服务器创建随机 R,并发送给客户端

2b 客户端生成 Ph x R(例如 bit at bit)并对其进行散列 => (Ph x R)h

然后发送

2c 服务器可以做同样的事情:(Ph x R)h 并比较它

一个弱点:如果你在服务器上获得 Ph,你可以欺骗客户端。为避免这种情况,您应该使用其他功能。

其他选项(如果您不信任服务器,则更安全):在您的原始代码中使用非对称密钥和 RSA。

【讨论】:

    【解决方案3】:

    我找到了原因。

    我发现如果我在 Android 4.4 上运行测试用例,它无法工作,但它在 4.1.2 上工作

    通过更多调查,Android 似乎在较新版本 (AndroidOpenSSL) 中使用 OpenSSL 进行加密/描述。

    在文件中,external/conscrypt/src/main/java/org/conscrypt/OpenSSLCipherRSA.java

    @Override
    protected void engineInit(int opmode, Key key, SecureRandom random) throws InvalidKeyException {
        engineInitInternal(opmode, key);
    }
    

    它表明它使用本地 OpenSSL 库来加密并且不使用给定的 SecureRandom。

    解决方案是在 Cipher.getInstance() 时提供 Provider

    @Test
    public void test_cipher() {
        private static final String PROVIDER_NAME = "BC";
        final Provider provider = Security.getProvider(PROVIDER_NAME);
    
        Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", provider);
    }
    

    【讨论】:

      猜你喜欢
      • 2012-08-02
      • 2012-09-10
      • 2011-04-07
      • 2010-09-12
      • 2018-11-28
      • 2016-10-15
      • 1970-01-01
      • 1970-01-01
      • 2017-06-02
      相关资源
      最近更新 更多