Class SecretsStore

java.lang.Object
com.iizix.server.secrets.SecretsStore
All Implemented Interfaces:
ISecretsStore, AutoCloseable

public final class SecretsStore extends Object implements ISecretsStore, AutoCloseable
The secrets store of a machine (SECRETS-STORE-DESIGN.md sections 4 to 7, build order SK-2): one owner-only XML property file, <user.home>/.iizi-secrets/iizi-secrets.xml unless the system property "iizi.secrets.file" names another (decision P-12 (a); there is no environment variable), that keeps the values of the secret-marked properties of every file on the machine, keyed by context and atom path (ISecretsContext).

The file is a property container loaded and saved through the caller's PropFactory, like every IIZI settings file: the root "secrets", a container per context, containers along the atom path, and each leaf of the class of the original property with that class's own codec (P-13): a PasswordProp stays obfuscated, a StringProp is clear, a PKCS12IdentityProp is its bytes. The obfuscation protects against a glance only; the protection of the file is its owner-only mode (section 6.2).

Changes - put(String,String), putProp(String,GProp), remove(String) - are held in memory as operations and written by flush(), which runs about 10000L ms after the last change on the store's own timer engine (never the system engine, which the server disposes before this store), at dispose(), and from the store's own JVM shutdown hook when dispose never ran (P-5 (a)). A flush (section 6.3) takes an exclusive lock on the owner-only sibling ".lock" - a FileLock between processes, a ReentrantLock per file between the stores of one JVM, which a FileLock cannot separate - re-reads the file, which another writer may have changed, applies this store's operations to that fresh image (last writer wins per key, keys this store did not touch are kept), writes an owner-only temporary sibling and forces it, keeps the current file as ".previous", and moves the temporary file over the store atomically. A kill at any moment leaves the old file or the new one. A failed flush keeps its operations, logs one SEVERE and is retried at the next change or at dispose.

External changes (P-9 (a)): a PushFileWatcher watches the file; on a change the store re-reads it under the lock and tells its listeners which keys changed.

Permissions (section 6.2, P-8): the directory is created 0700 and every file 0600 from its first byte. A directory writable by group or others is refused. A directory or file found broader is narrowed with a WARNING. A file that cannot be written - a mounted container secret - makes a read-only store, reported once at INFO and never written, and is refused when it is readable by group or others. On Windows an ACL that lets a broad principal read is a WARNING once per load.

Nothing leaks. No log line, exception text or toString carries a value. Keys and paths are not secret and are named.

Thread-safe.

Author:
Christopher Mindus
  • Field Details

    • PROPERTY

      public static final String PROPERTY
      The system property naming the store file: "iizi.secrets.file". A path, never a value.
      See Also:
    • DEFAULT_DIRECTORY

      public static final String DEFAULT_DIRECTORY
      The directory of the default store below user.home: ".iizi-secrets".
      See Also:
    • DEFAULT_NAME

      public static final String DEFAULT_NAME
      The name of the default store file: "iizi-secrets.xml". It keeps "secret" in its name on purpose (design section 6.1): that is what the Claude Code guards of the source tree match.
      See Also:
    • LOCK_SUFFIX

      public static final String LOCK_SUFFIX
      The suffix of the lock file beside the store: ".lock".
      See Also:
    • PREVIOUS_SUFFIX

      public static final String PREVIOUS_SUFFIX
      The suffix of the previous copy beside the store: ".previous".
      See Also:
    • TEMP_SUFFIX

      public static final String TEMP_SUFFIX
      The suffix of a temporary file beside the store: ".tmp".
      See Also:
    • FLUSH_DELAY_MILLIS

      public static final long FLUSH_DELAY_MILLIS
      The delay between the last change and the flush, in milliseconds: 10000L.
      See Also:
    • ROOT_NAME

      public static final String ROOT_NAME
      The name of the root container of the file: "secrets".
      See Also:
    • MAX_CONTEXT_LENGTH

      public static final int MAX_CONTEXT_LENGTH
      The longest context name: 128.
      See Also:
  • Method Details

    • open

      public static SecretsStore open(PropFactory factory) throws IOException
      Opens the store of this machine: the file named by the system property "iizi.secrets.file", else <user.home>/".iizi-secrets"/"iizi-secrets.xml", with the flush delay of 10000L ms and a watcher on the file.
      Parameters:
      factory - The factory that loads and saves the file: the server's, or the Designer's.
      Returns:
      The store.
      Throws:
      IOException - If the file or its directory is refused, or the file cannot be read. The caller reports it and runs without a store: a missing secret never stops a server (P-15).
    • open

      public static SecretsStore open(Path file, PropFactory factory) throws IOException
      Opens a store file with the flush delay of 10000L ms and a watcher on the file.
      Parameters:
      file - The store file; its directory is created when missing.
      factory - The factory that loads and saves the file.
      Returns:
      The store.
      Throws:
      IOException - If the file or its directory is refused, or the file cannot be read.
    • open

      public static SecretsStore open(Path file, PropFactory factory, long flushDelay, boolean watch) throws IOException
      Opens a store file.
      Parameters:
      file - The store file; its directory is created when missing.
      factory - The factory that loads and saves the file.
      flushDelay - The flush delay in milliseconds; zero or less writes at every change.
      watch - true to watch the file for changes by other processes.
      Returns:
      The store.
      Throws:
      IOException - If the file or its directory is refused, or the file cannot be read.
    • defaultFile

      public static Path defaultFile()
      Gets the store file of this machine: the system property "iizi.secrets.file" when it is a path, else the default below user.home.
      Returns:
      The file, absolute.
    • get

      public String get(String key)
      Gets the value of a key: the pending change of this store, else the file's.
      Specified by:
      get in interface ISecretsStore
      Parameters:
      key - The key.
      Returns:
      The element-string value, or null when there is no entry.
    • has

      public boolean has(String key)
      Tells whether the store has an entry for a key, pending changes included, through keys(): the value is not read (ruling PP-D-4).
      Specified by:
      has in interface ISecretsStore
      Parameters:
      key - The key.
      Returns:
      true when there is an entry.
    • put

      public void put(String key, String value)
      Puts a value, keeping the kind of an existing entry; a new entry is a SecretsStore.Kind.STRING. Use putProp(String,GProp) or put(String,String,Kind) to name the kind.
      Specified by:
      put in interface ISecretsStore
      Parameters:
      key - The key.
      value - The element-string value.
      Throws:
      IllegalArgumentException - If the key is not valid, or the value is not one of its kind.
      IllegalStateException - If the store is read-only or disposed of.
      NullPointerException - If the key or the value is null.
    • putProp

      public void putProp(String key, GProp<?> prop)
      Puts the value of a property under a key, with its kind: the file keeps the class (P-13).
      Specified by:
      putProp in interface ISecretsStore
      Parameters:
      key - The key.
      prop - The property, of a secret-capable class, with a value.
      Throws:
      IllegalArgumentException - If the key is not valid, the class is not secret-capable or the property has no value.
      IllegalStateException - If the store is read-only or disposed of.
    • put

      public void put(String key, String value, SecretsStore.Kind kind)
      Puts a value of a kind under a key.
      Parameters:
      key - The key.
      value - The element-string value of that kind.
      kind - The kind.
      Throws:
      IllegalArgumentException - If the key is not valid, or the value is not one of the kind.
      IllegalStateException - If the store is read-only or disposed of.
      NullPointerException - If an argument is null.
    • remove

      public void remove(String key)
      Removes a key. Removing an absent key does nothing.
      Specified by:
      remove in interface ISecretsStore
      Parameters:
      key - The key.
      Throws:
      IllegalArgumentException - If the key is not valid.
      IllegalStateException - If the store is read-only or disposed of.
    • isReadOnly

      public boolean isReadOnly()
      Tells whether the store is read-only: a file or directory that cannot be written.
      Specified by:
      isReadOnly in interface ISecretsStore
      Returns:
      true when read-only.
    • getFile

      public Path getFile()
      Gets the store file.
      Returns:
      The file, absolute.
    • keys

      public SortedSet<String> keys()
      Gets the keys, pending changes included.
      Returns:
      The keys, sorted; a copy.
    • getKind

      public SecretsStore.Kind getKind(String key)
      Gets the kind of a key.
      Parameters:
      key - The key.
      Returns:
      The kind, or null when there is no entry.
    • hasPendingChanges

      public boolean hasPendingChanges()
      Tells whether changes are waiting for a flush.
      Returns:
      true when a change is not written yet.
    • addChangeListener

      public void addChangeListener(Consumer<Set<String>> listener)
      Adds a listener told, on the watcher's thread or the thread of reload(), the keys whose value another process changed and this store has no pending change of (P-9 (a)): what a loaded secret property may take quietly.
      Parameters:
      listener - The listener.
    • removeChangeListener

      public boolean removeChangeListener(Consumer<Set<String>> listener)
      Removes a listener.
      Parameters:
      listener - The listener.
      Returns:
      true if it was registered.
    • flush

      public boolean flush()
      Writes the pending changes now (section 6.3). Nothing is written when nothing is pending or the store is read-only.
      Returns:
      true when nothing is pending afterwards but what was changed meanwhile, false when the store is read-only with changes, or the flush failed (logged SEVERE, the changes kept for the next flush).
    • reload

      public Set<String> reload() throws IOException
      Re-reads the file when another process changed it, and tells the listeners which keys changed. The watcher calls it; so may the lifecycle.
      Returns:
      The keys whose value changed and are not pending in this store; empty when the file did not change.
      Throws:
      IOException - If the file cannot be read.
    • dispose

      public void dispose()
      Disposes of the store: flushes pending changes, stops the timer and the watcher, and removes the shutdown hook. Idempotent. Getting still works afterwards; changing does not.
    • isDisposed

      public boolean isDisposed()
      Tells whether the store is disposed of.
      Returns:
      true after dispose().
    • close

      public void close()
      Same as dispose().
      Specified by:
      close in interface AutoCloseable
    • toString

      public String toString()
      Names the file, the entry count and whether the store is read-only; never a key's value.
      Overrides:
      toString in class Object
    • checkKey

      public static void checkKey(String key)
      Checks a key: a context of 1 to 128 characters of a-z A-Z 0-9 . _ -, then at least one atom name, separated by ISecretsContext.KEY_SEPARATOR.
      Parameters:
      key - The key.
      Throws:
      IllegalArgumentException - If the key is not valid.
      NullPointerException - If the key is null.