| | | 1 | | using System.Security.Cryptography; |
| | | 2 | | using System.Text; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.Shared.Security; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Turns an email address into a stable reference that can be written to a log without writing |
| | | 8 | | /// the address (NFR-22). |
| | | 9 | | /// </summary> |
| | | 10 | | /// <remarks> |
| | | 11 | | /// <para> |
| | | 12 | | /// <b>Why not the address.</b> A log line saying a password reset was sent to a named person |
| | | 13 | | /// is personal data — the address identifies them, and the line says something about them. |
| | | 14 | | /// The files are retained for a year and are read by operators, so the address would sit |
| | | 15 | | /// there, in bulk, long after the operational question it answered was closed. |
| | | 16 | | /// </para> |
| | | 17 | | /// <para> |
| | | 18 | | /// <b>Why not simply drop it.</b> Operators need to correlate: whether the three failures in |
| | | 19 | | /// the log are one address retrying or three different people is the first question asked of |
| | | 20 | | /// a delivery problem, and a line with no reference at all cannot answer it. A stable |
| | | 21 | | /// reference keeps the correlation and drops the disclosure. |
| | | 22 | | /// </para> |
| | | 23 | | /// <para> |
| | | 24 | | /// <b>What this is and is not.</b> It is the first twelve hex characters of the SHA-256 of |
| | | 25 | | /// the normalised address. It is not reversible, and it defeats bulk harvesting: a reader of |
| | | 26 | | /// the logs cannot recover the addresses. It does <em>not</em> defeat confirmation — somebody |
| | | 27 | | /// who already suspects a particular address can hash it and look. That is an accepted |
| | | 28 | | /// limit, and the honest way to describe this is pseudonymisation rather than anonymisation. |
| | | 29 | | /// Anything stronger would need a keyed hash and a key to manage, which buys little against |
| | | 30 | | /// a threat model where the log reader is an operator rather than an attacker. |
| | | 31 | | /// </para> |
| | | 32 | | /// </remarks> |
| | | 33 | | public static class LogSafeEmail |
| | | 34 | | { |
| | | 35 | | /// <summary>Hex characters kept. Twelve is 48 bits — ample against accidental collision.</summary> |
| | | 36 | | private const int ReferenceLength = 12; |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// A stable, non-reversible reference to <paramref name="email" />, or <c>"(none)"</c> when |
| | | 40 | | /// there is no address to refer to. |
| | | 41 | | /// </summary> |
| | | 42 | | /// <remarks> |
| | | 43 | | /// Normalised to lower case and trimmed first, so the same address logged from two call |
| | | 44 | | /// sites that disagree about casing still correlates. |
| | | 45 | | /// </remarks> |
| | | 46 | | public static string Reference(string? email) |
| | | 47 | | { |
| | 150 | 48 | | if (string.IsNullOrWhiteSpace(email)) |
| | | 49 | | { |
| | 3 | 50 | | return "(none)"; |
| | | 51 | | } |
| | | 52 | | |
| | 147 | 53 | | var digest = SHA256.HashData(Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant())); |
| | | 54 | | |
| | 147 | 55 | | return Convert.ToHexStringLower(digest)[..ReferenceLength]; |
| | | 56 | | } |
| | | 57 | | } |