Download this file
| 1 | package seal |
| 2 | |
| 3 | import ( |
| 4 | "crypto/hmac" |
| 5 | "encoding/hex" |
| 6 | ) |
| 7 | |
| 8 | // Fingerprint is a KEYED, stable digest of a value: equal values give |
| 9 | // equal fingerprints, and nothing else can be learned from one. |
| 10 | // |
| 11 | // It exists because an ordinary hash is not a seal. The app already |
| 12 | // stores an unkeyed SHA-256 of an event's UID, which is fine — a UID is |
| 13 | // a random string nobody can guess. An ADDRESS is the opposite: the set |
| 14 | // of real email addresses is small enough to enumerate, so a plain |
| 15 | // SHA-256 of "someone@example.com" is that address written in a |
| 16 | // slightly inconvenient alphabet. Publishing a column of them would |
| 17 | // give up exactly what the one rule promises to keep: who this team |
| 18 | // meets with. |
| 19 | // |
| 20 | // HMAC under an HKDF-derived purpose key makes the digest useless to |
| 21 | // anyone without the DEK, while still comparing equal for equal inputs |
| 22 | // — which is all a dedupe or a lookup needs. |
| 23 | // |
| 24 | // The DEK is therefore load-bearing for lookups as well as for reading: |
| 25 | // lose it and these rows can no longer be matched, exactly as sealed |
| 26 | // columns can no longer be opened. |
| 27 | func (s *Sealer) Fingerprint(purpose, value string) (string, error) { |
| 28 | key, err := s.purposeKey(purpose) |
| 29 | if err != nil { |
| 30 | return "", err |
| 31 | } |
| 32 | mac := hmac.New(sha256New, key) |
| 33 | mac.Write([]byte(value)) |
| 34 | return hex.EncodeToString(mac.Sum(nil)), nil |
| 35 | } |
| 36 | |