---
title: SAIF.NetCore.HashUtil business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.HashUtil
reference_type: package
---

# SAIF.NetCore.HashUtil business rules

This page captures business rules enforced by this library.

## BR-HU-01 Fixed 42-character location map prefix

| Field | Value |
| --- | --- |
| **Statement** | A salted hash is prefixed with a location map composed of seven 6-character entries (3-digit zero-padded index + pad char + unpadded decimal body length), one per embedded date part. The map is a fixed 42 characters wide only when each entry's body-length field happens to render as two digits (total salted string length between 10 and 99 characters at that insertion step); `SaltHash` appends `systemHash.Length` with no padding or width validation. |
| **Trigger** | `SaltHash` / `BlandHash` |
| **Enforcement** | `SaltShaker.MAP_LENGTH` (`= 42`); `SaltShaker.SaltHash`; `SaltShaker.BlandHash` |
| **Outcome** | `BlandHash` always reads exactly 42 characters as the map, so any entry whose body-length field is not two digits shifts every following map entry and corrupts or breaks decoding; `SaltHash` does not enforce or validate that the salted length stays in the 10–99 range before emitting output. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-02 Fixed hex-encoded date-part lengths

| Field | Value |
| --- | --- |
| **Statement** | Each date part is embedded as lowercase 2-digit hex per character, with fixed encoded lengths: Year 8, Millisecond 6, Minute/Day/Second/Hour/Month 4. |
| **Trigger** | Construction with a create date; `BlandHash` decode |
| **Enforcement** | `SaltShaker._datePartLength`; `SaltShaker.EncodePart`; `SaltShaker.DecodePart` |
| **Outcome** | Encoded parts round-trip to the original formatted digits. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-03 Millisecond-precision, 24-hour, culture-sensitive timestamp

| Field | Value |
| --- | --- |
| **Statement** | The embedded timestamp has millisecond precision and uses 24-hour hours; it is captured with culture-sensitive `DateTime.ToString` and reconstructed with culture-sensitive `Convert.ToDateTime("yyyy/MM/dd HH:mm:ss.fff")`. No time-zone/`DateTimeKind` information is embedded.* |
| **Trigger** | `SaltShaker(DateTime)`; `BlandHash` |
| **Enforcement** | `SaltShaker..ctor(DateTime)`; `SaltShaker.BlandHash` |
| **Outcome** | The recovered `DateTime` equals the create date to the millisecond under a culture whose date/time parsing accepts this layout; the recovered `DateTimeKind` is unspecified/local.* |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-04 Constructor split between salting and de-salting

| Field | Value |
| --- | --- |
| **Statement** | Only a `SaltShaker` constructed with a create date can salt; a parameterless instance can only de-salt. |
| **Trigger** | Calling `SaltHash` on a parameterless instance |
| **Enforcement** | `SaltShaker()` leaves `_dateParts` `null`; `SaltShaker.SaltHash` iterates `_dateParts` without a null guard |
| **Outcome** | Calling `SaltHash` on a parameterless instance throws an unguarded `NullReferenceException`. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-05 Random insertion position per part

| Field | Value |
| --- | --- |
| **Statement** | Each date part is inserted at a uniformly random index in `[0, currentLength)` of the progressively salted string, so placement is randomized rather than fixed across calls. |
| **Trigger** | Each loop iteration in `SaltHash` |
| **Enforcement** | `SaltShaker.SaltHash` using `System.Random` |
| **Outcome** | Output is randomized and typically differs between salts of the same hash and date, but `Random.Next` can produce the same index or pad character on different calls — the source performs no duplicate-detection or uniqueness check, so distinct output is not guaranteed. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-06 Insertion and removal order

| Field | Value |
| --- | --- |
| **Statement** | Parts are inserted in order Minute→Year→Day→Millisecond→Second→Hour→Month and removed in exact reverse order using the reversed map, so every recorded index is valid at removal time. |
| **Trigger** | `SaltHash`, `BlandHash` |
| **Enforcement** | `SaltShaker._dateParts` population order; `SaltShaker._datePartLength` order; `SaltShaker.BlandHash` reversal and `SplitAndReverse` |
| **Outcome** | Lossless recovery of the original hash. Relies on `Dictionary` enumeration order remaining stable.* |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-07 Random pad character, ignored on decode

| Field | Value |
| --- | --- |
| **Statement** | Each map entry carries one random filler character in the range `'A'`–`'Y'` that has no meaning on decode. |
| **Trigger** | `SaltHash` |
| **Enforcement** | `SaltShaker.SaltHash` — `Random.Next(65, 90)` (exclusive upper bound, so `'Z'` is never produced) |
| **Outcome** | Obfuscates the map; ignored by `BlandHash`, which reads only the first 3 characters of each entry. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-08 Map stripped by full-string replace

| Field | Value |
| --- | --- |
| **Statement** | The map is stripped from the salted hash by removing all occurrences of the 42-character map string, not only the prefix. |
| **Trigger** | `BlandHash` |
| **Enforcement** | `SaltShaker.BlandHash` — `string.Replace(map, string.Empty)` |
| **Outcome** | Correct when the map appears only as the prefix (practically always); would corrupt the body if the 42-character sequence recurred elsewhere in the body.* |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

---

## BR-HU-09 Fixed-format expiry message

| Field | Value |
| --- | --- |
| **Statement** | An expired system hash is reported with a fixed message containing the hash's embedded date and the evaluation date, both formatted `MM/dd/yyyy hh:mm:ss.ffffff`. |
| **Trigger** | Consumer constructs and throws the exception |
| **Enforcement** | `SystemHashExpiredException..ctor` |
| **Outcome** | The exception message is fixed as described; the 12-hour clock without an AM/PM designator makes the times ambiguous. The decision to throw is made entirely by `SAIF.NetCore.SystemHash` (`SystemHashService.RemoveSystemHashSaltAndValidateLifetime`), not by this library. |
| **Source** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SystemHashExpiredException.cs` |

## Notes
- No rule on this page is enforced by an inherited base class or service from a dependency package — HashUtil has no package dependencies.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.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.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs`
- `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SystemHashExpiredException.cs`
- `src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs`
