Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/platform/orgsettings/secrets.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/platform/orgsettings/secrets.go |
| Original SHA-256 | f3c2054c23f42df2611e686a4f981cbc7c94d51eb0db775f4f16e555076c0b75 |
| Git blob SHA | 6507f8e562183237547411865a850f0b8f9de4e5 |
| Reading-copy SHA-256 | 72a99daea5fe141a3aced05c8801cde593a968c8027efc352c5ea95f0b94f701 |
Recovered source transcript
Download complete source text ↓155 lines · 6.4 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
package orgsettings
import (
"context"
"encoding/hex"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/missionworkforce/missionmatch/internal/platform/security"
)
// PER ORGANIZATION SEALED SECRETS.
//
// A customer's own mail password today. A source connector credential when in place federation lands, which
// is why this is a named key value store rather than two columns bolted onto org_settings.
//
// THE THREAT THIS ACTUALLY ADDRESSES is a database disclosure: a backup that walks, a replica that is read,
// a support query run too broadly. In every one of those the attacker holds rows and not the server env file,
// and ciphertext is useless to them. It does NOT defend against a compromised application process, which by
// definition holds the key, and no design storing a reusable password can, because the password has to be
// presented to the mail server in a form the mail server accepts.
//
// KNOWN NAMES ONLY. The name is constrained by a database CHECK and again here, so a caller cannot invent an
// arbitrary key and turn a bounded credential store into a general purpose blob table nobody audits.
const (
// SecretSMTPPassword authenticates to the organization's own mail server.
SecretSMTPPassword = "[credential removed]"
// imapPrefix names a PER USER mailbox password, suffixed with that user's identifier in hex. A mailbox
// belongs to a person, so unlike the organization's sending credential there is one of these per
// recruiter, and the name has to carry which.
imapPrefix = "imap_password."
)
// IMAPSecretName builds the storage name for one user's mailbox password.
//
// HEX, NOT THE DASHED FORM. The database CHECK on org_secrets.name allows lowercase letters, digits,
// underscore and dot, and a UUID's dashes do not match it, so the dashed form would be refused at insert time
// by a constraint far away from here. Hex is unambiguous and reversible.
func IMAPSecretName(userID uuid.UUID) string {
return imapPrefix + hex.EncodeToString(userID[:])
}
// knownSecret bounds what may be stored. A name is either the one organization wide credential or a per user
// mailbox password whose suffix is exactly a hex encoded identifier, so the store cannot become a general
// purpose blob table nobody audits.
func knownSecret(name string) bool {
if name == SecretSMTPPassword {
return true
}
if !strings.HasPrefix(name, imapPrefix) {
return false
}
suffix := strings.TrimPrefix(name, imapPrefix)
if len(suffix) != 32 {
return false
}
_, err := hex.DecodeString(suffix)
return err == nil
}
// PutSecret seals value under the active key and stores it, replacing any previous value under that name.
//
// The plaintext is never written anywhere: not to the row, not to a log, not to the audit trail. The audit
// entry for this action is written by the CALLER and records the name only, which is the whole point of
// having names.
func (s *Store) PutSecret(ctx context.Context, tx pgx.Tx, name, value string, actor uuid.UUID) error {
if !knownSecret(name) {
return fmt.Errorf("orgsettings: refusing to store unknown secret name %q", name)
}
if !s.SealAvailable() {
return ErrSealUnavailable
}
if value == "" {
return errors.New("orgsettings: refusing to store an empty secret, remove it instead")
}
ct, nonce, err := security.SealSecret(s.kek, []byte(value))
if err != nil {
// The error from the sealing primitive is returned WITHOUT wrapping the value anywhere near it.
return fmt.Errorf("orgsettings: seal %s: %w", name, err)
}
_, err = tx.Exec(ctx, `
INSERT INTO org_secrets (organization_id, name, ciphertext, nonce, key_version, updated_by)
VALUES (NULLIF(current_setting('app.current_org_id', true), '')::uuid, $1, $2, $3, $4, $5)
ON CONFLICT (organization_id, name) DO UPDATE
SET ciphertext = EXCLUDED.ciphertext, nonce = EXCLUDED.nonce,
key_version = EXCLUDED.key_version, updated_by = EXCLUDED.updated_by, updated_at = now()`,
name, ct, nonce, s.kekVersion, actor)
if err != nil {
return fmt.Errorf("orgsettings: store %s: %w", name, err)
}
return nil
}
// OpenSecret returns the plaintext for name. It is called by the mail sender and by nothing that serves an
// HTTP response.
//
// A value sealed under the PREVIOUS key still opens, which is what makes rotation possible without a flag
// day: deploy the new key with the old one alongside, and each secret re seals the next time it is written.
// A value whose key_version matches neither is a real failure and is reported as one, because silently
// returning nothing would look identical to a customer who never configured mail.
func (s *Store) OpenSecret(ctx context.Context, tx pgx.Tx, name string) (string, error) {
if !knownSecret(name) {
return "", fmt.Errorf("orgsettings: unknown secret name %q", name)
}
var ct, nonce []byte
var ver int
err := tx.QueryRow(ctx,
`SELECT ciphertext, nonce, key_version FROM org_secrets WHERE name = $1`, name).
Scan(&ct, &nonce, &ver)
if errors.Is(err, pgx.ErrNoRows) {
return "", ErrNoSecret
}
if err != nil {
return "", fmt.Errorf("orgsettings: read %s: %w", name, err)
}
key := s.keyFor(ver)
if key == nil {
return "", fmt.Errorf("orgsettings: %s was sealed under key version %d, which this node does not hold", name, ver)
}
pt, err := security.OpenSecret(key, ct, nonce)
if err != nil {
return "", fmt.Errorf("orgsettings: open %s: %w", name, err)
}
return string(pt), nil
}
// keyFor picks the key that sealed a given version, or nil when neither key matches. The comparison is on the
// stored version rather than on trial decryption, so a wrong key is a clear error rather than a GCM
// authentication failure that reads like corruption.
func (s *Store) keyFor(version int) []byte {
switch {
case version == s.kekVersion && len(s.kek) == security.AESKeySize:
return s.kek
case version == s.kekVersion-1 && len(s.prev) == security.AESKeySize:
return s.prev
default:
return nil
}
}
// DeleteSecret removes a stored credential. This is the one place in the schema with a DELETE grant, and the
// reason is in the migration: a revoked credential that is merely marked revoked is still one key disclosure
// away from being read, so revocation removes the ciphertext.
func (s *Store) DeleteSecret(ctx context.Context, tx pgx.Tx, name string) error {
if !knownSecret(name) {
return fmt.Errorf("orgsettings: unknown secret name %q", name)
}
_, err := tx.Exec(ctx, `DELETE FROM org_secrets WHERE name = $1`, name)
return err
}