Add logging and encryption rules
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -204,6 +204,94 @@ Each entity gets a unique `uint16` constant in `entity_type_reg.go`. **Never reu
|
||||
|
||||
**Avoid default values.** Columns should not have `DEFAULT` clauses. When adding a non-nullable column to an existing table, use a `DEFAULT` to backfill existing rows, then drop it in the same migration.
|
||||
|
||||
## Sensitive data protection
|
||||
|
||||
Every column that stores a secret, credential, or private key **must** be protected at rest in the application layer. Never store sensitive values as plaintext in the database. There are three protection strategies depending on the data's nature.
|
||||
|
||||
### Strategy 1 — SHA-256 hash (high-entropy tokens)
|
||||
|
||||
Use for values generated by the application with guaranteed entropy: bearer tokens, API keys, SCIM tokens, one-time tokens, SAML relay state tokens. These values are random and never chosen by a human, so a fast non-reversible hash is sufficient.
|
||||
|
||||
- Package: `pkg/crypto/hash` → `hash.SHA256Hex([]byte) string`
|
||||
- DB column type: `BYTEA` (store the raw hash bytes) or `TEXT` (store hex-encoded hash)
|
||||
- Go field name: `Hashed*` (e.g. `HashedToken`, `HashedValue`)
|
||||
- Lookup: compute SHA-256 of the presented token, then `WHERE hashed_token = @hashed_token`
|
||||
- The plaintext token is returned to the caller **once** at creation time and never stored
|
||||
|
||||
Existing examples: `Token.HashedValue`, `SCIMConfiguration.HashedToken`.
|
||||
|
||||
```go
|
||||
// At creation time — hash before insert
|
||||
hashedValue := hash.SHA256Hex([]byte(rawToken))
|
||||
token.HashedValue = []byte(hashedValue)
|
||||
|
||||
// At verification time — hash the presented value, then query
|
||||
hashedValue := hash.SHA256Hex([]byte(presentedToken))
|
||||
token.LoadByHashedValueForUpdate(ctx, conn, []byte(hashedValue))
|
||||
```
|
||||
|
||||
### Strategy 2 — PBKDF2 (human-chosen passwords)
|
||||
|
||||
Use for values chosen by humans with low or unpredictable entropy: passwords, passphrases, PINs. PBKDF2 with HMAC-SHA256 pepper provides brute-force resistance.
|
||||
|
||||
- Package: `pkg/crypto/passwdhash`
|
||||
- DB column type: `BYTEA NOT NULL`
|
||||
- Go field name: `HashedPassword`
|
||||
- Hash on write: `profile.HashPassword([]byte(password))`
|
||||
- Compare on read: `profile.ComparePasswordAndHash([]byte(password), storedHash)`
|
||||
- Parameters: minimum 600 000 iterations, 32-byte salt, 32-byte pepper
|
||||
|
||||
Existing example: `Identity.HashedPassword`.
|
||||
|
||||
```go
|
||||
// At registration / password change
|
||||
hashed, err := passwdProfile.HashPassword([]byte(plainPassword))
|
||||
identity.HashedPassword = hashed
|
||||
|
||||
// At login
|
||||
ok, err := passwdProfile.ComparePasswordAndHash([]byte(inputPassword), identity.HashedPassword)
|
||||
```
|
||||
|
||||
### Strategy 3 — AES-256-GCM encryption (secrets that must be read back)
|
||||
|
||||
Use for values the application needs to decrypt later: OAuth `access_token` / `refresh_token`, `client_secret`, API keys for third-party services, TLS private keys, webhook signing secrets.
|
||||
|
||||
- Package: `pkg/crypto/cipher`
|
||||
- DB column type: `BYTEA NOT NULL`
|
||||
- Go field name: `Encrypted*` (e.g. `EncryptedConnection`, `EncryptedSigningSecret`)
|
||||
- Encrypt on write: `cipher.Encrypt(plaintext, encryptionKey)`
|
||||
- Decrypt on read: `cipher.Decrypt(ciphertext, encryptionKey)`
|
||||
- The `cipher.EncryptionKey` is a 32-byte key loaded from configuration — never stored in the database
|
||||
|
||||
Existing examples: `Connector.EncryptedConnection`, `WebhookSubscription.EncryptedSigningSecret`, `CustomDomain.EncryptedSSLPrivateKey`.
|
||||
|
||||
```go
|
||||
// On insert / update — encrypt before writing
|
||||
connection, _ := json.Marshal(c.Connection)
|
||||
encrypted, err := cipher.Encrypt(connection, encryptionKey)
|
||||
c.EncryptedConnection = encrypted
|
||||
|
||||
// On load — decrypt after reading
|
||||
plaintext, err := cipher.Decrypt(c.EncryptedConnection, encryptionKey)
|
||||
json.Unmarshal(plaintext, &c.Connection)
|
||||
```
|
||||
|
||||
### Decision table
|
||||
|
||||
| Data kind | Entropy source | Needs decryption? | Strategy | Go field prefix | Package |
|
||||
|-----------|---------------|-------------------|----------|----------------|---------|
|
||||
| Bearer / API / SCIM / one-time tokens | Application CSPRNG | No — compare by hash | SHA-256 | `Hashed*` | `pkg/crypto/hash` |
|
||||
| Passwords, passphrases | Human | No — compare with constant-time check | PBKDF2 | `HashedPassword` | `pkg/crypto/passwdhash` |
|
||||
| OAuth tokens, client secrets, private keys, signing secrets | External provider or application | Yes — must read back | AES-256-GCM | `Encrypted*` | `pkg/crypto/cipher` |
|
||||
|
||||
### Rules
|
||||
|
||||
- **Never store a plaintext secret** in a `TEXT` or `VARCHAR` column. If a column holds a secret, it must be `BYTEA` with one of the three strategies above.
|
||||
- **Never log sensitive values.** Do not pass raw tokens, passwords, or decrypted secrets to `slog` or `fmt.Errorf` messages.
|
||||
- **Name columns and fields consistently.** Use `hashed_` prefix for hashed values and `encrypted_` prefix for encrypted values. The Go struct field must mirror this (e.g. `HashedToken`, `EncryptedConnection`).
|
||||
- **Return plaintext tokens once.** For SHA-256-hashed tokens, return the raw token to the caller at creation time only. After that, the application only ever sees the hash.
|
||||
- **Migration columns.** When adding a new sensitive column, always use `BYTEA`. Never add `DEFAULT` on sensitive columns.
|
||||
|
||||
## New entity checklist
|
||||
|
||||
1. **Entity file** (`entity.go`) — struct with `db` tags, slice type alias, `LoadByID`, `Insert`, `Update`, `Delete`, `CursorKey`, `AuthorizationAttributes`
|
||||
|
||||
Reference in New Issue
Block a user