Java 相当于 SecureString

Java equivalent of SecureString

我正在寻找 Java 与 .NET SecureString.aspx 的等效项。 2018年有这样的实现吗?

OWASP implementation is not exactly the same because it's just a plain char array. While .NET equivalent provides additional features 例如获取实例的能力 from/to 非托管内存和加密。

我知道常见的 Java 模式将密码作为 char[] 传递,并在使用后将 Arrays.fill() 设为零。但它需要一直围绕 char[] 构建一个简单的实用程序 class。

Oracle 有一个 GuardedString implementation. It is the closest match to .NET's SecureString 解决方案。

Secure string implementation that solves the problems associated with keeping passwords as java.lang.String. That is, anything represented as a String is kept in memory as a clear text password and stays in memory at least until it is garbage collected.

The GuardedString class alleviates this problem by storing the characters in memory in an encrypted form. The encryption key will be a randomly-generated key.

In their serialized form, GuardedStrings will be encrypted using a known default key. This is to provide a minimum level of protection regardless of the transport. For communications with the Remote Connector Framework it is recommended that deployments enable SSL for true encryption.

Applications may also wish to persist GuardedString. In the case of Identity Manager, it should convert GuardedStrings to EncryptedData so that they can be stored and managed using the Manage Encryption features of Identity Manager. Other applications may wish to serialize APIConfiguration as a whole. These applications are responsible for encrypting the APIConfiguration blob for an additional layer of security (beyond the basic default key encryption provided by GuardedString).

我修改了 OWASP 版本以随机填充内存中的 char 数组,因此静态 char 数组不会与实际字符一起存储。

import java.security.SecureRandom;
import java.util.Arrays;


/**
* This is not a string but a CharSequence that can be cleared of its memory.
* Important for handling passwords. Represents text that should be kept
* confidential, such as by deleting it from computer memory when no longer
* needed or garbaged collected.
*/
public class SecureString implements CharSequence {

   private final int[] chars;
   private final int[] pad;

   public SecureString(final CharSequence original) {
      this(0, original.length(), original);
   }

   public SecureString(final int start, final int end, final CharSequence original) {
      final int length = end - start;
      pad = new int[length];
      chars = new int[length];
      scramble(start, length, original);
   }

   @Override
   public char charAt(final int i) {
      return (char) (pad[i] ^ chars[i]);
   }

   @Override
   public int length() {
      return chars.length;
   }

   @Override
   public CharSequence subSequence(final int start, final int end) {
      return new SecureString(start, end, this);
   }

   /**
    * Convert array back to String but not using toString(). See toString() docs
    * below.
    */
   public String asString() {
      final char[] value = new char[chars.length];
      for (int i = 0; i < value.length; i++) {
         value[i] = charAt(i);
      }
      return new String(value);
   }

   /**
    * Manually clear the underlying array holding the characters
    */
   public void clear() {
      Arrays.fill(chars, '0');
      Arrays.fill(pad, 0);
   }

   /**
    * Protect against using this class in log statements.
    * <p>
    * {@inheritDoc}
    */
   @Override
   public String toString() {
      return "Secure:XXXXX";
   }

   /**
    * Called by garbage collector.
    * <p>
    * {@inheritDoc}
    */
   @Override
   public void finalize() throws Throwable {
      clear();
      super.finalize();
   }

   /**
    * Randomly pad the characters to not store the real character in memory.
    *
    * @param start start of the {@code CharSequence}
    * @param length length of the {@code CharSequence}
    * @param characters the {@code CharSequence} to scramble
    */
   private void scramble(final int start, final int length, final CharSequence characters) {
      final SecureRandom random = new SecureRandom();
      for (int i = start; i < length; i++) {
         final char charAt = characters.charAt(i);
         pad[i] = random.nextInt();
         chars[i] = pad[i] ^ charAt;
      }
   }

}

这个答案对 @sanketshah's great 添加了更多解释。

下面的代码显示了用法:

import org.identityconnectors.common.security.GuardedString;

import java.security.SecureRandom;

public class Main {
    public static void main(String[] args) {
        /*
         * Using:
         *   "password".toCharArray();
         * would create an immutable String "password",
         * which remains in memory until GC is called.
         */
        char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};
        GuardedString guardedString = new GuardedString(password);

        /*
         * Securely wipe the char array by storing random values in it.
         * Some standards require multiple rounds of overwriting; see:
         * https://en.wikipedia.org/wiki/Data_erasure#Standards
         */
        SecureRandom sr = new SecureRandom();
        for (int i = 0; i < password.length; i++)
            password[i] = (char) sr.nextInt(Character.MAX_VALUE + 1);
        //noinspection UnusedAssignment
        password = null;

        /*
         * At some later point in the code, we might need the secret.
         * Here's how to obtain it using Java 8+ lambdas.
         */

        guardedString.access(chars -> {
            for (char c : chars) {
                System.out.print(c);
            }
        });
    }
}

GuardedString可以从maven获取IdentityConnectors: Framework. However, for the actual implementation, one also needs IdentityConnectors: Framework Internal.

更准确的说,前一个包定义了Encryptor接口:

package org.identityconnectors.common.security;

/**
 * Responsible for encrypting/decrypting bytes. Implementations
 * are intended to be thread-safe.
 */
public interface Encryptor {
    /**
     * Decrypts the given byte array
     * @param bytes The encrypted bytes
     * @return The decrypted bytes
     */
    public byte [] decrypt(byte [] bytes);
    /**
     * Encrypts the given byte array
     * @param bytes The clear bytes
     * @return The ecnrypted bytes
     */
    public byte [] encrypt(byte [] bytes);
}

由第二个包中的EncryptorImpl实现。 (抽象classEncryptorFactory也是一样,由EncryptorFactoryImpl扩展)。

EncryptorFactory 实际上修复了它的实现:

// At some point we might make this pluggable, but for now, hard-code
private static final String IMPL_NAME = "org.identityconnectors.common.security.impl.EncryptorFactoryImpl";

实现的一个不安全方面是它们将 AES/CBC/PKCS5Padding 与硬编码 IV 和密钥一起使用。 EncryptorFactoryImpl 的构造函数将 true 传递给 EncryptorImpl:

public EncryptorFactoryImpl() {
    _defaultEncryptor = new EncryptorImpl(true);
}

这导致它使用默认密钥。不管怎样,IV 总是固定的:

public EncryptorImpl( boolean defaultKey ) {
    if ( defaultKey ) {
        _key = new SecretKeySpec(_defaultKeyBytes,ALGORITHM);
        _iv  = new IvParameterSpec(_defaultIvBytes);            
    }
    else {
        try {
            _key = KeyGenerator.getInstance(ALGORITHM).generateKey();
            _iv  = new IvParameterSpec(_defaultIvBytes);
        }
        catch (RuntimeException e) {
            throw e;
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

这里有一些 space 需要改进的地方:

  1. 使用 AES/CTR 或 AES/GCM 而不是 AES/CBC。 (参见 block cipher mode of operation。)
  2. 始终使用随机 IV 和密钥。
  3. GuardedString 使用内部方法 SecurityUtil.clear() 清除字节数组,将字节清零。如果有其他可能的 data erasure 算法就好了。