Skip to content

SAIF.NetCore.SystemHash business rules

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.

BR-SH-01 Identity hash encoding

Field Value
Statement The identity hash is SHA-256 over the UTF-16LE (Encoding.Unicode) bytes of the identity string, Base64-encoded (44 characters).
Trigger Any hash computation (ComputeHash, ComputeHashNow).
Enforcement SystemHashService.GetHashValue
Outcome Deterministic identity hash; a validator must use the identical encoding to match.
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

BR-SH-02 Identity string uses type ordinal

Field Value
Statement 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.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

BR-SH-03 Credential embeds creation time

Field Value
Statement 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.
Trigger ComputeHash; RemoveSystemHashSaltAndValidateLifetime.
Enforcement 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.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs, src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs

BR-SH-04 Credential randomized per call

Field Value
Statement 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.*
Trigger ComputeHash, Get.
Enforcement SystemHashService.ComputeHash → SaltShaker.SaltHash
Outcome 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).
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash.Tests/SystemHashTests.cs

BR-SH-05 Immediate hash is unsalted

Field Value
Statement ComputeHashNow returns the unsalted identity hash only — no timestamp, no lifetime information.
Trigger ComputeHashNow.
Enforcement SystemHashService.ComputeHashNow
Outcome Produces a deterministic value suitable for server-side comparison against a de-salted credential.
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

BR-SH-06 System name from assembly

Field Value
Statement 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.
Trigger Get, GetAssembly.
Enforcement SystemHashService.GetAssembly; SystemHashService.Get; constructor.
Outcome 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.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

BR-SH-07 Credential is not cached

Field Value
Statement 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.
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs, src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash.Tests/SystemHashTests.cs

BR-SH-08 Credential lifetime window

Field Value
Statement 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.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs, src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SystemHashExpiredException.cs

BR-SH-09 Malformed credential raises BCL exceptions

Field Value
Statement 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.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

BR-SH-10 Lifetime validation is not identity

Field Value
Statement Lifetime validation does not establish identity: RemoveSystemHashSaltAndValidateLifetime returns the unsalted hash and leaves the comparison against an authorized hash to the caller.
Trigger RemoveSystemHashSaltAndValidateLifetime.
Enforcement SystemHashService.RemoveSystemHashSaltAndValidateLifetime
Outcome A validator that calls this method but skips comparing the returned hash against an authorized value accepts any well-formed, fresh credential.*
Source src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

Sources

  • src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs
  • src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash.Tests/SystemHashTests.cs
  • src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs, SystemHashExpiredException.cs