一尘不染

如何在Java中加密字符串

java

我需要加密的字符串会显示在2D条码中(PDF-417),所以当有人知道扫描的想法时,它就不会可读。

其他需求:

  • 不应该复杂
  • 它不应包含RSA,PKI基础结构,密钥对等。

它必须足够简单,以摆脱被监视的人,并且必须易于对其他有兴趣获取该数据的公司解密。他们打电话给我们,我们告诉他们标准,或者给他们一些简单的密钥,然后可以将其用于解密。

那些公司可能会使用不同的技术,因此最好坚持不依赖于某些特殊平台或技术的某些标准。

你有什么建议?有一些Java类做encrypt()decrypt()没有太多的并发症,实现高安全标准?


阅读 490

收藏
2020-03-02

共1个答案

一尘不染

与CBC等其他模式不同,GCM模式不需要IV是不可预测的。唯一的要求是,对于具有给定密钥的每次调用,IV必须是唯一的。如果对于给定的密钥重复一次,则可能会损害安全性。一种简单的实现方法是使用来自强伪随机数生成器的随机IV,如下所示。

也可以将序列或时间戳记用作IV,但听起来可能不那么琐碎。例如,如果系统未正确跟踪持久存储中已用作IV的序列,则在系统重新引导后,调用可能会重复IV。同样,也没有完美的时钟。电脑时钟重新调整等。

此外,每2 ^ 32次调用后应旋转一次键。有关IV要求的更多详细信息,请参阅此答案和NIST建议。

考虑到以下几点,这是我刚刚在Java 8中编写的加密和解密代码。希望有人会发现这个有用:

  1. 加密算法:具有256位密钥的分组密码AES被认为足够安全。要加密完整的消息,需要选择一种模式。建议使用经过身份验证的加密(同时提供机密性和完整性)。GCM,CCM和EAX是最常用的经过身份验证的加密模式。GCM通常是首选,它在为GCM提供专用指令的Intel体系结构中表现良好。所有这三种模式都是基于CTR(基于计数器)的模式,因此它们不需要填充。因此,它们不容易受到与填充相关的攻击
  2. GCM需要初始化向量(IV)。IV不是秘密。唯一的要求是它必须是随机的或不可预测的。在Java中,SecuredRandom该类用于产生加密强度高的伪随机数。可以在该getInstance()方法中指定伪随机数生成算法。但是,从Java 8开始,推荐的方法是使用getInstanceStrong()将使用由Java 配置和提供的最强算法的方法。Provider
  3. NIST建议GCM使用96位IV,以提高互操作性,效率和简化设计
  4. 为了确保额外的安全性,在下面的实现中将SecureRandom每产生2 ^ 16字节的伪随机字节生成后重新播种
  5. 接收者需要知道IV才能解密密文。因此,IV需要与密文一起传输。一些实现将IV作为AD(关联数据)发送,这意味着将在密文和IV上计算身份验证标签。但是,这不是必需的。IV可以简单地在前面加上密文,因为如果IV在传输过程中由于故意的攻击或网络/文件系统错误而被更改,那么身份验证标签的验证将始终失败。
  6. 字符串不应该用于保存明文消息或密钥,因为字符串是不可变的,因此我们无法在使用后清除它们。这些未清除的字符串随后会在内存中徘徊,并可能显示在堆转储中。出于相同的原因,调用这些加密或解密方法的客户端应在不再需要它们时清除包含消息或密钥的所有变量或数组。
  7. 遵循一般建议,没有提供者在代码中被硬编码
  8. 最后,为了通过网络或存储进行传输,应使用Base64编码对密钥或密文进行编码。Base64的详细信息可以在这里找到。应该遵循Java 8方法

字节数组可以使用以下方法清除:

Arrays.fill(clearTextMessageByteArray, Byte.MIN_VALUE);

但是,从Java 8开始,没有容易清除的方法,SecretKeyspec并且SecretKey由于这两个接口的实现似乎尚未实现destroy()该接口的方法Destroyable。在下面的代码中,编写了一个单独的方法来清除SecretKeySpecSecretKey使用反射。

密钥应使用以下两种方法之一生成。

请注意,密钥是像密码一样的秘密,但是与供人使用的密码不同,密钥是供加密算法使用的,因此只能使用上述方式生成。

package com.sapbasu.javastudy;

import java.lang.reflect.Field;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Crypto {

  private static final int AUTH_TAG_SIZE = 128; // bits

  // NIST recommendation: "For IVs, it is recommended that implementations
  // restrict support to the length of 96 bits, to
  // promote interoperability, efficiency, and simplicity of design."
  private static final int IV_LEN = 12; // bytes

  // number of random number bytes generated before re-seeding
  private static final double PRNG_RESEED_INTERVAL = Math.pow(2, 16);

  private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding";

  private static final List<Integer> ALLOWED_KEY_SIZES = Arrays
      .asList(new Integer[] {128, 192, 256}); // bits

  private static SecureRandom prng;

  // Used to keep track of random number bytes generated by PRNG
  // (for the purpose of re-seeding)
  private static int bytesGenerated = 0;

  public byte[] encrypt(byte[] input, SecretKeySpec key) throws Exception {

    Objects.requireNonNull(input, "Input message cannot be null");
    Objects.requireNonNull(key, "key cannot be null");

    if (input.length == 0) {
      throw new IllegalArgumentException("Length of message cannot be 0");
    }

    if (!ALLOWED_KEY_SIZES.contains(key.getEncoded().length * 8)) {
      throw new IllegalArgumentException("Size of key must be 128, 192 or 256");
    }

    Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);

    byte[] iv = getIV(IV_LEN);

    GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);

    cipher.init(Cipher.ENCRYPT_MODE, key, gcmParamSpec);
    byte[] messageCipher = cipher.doFinal(input);

    // Prepend the IV with the message cipher
    byte[] cipherText = new byte[messageCipher.length + IV_LEN];
    System.arraycopy(iv, 0, cipherText, 0, IV_LEN);
    System.arraycopy(messageCipher, 0, cipherText, IV_LEN,
        messageCipher.length);
    return cipherText;
  }

  public byte[] decrypt(byte[] input, SecretKeySpec key) throws Exception {
    Objects.requireNonNull(input, "Input message cannot be null");
    Objects.requireNonNull(key, "key cannot be null");

    if (input.length == 0) {
      throw new IllegalArgumentException("Input array cannot be empty");
    }

    byte[] iv = new byte[IV_LEN];
    System.arraycopy(input, 0, iv, 0, IV_LEN);

    byte[] messageCipher = new byte[input.length - IV_LEN];
    System.arraycopy(input, IV_LEN, messageCipher, 0, input.length - IV_LEN);

    GCMParameterSpec gcmParamSpec = new GCMParameterSpec(AUTH_TAG_SIZE, iv);

    Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO);
    cipher.init(Cipher.DECRYPT_MODE, key, gcmParamSpec);

    return cipher.doFinal(messageCipher);
  }

  public byte[] getIV(int bytesNum) {

    if (bytesNum < 1) throw new IllegalArgumentException(
        "Number of bytes must be greater than 0");

    byte[] iv = new byte[bytesNum];

    prng = Optional.ofNullable(prng).orElseGet(() -> {
      try {
        prng = SecureRandom.getInstanceStrong();
      } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException("Wrong algorithm name", e);
      }
      return prng;
    });

    if (bytesGenerated > PRNG_RESEED_INTERVAL || bytesGenerated == 0) {
      prng.setSeed(prng.generateSeed(bytesNum));
      bytesGenerated = 0;
    }

    prng.nextBytes(iv);
    bytesGenerated = bytesGenerated + bytesNum;

    return iv;
  }

  private static void clearSecret(Destroyable key)
      throws IllegalArgumentException, IllegalAccessException,
      NoSuchFieldException, SecurityException {
    Field keyField = key.getClass().getDeclaredField("key");
    keyField.setAccessible(true);
    byte[] encodedKey = (byte[]) keyField.get(key);
    Arrays.fill(encodedKey, Byte.MIN_VALUE);
  }
}

加密密钥主要可以通过两种方式生成:

没有任何密码

KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(KEY_LEN, SecureRandom.getInstanceStrong());
SecretKey secretKey = keyGen.generateKey();
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
    "AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);

带密码

SecureRandom random = SecureRandom.getInstanceStrong();
byte[] salt = new byte[32];
random.nextBytes(salt);
PBEKeySpec keySpec = new PBEKeySpec(password, salt, iterations, 
   keyLength);
SecretKeyFactory keyFactory = 
    SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
SecretKey secretKey = keyFactory.generateSecret(keySpec);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(),
    "AES");
Crypto.clearSecret(secretKey);
// After encryption or decryption with key
Crypto.clearSecret(secretKeySpec);
2020-03-02