| | | 1 | | using System.Security.Cryptography; |
| | | 2 | | |
| | | 3 | | namespace ArturRios.Heimdall.Command.Services; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Generates and hashes the secrets of UC-53 (FR-AP-10, Threat Model TH-33). |
| | | 7 | | /// </summary> |
| | | 8 | | /// <remarks> |
| | | 9 | | /// <para> |
| | | 10 | | /// <b>Shape.</b> <see cref="Prefix" /> plus 32 bytes from a cryptographic generator, |
| | | 11 | | /// base64url-encoded without padding. The prefix makes a leaked secret recognisable — to a |
| | | 12 | | /// person reading a log and to a secret scanner — and base64url keeps it safe in a URL, a |
| | | 13 | | /// header or an environment file. |
| | | 14 | | /// </para> |
| | | 15 | | /// <para> |
| | | 16 | | /// <b>Why a fast hash.</b> 256 random bits cannot be guessed, so a slow password hash buys |
| | | 17 | | /// nothing and would put UC-56 behind the Argon2id gate the password logins share (NFR-18). |
| | | 18 | | /// A credential is found by looking its hash up, so no comparison runs whose duration |
| | | 19 | | /// depends on how much of a submitted secret matches. |
| | | 20 | | /// </para> |
| | | 21 | | /// </remarks> |
| | | 22 | | public static class ApplicationCredentialSecret |
| | | 23 | | { |
| | | 24 | | /// <summary>The prefix every secret starts with.</summary> |
| | | 25 | | public const string Prefix = "hds_"; |
| | | 26 | | |
| | | 27 | | private const int SecretBytes = 32; |
| | | 28 | | |
| | | 29 | | /// <summary>A new secret, shown to the issuer once and never stored.</summary> |
| | | 30 | | public static string New() => |
| | 74 | 31 | | Prefix + Convert.ToBase64String(RandomNumberGenerator.GetBytes(SecretBytes)) |
| | 74 | 32 | | .TrimEnd('=') |
| | 74 | 33 | | .Replace('+', '-') |
| | 74 | 34 | | .Replace('/', '_'); |
| | | 35 | | |
| | | 36 | | /// <summary>The value stored in <c>ApplicationCredential.SecretHash</c> and looked up by UC-56.</summary> |
| | 104 | 37 | | public static string HashOf(string secret) => SingleUseTokenHash.Of(secret); |
| | | 38 | | } |