Class PushKeyFiles

java.lang.Object
com.iizix.push.PushKeyFiles

public final class PushKeyFiles extends Object
The Push Notification (PN) key files of PUSH-KEYFILES-DESIGN.md: the key directory (section 2), the references written in .iiziPush and in the credentials file's [server] section (3.2), and the one reader that turns a reference into key material (section 5) - for the server and the Designer alike. An application's .iiziPush and its jar carry paths, never a private key or a service-account JSON; the secret lives in a file of the key directory, readable by its owner only.

The key directory (keyDirectory(), ruling K-1): the system property "iizi.push.keys", else the environment variable "IIZI_PUSH_KEYS", else <user.home>/.iizi/push-keys - the same precedence as the credentials file, with a default so that a developer configures nothing. A configured value that is not a path is SEVERE once and the default is used.

A reference (resolve(String), K-2) is a path as [server] fcmServiceAccountFile writes it - a leading ~ and every %NAME% expanded, %USERPROFILE% falling back to user.home - and a relative result is resolved against the key directory, on every machine that reads it (K-8). An absolute path is used as it is.

Reading (vapid(Path,VapidStorage), serviceAccount(Path)): a file is read when it is first needed and held until forget(Path) - the owner's watcher calls it when the file changes (section 7.3) - so a failed read is tried again at the next need, never by a timer. Before any byte is read, the permission rule of PUSH-CREDENTIALS.md section 8 applies to every key file (K-6): on a POSIX file system a file readable by group or others is refused; on Windows a broad ACL is a WARNING once per file. A VAPID key file is the $push container the server writes as server-keys.iiziPush (K-4), loaded through the caller's PushKeyFiles.VapidStorage, and is accepted when both keys are present, each validates and the two belong together; the FCM key file is Google's service-account JSON as downloaded.

Nothing leaks (section 5.5): no refusal, log line or toString carries a byte of a key file. A PushKeyFiles.Refusal names the file and the reason only.

Thread-safe: the cache is guarded by its own lock; the readers run under it.

Author:
Christopher Mindus
  • Nested Class Summary

    Nested Classes
    Modifier and Type
    Class
    Description
    static final class 
    A key file that cannot be used: the file and the reason, never a byte of the file.
    static final record 
    A Firebase service-account key file as read.
    static final record 
    A VAPID key file as read: the pair it holds, validated and belonging together.
    static interface 
    Loads a VAPID key file as its $push container: the server's property factory in production, the Designer's property manager, a test's own.
    static interface 
    Watches a key file for the owner of the watcher: the engine's PushFileWatcher on a server.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final String
    The name of the default key directory inside DEFAULT_PARENT: "push-keys".
    static final String
    The default key directory below user.home: ".iizi", iiziGo's own user directory, then "push-keys".
    static final String
    The environment variable naming the key directory: "IIZI_PUSH_KEYS".
    static final String
    The POSIX mode of a key file: "rw-------", mode 0600.
    static final String
    The POSIX mode of an owner-only directory: "rwx------", mode 0700.
    static final String
    The system property naming the key directory: "iizi.push.keys".
  • Constructor Summary

    Constructors
    Constructor
    Description
    Constructor over the JVM's configuration: the system properties, the environment and user.home.
    PushKeyFiles(UnaryOperator<String> property, UnaryOperator<String> environment, String userHome)
    Constructor over supplied settings, for a test.
  • Method Summary

    Modifier and Type
    Method
    Description
    void
    Adds a listener told, on the watcher's thread, that a watched file changed and was forgotten.
    static Path
    Creates an empty key file owner-only: mode 0600 from the first byte on a POSIX file system, so there is no readable moment; a plain new file elsewhere, where the directory's ACL is inherited.
    static Path
    Creates a directory owner-only: mode 0700 from the start on a POSIX file system, so that no other user can ever list or enter it; a plain new directory elsewhere, where the parent's ACL is inherited.
    static Path
    Expands a path of the credentials file, against the working directory: a leading ~ and every %NAME% from the environment, %USERPROFILE% falling back to user.home where the variable is absent (S-3); an unknown variable is left as written.
    void
    forget(Path file)
    Drops the held material of a file, so that the next need re-reads it; a file never read is a no-op.
    static boolean
    Tells whether a principal name is one of the three the section 8 rule names, with or without its authority prefix, in any case.
    boolean
    isHeld(Path file)
    Tells whether a file's material is held, i.e.
    static boolean
    isMatchingPair(String publicKey, String privateKey)
    Checks that a VAPID pair belongs together: a signature made with the private key verifies under the public key (SHA256withECDSA, java.base only), the check the push services make at the first send.
    static boolean
    Checks that a text is Google's service-account key file: JSON with "type": "service_account".
    Gets the key directory: the system property, else the environment variable, else the default <user.home>/.iizi/push-keys.
    Tells where the key directory comes from, for a page or a log line.
    static String
    pathRefusal(String reference)
    Checks that a reference is a path, syntactically, without touching any file: what PushProps.verify can check on a machine that does not hold the key file.
    static String
    The POSIX refusal of a file: on a file system with POSIX permissions, a file that grants anything to group or others is refused, the rule ssh applies to private keys.
    static String
    The POSIX rule on a permission set: mode & 077 non-zero is refused.
    boolean
    Removes a listener.
    resolve(String reference)
    Resolves a reference to the file it names: blank is none; ~ and %NAME% are expanded; a relative path is taken under the key directory, an absolute one as it is.
    static String
    Narrows a directory to its owner on a POSIX file system, for a directory the caller owns and created itself: a directory that grants anything to group or others is set to mode 0700.
    static String
    Narrows a file to its owner on a POSIX file system, for a file the caller owns and wrote itself (the server's own generated pair, design 9.3): a file that grants anything to group or others is set to mode 0600.
    Reads a Firebase service-account key file, or answers it as held: Google's JSON as downloaded, accepted when "type" is "service_account" and it carries client_email and private_key, the members the FCM authorization needs.
    void
    Sets the owner's watcher: every file read from now on is watched after its first successful read, and forgotten when it changes.
    Reads a VAPID key file, or answers it as held: the $push container loaded through the storage, accepted when VPubKey and VPrivKey are present, each validates and the two belong together.
    void
    watch(Path file)
    Watches a file before it is read, so that its arrival is seen: what the credentials do for the files their [server] section names.
    static String
    The Windows WARNING of a file: an ACL that grants read to Everyone, BUILTIN\Users or Authenticated Users.
    static String
    The Windows rule on an ACL: an ALLOW entry with READ_DATA for one of the three broad principals, matched on the English names.
  • Field Details

    • PROPERTY

      public static final String PROPERTY
      The system property naming the key directory: "iizi.push.keys". Wins over the environment variable.
      See Also:
    • ENVIRONMENT

      public static final String ENVIRONMENT
      The environment variable naming the key directory: "IIZI_PUSH_KEYS". Read when the property is absent.
      See Also:
    • DEFAULT_PARENT

      public static final String DEFAULT_PARENT
      The default key directory below user.home: ".iizi", iiziGo's own user directory, then "push-keys".
      See Also:
    • DEFAULT_NAME

      public static final String DEFAULT_NAME
      The name of the default key directory inside DEFAULT_PARENT: "push-keys".
      See Also:
    • OWNER_ONLY

      public static final String OWNER_ONLY
      The POSIX mode of a key file: "rw-------", mode 0600.
      See Also:
    • OWNER_ONLY_DIRECTORY

      public static final String OWNER_ONLY_DIRECTORY
      The POSIX mode of an owner-only directory: "rwx------", mode 0700.
      See Also:
  • Constructor Details

    • PushKeyFiles

      public PushKeyFiles()
      Constructor over the JVM's configuration: the system properties, the environment and user.home.
    • PushKeyFiles

      public PushKeyFiles(UnaryOperator<String> property, UnaryOperator<String> environment, String userHome)
      Constructor over supplied settings, for a test.
      Parameters:
      property - Answers a system property by name.
      environment - Answers an environment variable by name.
      userHome - The user's home directory.
  • Method Details

    • keyDirectory

      public Path keyDirectory()
      Gets the key directory: the system property, else the environment variable, else the default <user.home>/.iizi/push-keys. A configured value that is not a path is SEVERE once, and the default is used. The directory need not exist.
      Returns:
      The directory, absolute and normalized.
    • keyDirectorySource

      public String keyDirectorySource()
      Tells where the key directory comes from, for a page or a log line.
      Returns:
      "system property iizi.push.keys", "environment variable IIZI_PUSH_KEYS" or "the default".
    • pathRefusal

      public static String pathRefusal(String reference)
      Checks that a reference is a path, syntactically, without touching any file: what PushProps.verify can check on a machine that does not hold the key file.
      Parameters:
      reference - The reference as written, may be null.
      Returns:
      null when it is a path; otherwise the refusal, e.g. "is empty" or "is not a valid path (...)".
    • resolve

      public Path resolve(String reference)
      Resolves a reference to the file it names: blank is none; ~ and %NAME% are expanded; a relative path is taken under the key directory, an absolute one as it is.
      Parameters:
      reference - The reference as written, may be null or blank.
      Returns:
      The absolute normalized path, or null when the reference is blank or not a path.
    • expandPath

      public static Path expandPath(String value)
      Expands a path of the credentials file, against the working directory: a leading ~ and every %NAME% from the environment, %USERPROFILE% falling back to user.home where the variable is absent (S-3); an unknown variable is left as written. The Windows notation is accepted on every platform, so on a system whose separator is / (Linux, macOS) every backslash is taken as a separator: %USERPROFILE%\.iizi\key.json names the same file there as on Windows instead of one file whose name contains backslashes. Moved from PushCredentials unchanged; a reference of a key file goes through resolve(String) instead, which takes a relative path under the key directory.
      Parameters:
      value - The path as written, may be null or blank.
      Returns:
      The absolute path, or null when absent or not a valid path.
    • vapid

      Reads a VAPID key file, or answers it as held: the $push container loaded through the storage, accepted when VPubKey and VPrivKey are present, each validates and the two belong together. Whether the file's public key is the referencing container's is the caller's check.
      Parameters:
      file - The file, from resolve(String).
      storage - The storage that loads the container.
      Returns:
      The pair, held until the file changes.
      Throws:
      PushKeyFiles.Refusal - When the file is missing, refused for its permissions, cannot be loaded, or holds no valid pair; the message never carries a key.
    • serviceAccount

      public PushKeyFiles.ServiceAccountFile serviceAccount(Path file) throws PushKeyFiles.Refusal
      Reads a Firebase service-account key file, or answers it as held: Google's JSON as downloaded, accepted when "type" is "service_account" and it carries client_email and private_key, the members the FCM authorization needs. Whether its project_id is the referencing container's fcmProject is the caller's check.
      Parameters:
      file - The file, from resolve(String).
      Returns:
      The key file, held until it changes.
      Throws:
      PushKeyFiles.Refusal - When the file is missing, refused for its permissions, cannot be read or is not a service-account key file; the message never carries its content.
    • watch

      public void watch(Path file)
      Watches a file before it is read, so that its arrival is seen: what the credentials do for the files their [server] section names. A file whose directory does not exist yet cannot be watched; it is then read at the next need and watched from its first successful read, as every file is. A no-op without a watcher, or for a file already watched.
      Parameters:
      file - The file, may be null for none.
    • forget

      public void forget(Path file)
      Drops the held material of a file, so that the next need re-reads it; a file never read is a no-op. The Windows ACL WARNING is logged again at that read.
      Parameters:
      file - The file.
    • isHeld

      public boolean isHeld(Path file)
      Tells whether a file's material is held, i.e. read and not forgotten since.
      Parameters:
      file - The file.
      Returns:
      true when held.
    • setWatch

      public void setWatch(PushKeyFiles.Watch watch)
      Sets the owner's watcher: every file read from now on is watched after its first successful read, and forgotten when it changes.
      Parameters:
      watch - The watcher, or null for none.
    • addListener

      public void addListener(Consumer<Path> listener)
      Adds a listener told, on the watcher's thread, that a watched file changed and was forgotten.
      Parameters:
      listener - The listener, given the file.
    • removeListener

      public boolean removeListener(Consumer<Path> listener)
      Removes a listener.
      Parameters:
      listener - The listener.
      Returns:
      true if it was registered.
    • posixRefusal

      public static String posixRefusal(Path file)
      The POSIX refusal of a file: on a file system with POSIX permissions, a file that grants anything to group or others is refused, the rule ssh applies to private keys.
      Parameters:
      file - The file.
      Returns:
      The refusal text, e.g. "grants mode 0644 to group or others", or null when the file is fine or the file system has no POSIX permissions.
    • posixRefusal

      public static String posixRefusal(Set<PosixFilePermission> permissions)
      The POSIX rule on a permission set: mode & 077 non-zero is refused.
      Parameters:
      permissions - The permissions.
      Returns:
      The refusal text naming the mode, or null when only the owner has any.
    • windowsAclWarning

      public static String windowsAclWarning(Path file)
      The Windows WARNING of a file: an ACL that grants read to Everyone, BUILTIN\Users or Authenticated Users. The server cannot judge every ACL - a localized Windows names these principals differently - so it warns and does not refuse.
      Parameters:
      file - The file.
      Returns:
      The warning text naming the principals, or null when none of the three reads or the file system has no ACLs.
    • windowsAclWarning

      public static String windowsAclWarning(List<AclEntry> acl)
      The Windows rule on an ACL: an ALLOW entry with READ_DATA for one of the three broad principals, matched on the English names.
      Parameters:
      acl - The entries.
      Returns:
      The warning text naming the principals, or null.
    • isBroadPrincipal

      public static boolean isBroadPrincipal(String name)
      Tells whether a principal name is one of the three the section 8 rule names, with or without its authority prefix, in any case.
      Parameters:
      name - The principal name, may be null.
      Returns:
      true for Everyone, Users or Authenticated Users.
    • createOwnerOnly

      public static Path createOwnerOnly(Path file) throws IOException
      Creates an empty key file owner-only: mode 0600 from the first byte on a POSIX file system, so there is no readable moment; a plain new file elsewhere, where the directory's ACL is inherited.
      Parameters:
      file - The file, which must not exist; its directory must.
      Returns:
      The file.
      Throws:
      IOException - If the file exists or cannot be created.
    • restrictToOwner

      public static String restrictToOwner(Path file) throws IOException
      Narrows a file to its owner on a POSIX file system, for a file the caller owns and wrote itself (the server's own generated pair, design 9.3): a file that grants anything to group or others is set to mode 0600.
      Parameters:
      file - The file.
      Returns:
      The former refusal text when the mode was narrowed, null when nothing was changed (the mode was fine, or the file system has no POSIX permissions).
      Throws:
      IOException - If the permissions cannot be read or set.
    • createOwnerOnlyDirectory

      public static Path createOwnerOnlyDirectory(Path dir) throws IOException
      Creates a directory owner-only: mode 0700 from the start on a POSIX file system, so that no other user can ever list or enter it; a plain new directory elsewhere, where the parent's ACL is inherited. Missing parents are created with the default permissions: only the last directory is the owner's. The directory twin of createOwnerOnly(Path), for the secrets store (decision P-8 (a) of SECRETS-STORE-DESIGN.md).
      Parameters:
      dir - The directory, which must not exist.
      Returns:
      The directory.
      Throws:
      IOException - If the directory exists or cannot be created.
    • restrictDirectoryToOwner

      public static String restrictDirectoryToOwner(Path dir) throws IOException
      Narrows a directory to its owner on a POSIX file system, for a directory the caller owns and created itself: a directory that grants anything to group or others is set to mode 0700. The directory twin of restrictToOwner(Path).
      Parameters:
      dir - The directory.
      Returns:
      The former refusal text when the mode was narrowed, null when nothing was changed (the mode was fine, or the file system has no POSIX permissions).
      Throws:
      IOException - If the permissions cannot be read or set.
    • isServiceAccount

      public static boolean isServiceAccount(String text)
      Checks that a text is Google's service-account key file: JSON with "type": "service_account".
      Parameters:
      text - The text.
      Returns:
      true for a service-account key file.
    • isMatchingPair

      public static boolean isMatchingPair(String publicKey, String privateKey)
      Checks that a VAPID pair belongs together: a signature made with the private key verifies under the public key (SHA256withECDSA, java.base only), the check the push services make at the first send. The core twin of the server's ServerPushKeys.isMatchingPair, for the Designer, which cannot reach the server's key loader. Either key failing to validate or to load is a mismatch.
      Parameters:
      publicKey - The public key, base64url, may be null.
      privateKey - The private key, base64url, may be null.
      Returns:
      true when the public key is the private key's.