---
title: De-salt a system hash and recover the embedded date
summary: Strip the location map from a salted hash, recover each hex-encoded date part in reverse insertion order, and reconstruct the original hash and create date.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.HashUtil
reference_type: package
operation_id: OP-HU-04
trigger: Called after constructing a de-salting SaltShaker
---

# De-salt a system hash and recover the embedded date

This operation (`BlandHash`) reads the 42-character location map off the front of a salted hash, walks it in reverse insertion order to remove and hex-decode each embedded date part, and reconstructs both the original (unsalted) hash and the create date that was embedded at salting time. In-repo, `SystemHashService.RemoveSystemHashSaltAndValidateLifetime` calls this immediately after constructing a de-salting `SaltShaker` (OP-HU-02).

| Field | Value |
| --- | --- |
| **Operation ID** | OP-HU-04 |
| **Trigger** | Called after OP-HU-02 (in-repo: `SystemHashService.RemoveSystemHashSaltAndValidateLifetime`) |
| **Inputs** | `saltyHash` produced by OP-HU-03 |
| **Outputs** | Return value: original (unsalted) hash; `out expireDate`: the create date embedded at salting time |
| **Primary components** | `SaltShaker.BlandHash(string saltyHash, out DateTime expireDate)` |
| **Entry point** | `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs` |

## Steps

1. Take the first 42 characters of `saltyHash` as the location map ([BR-HU-01](../business-rules.md#br-hu-01-fixed-42-character-location-map-prefix)).
   - On failure: a `saltyHash` shorter than 42 characters throws an `ArgumentOutOfRangeException`.
2. Split the map into 6-character chunks and reverse their order, producing entries in Month → Minute order ([BR-HU-01](../business-rules.md#br-hu-01-fixed-42-character-location-map-prefix), [BR-HU-06](../business-rules.md#br-hu-06-insertion-and-removal-order)).
3. Remove every occurrence of the map string from `saltyHash` ([BR-HU-08](../business-rules.md#br-hu-08-map-stripped-by-full-string-replace)).
4. Enumerate the date-part lengths in reverse order (Month, Hour, Second, Millisecond, Day, Year, Minute), with a parallel index into the reversed map entries ([BR-HU-06](../business-rules.md#br-hu-06-insertion-and-removal-order)). For each part:
   1. Parse the insertion index from the first 3 characters of the map entry; the pad character and body-length digits are ignored.
   2. Read the appropriate number of characters (4, 6, or 8, depending on the part) at that index and hex-decode them ([BR-HU-02](../business-rules.md#br-hu-02-fixed-hex-encoded-date-part-lengths)).
      - On failure: malformed hexadecimal digits in the salted body produce a `FormatException`; an out-of-range insertion index produces an `ArgumentOutOfRangeException`.
   3. Remove those characters from the working string, undoing the insertion in LIFO order.
   4. Map the dictionary key back to the matching local variable (`minute`, `year`, `day`, `millisecond`, `second`, `hour`, `month`).
5. Build the recovered `DateTime` from the decoded parts ([BR-HU-03](../business-rules.md#br-hu-03-millisecond-precision%2C-24-hour%2C-culture-sensitive-timestamp)).
   - On failure: an unparseable date string under the current culture produces a `FormatException`.
6. Return the remaining string as the original (unsalted) hash.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([Called after OP-HU-02]) --> S1[Take first 42 chars as map]
	S1 --> S2["Split map into 6-char chunks,<br>reverse to Month..Minute order"]
	S2 --> S3[Remove map string from saltyHash]
	S3 --> Loop{For each part in<br>Month..Minute order}
	Loop -->|next part| S4[Parse insertion index from map entry]
	S4 --> S5["Read + hex-decode date-part chars"]
	S5 --> S6[Remove decoded chars from working string]
	S6 --> Loop
	Loop -->|all parts done| S7[Build recovered DateTime]
	S7 --> S8[Return remaining string as original hash]
	S8 --> EndOk(["Original hash + create date returned"])
	S1 -->|saltyHash < 42 chars| EndErr([ArgumentOutOfRangeException])
```

## Failure and edge-case behavior
- `saltyHash` shorter than 42 characters: `ArgumentOutOfRangeException` at step 1.
- Malformed decimal digits in the location map (step 4.1 index parse): `FormatException`.
- Malformed hexadecimal digits in the salted body (step 4.2 date-part decode): `FormatException`.
- Out-of-range insertion index used as a substring/removal bound: `ArgumentOutOfRangeException` at steps 4.2–4.3 (step 4.1 only converts the fixed three-character decimal field and does not itself use the parsed value as an index).
- Unparseable date string under the current culture: `FormatException` at step 5.
- `null` input: `NullReferenceException`.
- This operation performs no lifetime check of its own and never throws `SystemHashExpiredException`; that decision belongs entirely to the calling consumer (see [Signal an expired system hash](op-hu-05-signal-expired-system-hash.md)).

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

## Sources
- `src/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs`
- `src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs`
