Sensitive.java
package com.maybeitssquid.sensitive;
import java.util.Formattable;
import java.util.FormattableFlags;
import java.util.Formatter;
import java.util.Objects;
import java.util.function.Supplier;
/**
* Container for sensitive data to protect it being inadvertently rendered as a plain {@link
* String}. Subclasses must ensure that {@link Formattable#formatTo(Formatter, int, int, int)} by
* default does not disclose too much information, such as by truncating or masking the value. The
* strategy for ensuring that formatted renditions of this class do not disclose sensitive data is
* defined by a {@link Renderer}. The renderer represents the contained object as a {@link
* CharSequence} with sensitive data redacted. The default renderer produces an empty sequence.
*
* <h2>Rendering Model</h2>
*
* The rendering strategy is provided by the {@link #getRenderer()} method, which subclasses
* override to define how their sensitive data should be formatted. This design allows many
* instances of the same subclass to share a single renderer instance, typically defined as a static
* constant.
*
* <p>The default implementation of {@code getRenderer()} returns a renderer that produces an empty
* string, ensuring no sensitive data is disclosed by default.
*
* <h3>Example Subclass</h3>
*
* <pre>{@code
* public class MaskedSecret extends Sensitive<String> {
* private static final Renderer<String> RENDERER = Renderers.mask();
*
* public MaskedSecret(String value) {
* super(value);
* }
*
* @Override
* protected Renderer<String> getRenderer() {
* return RENDERER;
* }
* }
* }</pre>
*
* <h2>Storage Model</h2>
*
* Sensitive data is stored internally via a {@link Supplier Supplier<T>} rather than
* directly. This indirection enables flexible storage strategies, particularly for controlling
* serialization behavior. When using the convenience constructor that accepts a value directly, the
* value is automatically wrapped in a {@link DoNotSerialize} supplier, which prevents the value
* from surviving Java serialization.
*
* <p>For custom storage behavior, use the constructor that accepts a {@code Supplier<T>} directly:
*
* <pre>{@code
* // Default: value will not survive serialization
* Sensitive<String> safe = new Sensitive<>("secret");
*
* // Custom supplier for alternative storage strategies
* Sensitive<String> custom = new Sensitive<>(() -> retrieveFromSecureStore());
*
* // Lambda returning a constant: value WILL survive serialization
* // Use this when you need the object to be serializable
* Sensitive<String> serializable = new Sensitive<>(() -> "secret");
* }</pre>
*
* <h2>Serialization Protection</h2>
*
* By default, this class provides automatic protection against inadvertent serialization of
* sensitive data. When constructed with a raw value, the value is wrapped in {@link
* DoNotSerialize}, which:
*
* <ul>
* <li>Does not implement {@link java.io.Serializable}
* <li>Causes any attempt to serialize the containing object to fail with {@link
* java.io.NotSerializableException}
* </ul>
*
* <p>This fail-fast behavior ensures that sensitive data cannot be accidentally exposed through
* Java serialization, logging frameworks that serialize objects, or distributed caches.
*
* <p><b>Important:</b> If you need to serialize objects containing sensitive data (e.g., for
* session storage in Redis, Memcached, or distributed session stores), you must explicitly handle
* this by:
*
* <ul>
* <li>Storing only non-sensitive identifiers and retrieving sensitive data on-demand
* <li>Providing a custom {@code Supplier<T>} that implements appropriate serialization behavior
* <li>Implementing custom {@code writeObject()}/{@code readObject()} methods in subclasses
* </ul>
*
* <h2>Thread Safety</h2>
*
* Instances of this class are immutable and thread-safe, provided the contained data type {@code T}
* is itself immutable or properly synchronized, and the {@code Supplier<T>} is thread-safe.
*
* <h2>Subclassing Guidelines</h2>
*
* Subclasses extending this class for custom behavior MUST:
*
* <ul>
* <li>Override {@link #getRenderer()} to provide custom rendering (typically returning a shared
* static instance)
* <li>Not override {@code toString()} to expose sensitive data
* <li>Override {@code getContained()} only to add additional protection (e.g., cloning)
* <li>Document any security implications of their custom behavior
* </ul>
*
* @param <T> The type of sensitive data to be protected.
* @see DoNotSerialize
* @see Renderer
* @see #getRenderer()
*/
public class Sensitive<T> implements Formattable {
/**
* The supplier that provides the sensitive value. Using a supplier allows flexible storage
* strategies, including protection against serialization via {@link DoNotSerialize}.
*/
protected final Supplier<T> supplier;
/**
* Creates a new Sensitive container with the specified supplier.
*
* @param supplier the supplier providing the sensitive value; must not be {@code null}. The
* supplier is expected to return the same value every time.
* @throws NullPointerException if contained is {@code null}
*/
public Sensitive(final Supplier<T> supplier) {
Objects.requireNonNull(supplier, "Sensitive value supplier cannot be null");
this.supplier = supplier;
}
/**
* Convenience constructor that wraps the value in a {@link DoNotSerialize} supplier.
*
* @param value the sensitive value to wrap
*/
public Sensitive(final T value) {
this(new DoNotSerialize<>(value));
}
/**
* Returns the renderer used to format this sensitive value. The renderer is used by {@link
* #formatTo(Formatter, int, int, int)} to transform the value into a partially redacted string
* that exposes the sensitive data only to the specified precision.
*
* <p>The default implementation returns a renderer that produces an empty string, ensuring no
* sensitive data is disclosed by default. Subclasses should override this method to provide
* custom rendering behavior, typically returning a shared static renderer instance.
*
* <p>A Renderer is expected to be stateless and thread-safe. There should be no reason to create
* more than one renderer instance for a given subclass. The implementation normally returns a
* private static singleton. Using a private but non-static renderer causes a new instance to be
* created for every {@code Sensitive} value. Constructing the renderer dynamically inside this
* method, such as by calling a static factory in {@link Renderers} on every call, creates even
* more temporary objects.
*
* <pre>{@code
* // Good: shared static singleton
* private static final Renderer<String> RENDERER = Renderers.mask();
*
* @Override
* protected Renderer<String> getRenderer() {
* return RENDERER;
* }
*
* // Bad: non-static instance field causes per-instance renderer creation
* private final Renderer<String> renderer = Renderers.mask();
*
* @Override
* protected Renderer<String> getRenderer() {
* return this.renderer;
* }
*
* // Bad: dynamic creation on each call
* @Override
* protected Renderer<String> getRenderer() {
* return Renderers.mask();
* }
* }</pre>
*
* @return the renderer for this sensitive value; never {@code null}
*/
protected Renderer<T> getRenderer() {
return Renderer.empty();
}
/**
* Returns the renderer used to format this sensitive value when the alternate form is specified.
*
* <p>The default implementation delegates to {@link #getRenderer()}. Override this method to
* provide an alternate rendition when using {@code String.format("%#s", this)}, such as showing
* the value in a commonly used human-readable form.
*
* <h4>Example</h4>
*
* <pre>{@code
* public class MySecret extends Sensitive<String> {
* private static final Renderer<String> TRUNCATED = Renderers.truncate();
* private static final Renderer<String> MASKED = Renderers.mask('?');
*
* @Override
* protected Renderer<String> getRenderer() { return TRUNCATED; }
*
* @Override
* protected Renderer<String> getAltRenderer() { return MASKED; }
* }
*
* MySecret secret = new MySecret("secret123");
* String.format("%s", secret); // Returns "t123" (truncated)
* String.format("%#s", secret); // Returns "?????t123" (masked with question marks)
* }</pre>
*
* @return the alternate renderer for this sensitive value; never {@code null}
*/
protected Renderer<T> getAltRenderer() {
return getRenderer();
}
/**
* Returns the sensitive value. Subclasses may access this to implement custom behavior.
* Subclasses may override to provide additional protection (e.g., cloning arrays). Use with
* caution to avoid exposing sensitive data.
*
* <p><b>Security Note:</b> This method provides direct access to sensitive data for subclass
* implementation purposes. Avoid calling this method from public APIs or in contexts where the
* result might be logged, serialized, or otherwise persisted.
*
* @return the contained sensitive value, or {@code null} if deserialized
*/
protected T getValue() {
return supplier.get();
}
private static final String FORMAT_S = "%s";
private static final String FORMAT_UPPER_S = "%S";
/**
* Generates a format string to apply the parts of the formatting instructions that are not
* covered by the renderer.
*
* @param width the minimum width of the output
* @param left whether the output should be left-justified
* @param upper whether the output should be converted to uppercase
* @return a format string
*/
protected String residualFormat(final int width, final boolean left, final boolean upper) {
if (width == -1 && !left) {
return upper ? FORMAT_UPPER_S : FORMAT_S;
}
final StringBuilder sb = new StringBuilder(8);
sb.append('%');
if (left) sb.append('-');
if (width != -1) sb.append(width);
sb.append(upper ? 'S' : 's');
return sb.toString();
}
/**
* Formats this sensitive value according to the specified flags, width, and precision.
*
* <p>The rendering behavior is determined by the {@link Renderer} returned by {@link
* #getRenderer()} or {@link #getAltRenderer()} (when the alternate flag is set). Width,
* left-justification, and uppercase flags are applied after rendering.
*
* @param formatter the formatter to write to
* @param flags formatting flags (see {@link FormattableFlags})
* @param width the minimum number of characters; -1 for no minimum
* @param precision passed to the renderer to control redaction; -1 for default
*/
@Override
public void formatTo(Formatter formatter, int flags, int width, int precision) {
final boolean alternate = (flags & FormattableFlags.ALTERNATE) == FormattableFlags.ALTERNATE;
final boolean upper = ((flags & FormattableFlags.UPPERCASE) == FormattableFlags.UPPERCASE);
final boolean left = ((flags & FormattableFlags.LEFT_JUSTIFY) == FormattableFlags.LEFT_JUSTIFY);
final Renderer<T> renderer = alternate ? getAltRenderer() : getRenderer();
Objects.requireNonNull(renderer, "Unexpected null renderer");
final CharSequence redacted = renderer.apply(this.supplier.get(), precision);
formatter.format(residualFormat(width, left, upper), redacted);
}
/**
* Returns the result of applying default string formatting to this value. Equivalent to {@code
* String.format("%s", this)}.
*
* @return the result of applying default string formatting to this value.
*/
@Override
public final String toString() {
return "%s".formatted(this);
}
/**
* Returns the hash of the enclosed {@code raw} data.
*
* @return the hash of the enclosed {@code raw} data.
*/
@Override
public int hashCode() {
return Objects.hashCode(this.supplier.get());
}
/**
* Returns true if the types match and the enclosed raw data are equal.
*
* @param o {@inheritDoc}
* @return if the types match and the enclosed raw data are equal.
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Sensitive<?> other = (Sensitive<?>) o;
return Objects.equals(this.supplier.get(), other.supplier.get());
}
/**
* A {@link Supplier} implementation that wraps a value in a {@code transient} field, preventing
* the value from being serialized via standard Java serialization.
*
* <p>This class is designed to be used with {@link Sensitive} to provide automatic protection
* against inadvertent serialization of sensitive data. When an instance of this class is
* serialized, the contained value will be lost (deserialized as {@code null}).
*
* <h2>Usage</h2>
*
* <pre>{@code
* DoNotSerialize<String> wrapped = new DoNotSerialize<>("secret");
* String value = wrapped.get(); // Returns "secret"
*
* // After serialization and deserialization:
* String value = deserializedWrapped.get(); // Returns null
* }</pre>
*
* <h2>Thread Safety</h2>
*
* Instances of this class are immutable and thread-safe, provided the contained value type {@code
* T} is itself immutable or properly synchronized.
*
* @param <T> the type of value to protect from serialization
* @see Sensitive
*/
public static class DoNotSerialize<T> implements Supplier<T> {
private final transient T value;
/**
* Creates a new instance wrapping the specified value.
*
* @param value the value to wrap; will not be serialized
*/
public DoNotSerialize(final T value) {
this.value = value;
}
/**
* Returns the contained value, or {@code null} if this instance was deserialized.
*
* @return the contained value, or {@code null} after deserialization
*/
@Override
public T get() {
return value;
}
}
}