SecureString.java

package org.flasby.crypto;

import java.lang.ref.Cleaner;
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 memory
 * when no longer needed or garbaged collected.
 *
 * <p>Modified from an original post in stackoverflow by Melloware who modified something from
 * OWASP.
 */
public final class SecureString implements CharSequence, AutoCloseable {

  private static final Cleaner CLEANER = Cleaner.create();
  private final Cleaner.Cleanable cleanable;
  private final State state;

  private static class State implements Runnable {
    private final int[] chars;
    private final int[] pad;
    private boolean cleared = false;

    /**
     * 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
     */
    State(final int start, final int length, final CharSequence characters) {
      pad = new int[length];
      chars = new int[length];
      for (int i = start; i < length; i++) {
        final char charAt = characters.charAt(i);
        pad[i] = random.nextInt();
        chars[i] = pad[i] ^ charAt;
      }
    }

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

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

    public void run() {
      // Native resource deallocation happens here.
      Arrays.fill(chars, '0');
      Arrays.fill(pad, 0);
      cleared = true;
    }
  }

  private static final SecureRandom random = new SecureRandom();

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

  public SecureString(final int start, final int end, final CharSequence original) {
    state = new State(start, (end - start), original);
    cleanable = CLEANER.register(this, state);
  }

  @Override
  public char charAt(final int i) {
    return state.charAt(i);
  }

  @Override
  public int length() {
    return state.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. Calling
   * should be avoided as it leaves passwords in memory which can't be cleared.
   */
  public String asString() {
    if (state.cleared) throw new NullPointerException("SecureString is cleared, check your code");
    final char[] value = new char[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() {
    state.run();
  }

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

  @Override
  public void close() {
    cleanable.clean();
  }
}