De-salt and validate credential lifetime¶
Recovers the embedded create-timestamp and unsalted identity hash from a credential previously issued by Compute a salted system hash, and rejects the credential if it falls outside the allowed lifetime window.
| Field | Value |
|---|---|
| Operation ID | OP-SH-06 |
| Trigger | Direct call by a validating service (exercised in-repo only by this library's unit tests). |
| Inputs | systemHashValue (salted credential), expireSeconds, nowBufferSeconds |
| Outputs | Unsalted 44-char identity hash, or a thrown SystemHashExpiredException |
| Primary components | SystemHashService.RemoveSystemHashSaltAndValidateLifetime |
| Entry point | src/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SAIF.NetCore.SystemHash/SystemHashService.cs |
Steps¶
- Construct a
SaltShakerusing the parameterless constructor. This only creates a newSystem.Randominstance and leaves_datePartsnull; it does not read, parse, or otherwise touchsystemHashValue— parsing begins only in the next step, insideBlandHash.
Origin: This step is implemented in Construct a de-salting SaltShaker (
SAIF.NetCore.HashUtil) and executed on behalf of this operation. 2. CallBlandHash(systemHashValue, out createDate)to parse and de-salt the credential, recovering the embedded create timestamp and the plain identity hash (BR-SH-03). - Sub-steps embedded fromSAIF.NetCore.HashUtil: - Take the first 42 characters ofsystemHashValueas the location map. - Split the map into 6-character chunks and reverse their order, producing entries in Month → Minute order. - Remove every occurrence of the map string fromsystemHashValue(a full-stringReplace, not a single removal). - For each date part in Month, Hour, Second, Millisecond, Day, Year, Minute order: parse the 3-digit insertion index from the corresponding map entry (ignoring the pad character and body-length digits also recorded in that entry), read the correct number of characters (4, 6, or 8, depending on the part) at that index, hex-decode them, and remove those characters from the working string. - Parse the reconstructed"yyyy/MM/dd HH:mm:ss.fff"string into aDateTimeusing the current culture (Convert.ToDateTime). - On a malformed input: raiseArgumentOutOfRangeException,FormatException, orNullReferenceException— notSystemHashExpiredException— because this method performs no try/catch around the call (BR-SH-09).
> **Origin**: The sub-steps above are implemented in [De-salt a system hash and recover the embedded date](../../saif-netcore-hashutil/operations/op-hu-04-de-salt-a-system-hash.md) (`SAIF.NetCore.HashUtil`) and executed on behalf of this operation.
- Compare the recovered create timestamp against the allowed lifetime window: valid only if
createDate ≤ now + abs(nowBufferSeconds)ANDnow − abs(nowBufferSeconds) < createDate + expireSeconds(BR-SH-08). This simplified predicate holds for everynowBufferSecondsvalue exceptint.MinValue, provided the date shifts stay within theDateTimerange. The literal source callscreateDate.AddSeconds(expireSeconds)andnow.AddSeconds(...)directly, and those calls throwArgumentOutOfRangeExceptionif a shifted value falls outside theDateTimerange. It also computesnowBufferSeconds * -1in an unchecked integer context: the project does not setCheckForOverflowUnderflowand the method has nocheckedblock. That negation never throwsOverflowException. Atint.MinValueit wraps silently back toint.MinValue, so both arms of each bound check shiftnowback by about 68 years and the window is no longer symmetric — but the method does not special-case this value; it is still evaluated through the same shifted-window predicate as any othernowBufferSeconds. A current credential'screateDatestill fails that shifted predicate and is rejected, but the outcome is conditional, not unconditional: a credential with acreateDateroughly 68+ years in the past, combined with a sufficiently largeexpireSeconds, can satisfy the shifted predicate and pass the check. -
If the window check fails:
- Raise a
SystemHashExpiredException(now, createDate), unless aDateTime.AddSecondscall above has already thrownArgumentOutOfRangeExceptionbecause a shifted date fell outside theDateTimerange. AnowBufferSecondsofint.MinValuedoes not throw — the negation wraps silently — and this path is reached the same way as any other failingnowBufferSeconds: a current credential is rejected through thisSystemHashExpiredExceptionpath, but a crafted old credential (see step 3) can instead satisfy the window check and avoid this path entirely.
Origin: The exception type and its construction semantics are implemented in Signal an expired system hash (
SAIF.NetCore.HashUtil) and raised on behalf of this operation. 4. If the window check succeeds, return the unsalted identity hash recovered in step 2 (BR-SH-10). - The caller — not this operation — is responsible for comparing the returned hash against an authorized/ComputeHashNowvalue; this operation only enforces freshness. - Raise a
Process flow¶
The following diagram shows the lifetime-check path from a received salted credential to acceptance, expiry, or an uncaught date-range failure. The method checks freshness only; it does not issue credentials or compare against an external identity. If createDate.AddSeconds(expireSeconds) falls outside the representable DateTime range, DateTime.AddSeconds throws ArgumentOutOfRangeException before the window predicate is evaluated. That exception propagates uncaught rather than becoming SystemHashExpiredException. The nowBufferSeconds * -1 negation never throws, because integer arithmetic here is unchecked. At int.MinValue it wraps back to int.MinValue, so both bound checks shift now back by about 68 years. The method does not special-case this value in code, but because both arms of the window check then collapse onto the same shifted value, the diagram shows it as its own comparison, shiftedNow = now.AddSeconds(int.MinValue); shiftedNow >= createDate && shiftedNow < expires, rather than the simplified symmetric predicate used for every other nowBufferSeconds. A current credential still fails that shifted comparison and is rejected, but the outcome is conditional, not unconditional: a credential with a createDate far enough in the past, combined with a sufficiently large expireSeconds, can satisfy the shifted predicate and be accepted.
flowchart TD
Start([Validator receives salted credential]) --> ConstructDesalt[Construct de-salting SaltShaker]
ConstructDesalt --> Desalt["De-salt: recover createDate and unsalted hash"]
Desalt --> Malformed{Well-formed HashUtil format?}
Malformed -->|No| BclError(["BCL exception propagates: Argument / Format / NullReference"])
Malformed -->|Yes| Domain{"createDate + expireSeconds<br>within DateTime range?"}
Domain -->|No| ArithError(["Uncaught ArgumentOutOfRangeException<br>from DateTime.AddSeconds"])
Domain -->|Yes| MinVal{"nowBufferSeconds<br>== int.MinValue?"}
MinVal -->|Yes, wraps to int.MinValue| MinValWindow{"shiftedNow = now.AddSeconds(int.MinValue);<br>shiftedNow >= createDate && shiftedNow < expires?"}
MinVal -->|No| Window{"createDate within<br>(now − abs(buffer) − expireSeconds, now + abs(buffer)]?"}
Window -->|No| Expired([Throw SystemHashExpiredException])
Window -->|Yes| ReturnHash(["Return unsalted hash — caller must compare separately"])
MinValWindow -->|Yes| ReturnHash
MinValWindow -->|No| Expired
Failure and edge-case behavior¶
- A malformed credential fails with a BCL exception type, not
SystemHashExpiredException, because de-salting failures are not caught (BR-SH-09). - The window check's two
ORclauses are symmetric in the sign ofnowBufferSeconds(the literal source evaluates thenowBufferSeconds * -1arm first in each bound check and short-circuits, evaluating the plainnowBufferSecondsarm only when that first arm is false), so a negativenowBufferSecondsproduces the same window as a positive buffer of equal magnitude — it does not narrow the window (BR-SH-08). This symmetry holds for everynowBufferSecondsvalue exceptint.MinValue. Because the arithmetic is unchecked (noCheckForOverflowUnderflowproject setting and nocheckedblock),int.MinValue * -1wraps silently toint.MinValueinstead of throwingOverflowException. Both arms then shiftnowback by about 68 years, so the window moves into the past — but this value is not special-cased to an unconditional outcome, it is still evaluated through the same shifted-window predicate as any othernowBufferSeconds. A current credential'screateDatestill fails that shifted predicate and is rejected withSystemHashExpiredException, but a credential whosecreateDateis itself roughly 68+ years in the past, combined with a sufficiently largeexpireSeconds, can satisfy the shifted predicate and be accepted instead (BR-SH-08). Separately, anyAddSecondsshift that falls outside theDateTimerange throwsArgumentOutOfRangeException. There is no guard against a zero or negativeexpireSeconds; such a value shrinks or eliminates the acceptance window rather than causing the check to always pass or always fail based on sign alone.* - Returning a hash from this operation does not by itself authorize the caller — the comparison step is the validating service's own responsibility (BR-SH-10).
Related documentation¶
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.cssrc/SAIF.NetCore.HashUtil/SAIF.NetCore.HashUtil/SaltShaker.cs,SystemHashExpiredException.cs