Repositories with sqlc; the OMANG hash
generate type-safe Go from SQL with sqlc, wrap it in a citizens
repository, and implement the blind-index helper that lets you check "is this OMANG/phone already registered?" without storing the raw value.
Concepts — sqlc codegen, the repository pattern in Go (interface in the consumer package), HMAC blind indexes, separating "store" from "domain".
Idiom note: Go's repository pattern is lighter than what you may know from other ecosystems. Define the interface where it's used (the service), and let the concrete struct live in store. No DI container needed.
Build
sqlc.yaml:
version: "2"
sql:
- engine: "postgresql"
schema: "migrations"
queries: "query"
gen:
go:
package: "sqlcgen"
out: "internal/store/sqlcgen"
sql_package: "pgx/v5"
emit_pointers_for_null_types: truequery/citizens.sql
query/citizens.sql:
-- name: CreateCitizen :one
INSERT INTO citizens (phone_blind_idx, phone_enc, password_hash, dek_wrapped, kek_version)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, status, created_at;
-- name: GetCitizenByPhoneIdx :one
SELECT * FROM citizens WHERE phone_blind_idx = $1;
-- name: CitizenExistsByOmangIdx :one
SELECT EXISTS(SELECT 1 FROM citizens WHERE omang_blind_idx = $1);
-- ⟵ YOU: add SetOmangIdx (used by the verification pipeline in lab 10)internal/crypto/blindindex.go
internal/crypto/blindindex.go:
package crypto
import (
"crypto/hmac"
"crypto/sha256"
)
// BlindIndex is a deterministic keyed hash: same input + pepper → same output,
// but the input can't be recovered. Lets us enforce UNIQUE on a value we never
// store in plaintext. The pepper is a server secret (loaded in lab 7), NOT in DB.
func BlindIndex(pepper []byte, value string) []byte {
mac := hmac.New(sha256.New, pepper)
mac.Write([]byte(value)) // ⟵ YOU: should you normalize `value` first (trim, lowercase, E.164)? decide & document
return mac.Sum(nil)
}internal/store/citizens.go
internal/store/citizens.go — wrap sqlcgen:
type CitizenRepo struct{ q *sqlcgen.Queries }
func (r *CitizenRepo) Create(ctx context.Context, in NewCitizen) (Citizen, error) {
// ⟵ YOU: map domain struct → sqlcgen params → domain result.
// Keep sqlcgen types from leaking out of the store package.
}Research checkpoints
; understand :one :many :exec annotations and how nullable columns map to Go (pointers vs pgtype).
(Search "blind index HMAC vs hash"; the pepper is the point — defends against offline dictionary attacks on a stolen DB. Botswana phone space is tiny.)
if two users enter the same phone differently (+267... vs 267...), do their blind indexes match? They must, or UNIQUE is useless. Decide the canonical form and where you enforce it.
read a short piece on "accept interfaces, return structs" in Go.
Verify
sqlc generateproduces compiling Go;make tidyclean.- A unit test:
BlindIndex(pepper, "+26771234567")is stable across runs
and differs for a different pepper.
- Creating two citizens with the same normalized phone → the second fails
on the UNIQUE constraint (caught and surfaced as a clean 409, not a 500).
grep -rconfirms no raw phone/OMANG string is ever passed to an INSERT
except as *_enc (encrypted, lab 7) or *_blind_idx.