Class KeychainSecretStore

java.lang.Object
org.apache.juneau.secret.macos.keychain.KeychainSecretStore
All Implemented Interfaces:
SecretStore

public class KeychainSecretStore extends Object implements SecretStore
A SecretStore backed by the macOS security keychain CLI (generic passwords).

Each operation shells out to /usr/bin/security:

Items are namespaced by a caller-supplied service name (the keychain -s attribute) with the secret key as the account (-a). Choose a stable, collision-resistant service name (for example a reverse-DNS string) so unrelated consumers do not clobber each other's entries.

Platform. Only macOS is supported; on any other OS, or when /usr/bin/security is unavailable, calls are treated as a backend failure and resolved per the configured FailMode.

Backend-unavailable behavior. A key that is simply not present is a normal absent result, never a failure. A genuine failure — the tool missing, a non-zero exit that is not "item not found", or a timeout — is resolved by the FailMode chosen at construction: FailMode.FAIL_OPEN degrades to absent/no-op, while FailMode.FAIL_CLOSED (the default) throws.

Secret handling. store(java.lang.String, char[]) never places the secret on the security process's argv, and never relies on the CLI's password prompt (a bare -w makes security read from the controlling terminal, /dev/tty, whenever the JVM has one — not from stdin — so it would hang). Instead it runs security -i (interactive mode, which reads commands from stdin) and writes a single command line to stdin: add-generic-password -U -a "key" -s "service" -w "secret". The secret is therefore never visible in the process table (e.g. to ps), and no terminal is ever read. Each of the three values is double-quoted, with only " and \ escaped by a preceding backslash (verified against security -i for spaces, tabs, single and double quotes, backslashes, shell metacharacters, and non-ASCII text). Neither store(java.lang.String, char[]) nor find(java.lang.String) materializes the secret as a String: store encodes the char[] directly to the stdin byte payload, and find decodes the retrieved bytes into a char[] (zeroing the intermediate buffers). The value is never logged or toString()'d by this class. Because the command is newline-terminated, a key, service, or secret containing '\n' or '\r' cannot be delivered this way (it would end or inject a command) and is rejected by store(java.lang.String, char[]) before any process is started.

Reading secrets. find(java.lang.String) deliberately does not use -w: for a secret holding a control character (such as a tab) or any non-ASCII byte, -w prints a bare hex dump of the bytes instead of the text, which cannot be told apart from a secret that merely looks like hex (e.g. abcd). The -g flag instead writes one unambiguous line to stderr: password: "text" for a plain printable secret, password: 0xHEX "escaped" for anything else (including a secret containing " or \), or a bare password: for an empty secret. The hex form is decoded to bytes and then as UTF-8. The stdout of -g holds only the non-secret item attributes and is ignored.

Since:
10.0.0
  • Field Details

  • Constructor Details

    • KeychainSecretStore

      public KeychainSecretStore(String service)
      Constructor using FailMode.FAIL_CLOSED and the default timeout.
      Parameters:
      service - The keychain service name that namespaces this store's items. Must not be null or blank.
    • KeychainSecretStore

      public KeychainSecretStore(String service, FailMode failMode)
      Constructor using the default timeout.
      Parameters:
      service - The keychain service name that namespaces this store's items. Must not be null or blank.
      failMode - The policy applied on a backend failure. Must not be null.
    • KeychainSecretStore

      public KeychainSecretStore(String service, FailMode failMode, long timeoutSeconds)
      Constructor.
      Parameters:
      service - The keychain service name that namespaces this store's items. Must not be null or blank.
      failMode - The policy applied on a backend failure. Must not be null.
      timeoutSeconds - The per-call timeout, in seconds, bounding a slow/hung security invocation. Must be > 0.
  • Method Details

    • store

      public void store(String key, char[] secret)
      Description copied from interface: SecretStore
      Stores a secret under the specified key, replacing any existing value.

      The caller retains ownership of secret and may zero it after this call returns; an implementation that needs to retain the value must copy it.

      Specified by:
      store in interface SecretStore
      Parameters:
      key - The key under which to store the secret. Must not be null.
      secret - The secret value. Must not be null.
    • find

      public Optional<char[]> find(String key)
      Description copied from interface: SecretStore
      Returns the secret stored under the specified key.

      The returned array is the caller's to own and zero; it is not a live view of the store's internal state.

      Specified by:
      find in interface SecretStore
      Parameters:
      key - The key to look up. Must not be null.
      Returns:
      The secret value, or Optional.empty() if no secret is stored under the key.
    • exists

      public boolean exists(String key)
      Description copied from interface: SecretStore
      Returns whether a secret is stored under the specified key without materializing its value.
      Specified by:
      exists in interface SecretStore
      Parameters:
      key - The key to check. Must not be null.
      Returns:
      true if a secret is stored under the key.
    • delete

      public boolean delete(String key)
      Description copied from interface: SecretStore
      Deletes the secret stored under the specified key.
      Specified by:
      delete in interface SecretStore
      Parameters:
      key - The key to delete. Must not be null.
      Returns:
      true if a secret was present and removed; false if the key was already absent.