The following rules govern how SystemHashService computes, salts, and validates a system-identity credential. All rules are local to this library except where HashUtil's salting mechanics are the Origin.
The identity string is "{systemName}-{(int)type}" — the SystemType ordinal (Console=0, WindowsService=1, WebApp=2, Guidewire=3, ThickClient=4, Script=5, Other=6), not its name, distinguishes system types. Same name and type always yields the same identity hash; a different name or type yields a different hash.
Trigger
Any hash computation.
Enforcement
SystemHashService.GetHashValue; ordinals from SystemType (SAIF.NetCore.ServiceContracts)
Outcome
Reordering or inserting SystemType members silently changes every hash for later members.*
A salted credential embeds its creation time (host-local DateTime.Now, millisecond precision), via HashUtil salting, and that time is the sole basis for later lifetime validation.
SystemHashService.ComputeHash; SystemHashService.RemoveSystemHashSaltAndValidateLifetime; timestamp encoding performed by SAIF.NetCore.HashUtil's SaltShaker.
Outcome
Issuer and validator must share time zone and a culture compatible with HashUtil's date format.*
A salted credential is regenerated and randomized on every call: HashUtil's salting inserts the date parts at pseudo-random positions using its own Random instance, and the embedded timestamp generally differs between calls. Two calls with identical inputs therefore normally return different strings, but this is not a guaranteed-unique/collision-free property — finite millisecond timestamps and a finite set of random insertion choices mean a repeat is not impossible.*
Credentials cannot be compared directly for equality; a presented credential must be de-salted first. No caching occurs, so repeated calls are not cheaper or idempotent (verified; see BR-SH-07).
The system name used by Get is the simple name of the assembly injected into the constructor; if no assembly was injected, it is the simple name of Assembly.GetCallingAssembly() as evaluated inside GetAssembly.
With injection: deterministic per-assembly identity. Without injection: the identity most likely resolves to the SAIF.NetCore.SystemHash assembly itself (since GetAssembly is called from within Get), not the consuming application, unless the JIT inlines the call.*
Get does not cache a computed credential: it recomputes and re-salts on every call, contrary to its own XML documentation, which states the value is generated once and returned on subsequent calls.
Trigger
Get.
Enforcement
SystemHashService.Get (no field assignment).
Outcome
Each call to Get recomputes and re-salts using the current time, normally producing a different credential, but a distinct credential or a distinct embedded timestamp is not guaranteed: calls within the same millisecond share a timestamp, and the finite set of randomized insertion choices can repeat.
A presented credential is valid only if createDate ≤ now + abs(nowBufferSeconds) AND createDate > now − abs(nowBufferSeconds) − expireSeconds; otherwise a SystemHashExpiredException(now, createDate) is thrown. The source expresses each bound as an OR of the +nowBufferSeconds and −nowBufferSeconds cases, and that OR is symmetric in the sign of nowBufferSeconds — a negative buffer produces the same window as a positive buffer of equal magnitude; it does not narrow the window. The additional grace this window grants past the credential's nominal createDate + expireSeconds expiry is abs(nowBufferSeconds) alone — not the figure below. Measured forward from createDate, a credential's total lifetime (how long after issuance it still validates, inclusive of that grace) is expireSeconds + abs(nowBufferSeconds). But the buffer also applies independently on the early side, admitting a createDate up to abs(nowBufferSeconds) in the future relative to the validator's clock — so the full admissible createDate interval for a given now is (now − abs(nowBufferSeconds) − expireSeconds, now + abs(nowBufferSeconds)], a total width of expireSeconds + 2 × abs(nowBufferSeconds), not expireSeconds + abs(nowBufferSeconds) (e.g., now=100, expireSeconds=30, nowBufferSeconds=10 admits createDate in (60, 110], width 50). This equivalence holds only for non-overflowing int arithmetic and in-range DateTime values: the literal source computes nowBufferSeconds * -1 as an int multiplication with no checked context and no project-level CheckForOverflowUnderflow setting, so at int.MinValue the multiplication does not throw OverflowException — it wraps unchecked back to int.MinValue itself (negating int.MinValue has no representable positive counterpart in int, and unchecked arithmetic silently returns the original value instead of a larger-magnitude positive one). Because both the nowBufferSeconds * -1 arm and the plain nowBufferSeconds arm then evaluate to the same int.MinValue offset, the symmetric-window simplification above is invalid at this input: both bounds collapse onto the same shifted-time comparison, now.AddSeconds(int.MinValue), roughly 68 years in the past. The method does not special-case this value — it still evaluates (shiftedNow ≥ createDate) && (shiftedNow < expires) against that shifted time. For a credential with a recent/current createDate, this shifted comparison still evaluates false and the credential is rejected, consistent with ordinary expiry. But the outcome is conditional, not unconditional: a crafted credential whose createDate is itself roughly 68+ years in the past, combined with a sufficiently large expireSeconds, can satisfy both arms of the shifted predicate and be accepted rather than rejected. The code also calls createDate.AddSeconds(expireSeconds)/now.AddSeconds(nowBufferSeconds), which throw ArgumentOutOfRangeException if the shifted value falls outside the DateTime range. Each OR also short-circuits: the literal source evaluates the nowBufferSeconds * -1 arm first in both bound checks, and only evaluates the plain nowBufferSeconds arm when that first arm is false, so both offsets are not always computed. The create-date upper bound is inclusive; the lower bound is exclusive.
Trigger
RemoveSystemHashSaltAndValidateLifetime.
Enforcement
SystemHashService.RemoveSystemHashSaltAndValidateLifetime; exception type from SAIF.NetCore.HashUtil's SystemHashExpiredException.
Outcome
An expired or not-yet-valid credential is rejected with an exception rather than silently accepted. At nowBufferSeconds == int.MinValue, the unchecked negation wraps back to int.MinValue instead of throwing, so this input is not special-cased to an unconditional outcome: it is evaluated through the same shifted-window predicate as any other value. A current credential still fails that shifted predicate and is rejected, but a credential with a createDate far enough in the past (roughly 68+ years) combined with a large enough expireSeconds can satisfy the shifted predicate and be accepted. There is no guard against a zero or negative expireSeconds; such a value shrinks or eliminates the acceptance window rather than causing an unconditional pass or fail — whether a given credential still passes continues to depend on the relationship between now, createDate, and nowBufferSeconds.*
A malformed (non-HashUtil-format) credential fails with a BCL exception raised by HashUtil (ArgumentOutOfRangeException, FormatException, or NullReferenceException), not SystemHashExpiredException; SystemHashService does not catch or translate these exceptions.
Trigger
RemoveSystemHashSaltAndValidateLifetime with malformed input.
Enforcement
SystemHashService.RemoveSystemHashSaltAndValidateLifetime (no try/catch) → SaltShaker.BlandHash
Outcome
Callers must handle these BCL exception types in addition to SystemHashExpiredException.*
Lifetime validation does not establish identity: RemoveSystemHashSaltAndValidateLifetime returns the unsalted hash and leaves the comparison against an authorized hash to the caller.
Note: Statements marked with * were inferred from code structure, naming
conventions, or contextual evidence where direct confirmation was not available
in source artifacts.