---
title: SAIF.NetCore.SystemHash business rules
summary: Documented business rules enforced by this library.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.SystemHash
reference_type: package
---

# 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](#br-sh-07-credential-is-not-cached)). |
| 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` |

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [API reference](api/index.md)

> **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`
