internal/store/secrets.go

f8b976a97290a20d552056a999511f5d27d8e8ec
gitbay/internal/store/secrets.go history · blame · raw

242 lines · 7030 bytes

  1package store
  2
  3import (
  4	"crypto/sha256"
  5	"database/sql"
  6	"encoding/hex"
  7	"errors"
  8	"fmt"
  9
 10	"gitbay.org/gitbay/internal/seal"
 11)
 12
 13// The additional data of a sealed value is "<table>.<column>:<row key>",
 14// so a value copied into another column or another row does not open.
 15// Each row key is known when the value is written and survives a
 16// repository rename or transfer. Every read and write of a column builds
 17// its additional data through the one function here.
 18
 19func buildSecretAAD(repoID int64, name string) string {
 20	return fmt.Sprintf("build_secrets.value:%d/%s", repoID, name)
 21}
 22
 23func webhookAAD(id int64) string { return fmt.Sprintf("webhooks.secret:%d", id) }
 24
 25func mirrorAAD(id int64) string { return fmt.Sprintf("mirrors.token:%d", id) }
 26
 27// pushTokenAAD names the owner as well as the token, so a handover to
 28// another account reseals the token.
 29func pushTokenAAD(userID int64, hash string) string {
 30	return fmt.Sprintf("push_devices.token:%d/%s", userID, hash)
 31}
 32
 33type secretColumn struct {
 34	table, column string
 35	// key selects the two parts of the row key, an integer and a text.
 36	key string
 37	aad func(n int64, s string) string
 38}
 39
 40// secretColumns are the columns sealed under the key file (#273).
 41var secretColumns = []secretColumn{
 42	{"build_secrets", "value", "repo_id, name", buildSecretAAD},
 43	{"webhooks", "secret", "id, ''", func(id int64, _ string) string { return webhookAAD(id) }},
 44	{"mirrors", "token", "id, ''", func(id int64, _ string) string { return mirrorAAD(id) }},
 45	{"push_devices", "token", "user_id, COALESCE(token_hash, '')", pushTokenAAD},
 46}
 47
 48// SetKeyring sets the keys the secret columns are sealed under.
 49func (s *Store) SetKeyring(k *seal.Keyring) { s.secrets = k }
 50
 51// sealValue seals v for storage. An empty value stays empty: for
 52// webhooks and mirrors it means there is no secret.
 53func (s *Store) sealValue(aad, v string) (string, error) {
 54	if s.secrets == nil || v == "" {
 55		return v, nil
 56	}
 57	return s.secrets.Seal(aad, v)
 58}
 59
 60// openValue returns a stored value in clear. A value not yet sealed is
 61// returned as stored: rows from before sealing existed stay readable
 62// until ResealSecrets reaches them.
 63func (s *Store) openValue(aad, v string) (string, error) {
 64	if !seal.IsSealed(v) {
 65		return v, nil
 66	}
 67	if s.secrets == nil {
 68		return "", errors.New("value is sealed and no secret key is loaded")
 69	}
 70	return s.secrets.Open(aad, v)
 71}
 72
 73// tokenHash is the lookup key for a push device token.
 74func tokenHash(token string) string {
 75	sum := sha256.Sum256([]byte(token))
 76	return hex.EncodeToString(sum[:])
 77}
 78
 79type secretRow struct {
 80	rowid int64
 81	value string
 82	aad   string
 83}
 84
 85type queryer interface {
 86	Query(query string, args ...any) (*sql.Rows, error)
 87}
 88
 89func secretRows(q queryer, c secretColumn) ([]secretRow, error) {
 90	rows, err := q.Query(fmt.Sprintf("SELECT rowid, %s, %s FROM %s WHERE %s != ''", c.column, c.key, c.table, c.column))
 91	if err != nil {
 92		return nil, err
 93	}
 94	defer rows.Close()
 95	var out []secretRow
 96	for rows.Next() {
 97		var r secretRow
 98		var n int64
 99		var k string
100		if err := rows.Scan(&r.rowid, &r.value, &n, &k); err != nil {
101			return nil, err
102		}
103		r.aad = c.aad(n, k)
104		out = append(out, r)
105	}
106	return out, rows.Err()
107}
108
109// ResealSecrets fills push_devices.token_hash where it is missing, then
110// seals every clear value in the secret columns and reseals every value
111// not under the key file's current key. It runs in one write
112// transaction: every store write of a secret seals inside its own
113// transaction, so a write either lands before this one and is resealed,
114// or after it and is sealed under the key this one saw. It returns how
115// many values it rewrote.
116func (s *Store) ResealSecrets() (int, error) {
117	if s.secrets == nil {
118		return 0, errors.New("no secret key loaded")
119	}
120	tx, err := s.DB.Begin()
121	if err != nil {
122		return 0, err
123	}
124	defer tx.Rollback()
125	cur, err := s.secrets.CurrentID()
126	if err != nil {
127		return 0, err
128	}
129
130	// A token without a hash was written before sealing, so it is clear.
131	rows, err := tx.Query("SELECT id, token FROM push_devices WHERE token_hash IS NULL")
132	if err != nil {
133		return 0, err
134	}
135	var missing []secretRow
136	for rows.Next() {
137		var r secretRow
138		if err := rows.Scan(&r.rowid, &r.value); err != nil {
139			rows.Close()
140			return 0, err
141		}
142		missing = append(missing, r)
143	}
144	rows.Close()
145	if err := rows.Err(); err != nil {
146		return 0, err
147	}
148	for _, r := range missing {
149		if seal.IsSealed(r.value) {
150			return 0, fmt.Errorf("push_devices row %d: sealed token without a token_hash", r.rowid)
151		}
152		if _, err := tx.Exec("UPDATE push_devices SET token_hash = ? WHERE id = ?", tokenHash(r.value), r.rowid); err != nil {
153			return 0, err
154		}
155	}
156
157	n := 0
158	for _, c := range secretColumns {
159		rows, err := secretRows(tx, c)
160		if err != nil {
161			return 0, err
162		}
163		for _, r := range rows {
164			if id, ok := seal.KeyID(r.value); ok && id == cur {
165				continue
166			}
167			plain, err := s.openValue(r.aad, r.value)
168			if err != nil {
169				return 0, fmt.Errorf("%s.%s row %d: %w", c.table, c.column, r.rowid, err)
170			}
171			sealed, err := s.secrets.Seal(r.aad, plain)
172			if err != nil {
173				return 0, err
174			}
175			if _, err := tx.Exec(fmt.Sprintf("UPDATE %s SET %s = ? WHERE rowid = ?", c.table, c.column), sealed, r.rowid); err != nil {
176				return 0, err
177			}
178			n++
179		}
180	}
181	return n, tx.Commit()
182}
183
184// SecretColumnUse is one secret column's values by the id of the key
185// that sealed them ("" for a value still in clear), and the values that
186// do not open under the loaded key file.
187type SecretColumnUse struct {
188	Column string // "<table>.<column>"
189	ByKey  map[string]int
190	Failed []SecretFailure
191}
192
193// SecretFailure is a stored value that does not open.
194type SecretFailure struct {
195	RowID int64
196	Err   error
197}
198
199// SecretReport opens every value in the secret columns and counts them
200// per column by key id. A value that does not open is listed rather than
201// ending the scan.
202func (s *Store) SecretReport() ([]SecretColumnUse, error) {
203	var out []SecretColumnUse
204	for _, c := range secretColumns {
205		rows, err := secretRows(s.DB, c)
206		if err != nil {
207			return nil, err
208		}
209		u := SecretColumnUse{Column: c.table + "." + c.column, ByKey: map[string]int{}}
210		for _, r := range rows {
211			if _, err := s.openValue(r.aad, r.value); err != nil {
212				u.Failed = append(u.Failed, SecretFailure{RowID: r.rowid, Err: err})
213				continue
214			}
215			id, _ := seal.KeyID(r.value)
216			u.ByKey[id]++
217		}
218		out = append(out, u)
219	}
220	return out, nil
221}
222
223// SecretKeyUse counts the values in the secret columns by the id of the
224// key that sealed them ("" for a value still in clear), opening each
225// one, so a wrong or incomplete key file is an error naming the row.
226func (s *Store) SecretKeyUse() (map[string]int, error) {
227	report, err := s.SecretReport()
228	if err != nil {
229		return nil, err
230	}
231	use := map[string]int{}
232	for _, u := range report {
233		if len(u.Failed) > 0 {
234			f := u.Failed[0]
235			return nil, fmt.Errorf("%s row %d: %w", u.Column, f.RowID, f.Err)
236		}
237		for id, n := range u.ByKey {
238			use[id] += n
239		}
240	}
241	return use, nil
242}