internal/store/secrets.go

v1.43.1
gitbay/internal/store/secrets.go history · blame · raw

245 lines · 7149 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// Keyring is the loaded key file, nil when none is set.
 52func (s *Store) Keyring() *seal.Keyring { return s.secrets }
 53
 54// sealValue seals v for storage. An empty value stays empty: for
 55// webhooks and mirrors it means there is no secret.
 56func (s *Store) sealValue(aad, v string) (string, error) {
 57	if s.secrets == nil || v == "" {
 58		return v, nil
 59	}
 60	return s.secrets.Seal(aad, v)
 61}
 62
 63// openValue returns a stored value in clear. A value not yet sealed is
 64// returned as stored: rows from before sealing existed stay readable
 65// until ResealSecrets reaches them.
 66func (s *Store) openValue(aad, v string) (string, error) {
 67	if !seal.IsSealed(v) {
 68		return v, nil
 69	}
 70	if s.secrets == nil {
 71		return "", errors.New("value is sealed and no secret key is loaded")
 72	}
 73	return s.secrets.Open(aad, v)
 74}
 75
 76// tokenHash is the lookup key for a push device token.
 77func tokenHash(token string) string {
 78	sum := sha256.Sum256([]byte(token))
 79	return hex.EncodeToString(sum[:])
 80}
 81
 82type secretRow struct {
 83	rowid int64
 84	value string
 85	aad   string
 86}
 87
 88type queryer interface {
 89	Query(query string, args ...any) (*sql.Rows, error)
 90}
 91
 92func secretRows(q queryer, c secretColumn) ([]secretRow, error) {
 93	rows, err := q.Query(fmt.Sprintf("SELECT rowid, %s, %s FROM %s WHERE %s != ''", c.column, c.key, c.table, c.column))
 94	if err != nil {
 95		return nil, err
 96	}
 97	defer rows.Close()
 98	var out []secretRow
 99	for rows.Next() {
100		var r secretRow
101		var n int64
102		var k string
103		if err := rows.Scan(&r.rowid, &r.value, &n, &k); err != nil {
104			return nil, err
105		}
106		r.aad = c.aad(n, k)
107		out = append(out, r)
108	}
109	return out, rows.Err()
110}
111
112// ResealSecrets fills push_devices.token_hash where it is missing, then
113// seals every clear value in the secret columns and reseals every value
114// not under the key file's current key. It runs in one write
115// transaction: every store write of a secret seals inside its own
116// transaction, so a write either lands before this one and is resealed,
117// or after it and is sealed under the key this one saw. It returns how
118// many values it rewrote.
119func (s *Store) ResealSecrets() (int, error) {
120	if s.secrets == nil {
121		return 0, errors.New("no secret key loaded")
122	}
123	tx, err := s.DB.Begin()
124	if err != nil {
125		return 0, err
126	}
127	defer tx.Rollback()
128	cur, err := s.secrets.CurrentID()
129	if err != nil {
130		return 0, err
131	}
132
133	// A token without a hash was written before sealing, so it is clear.
134	rows, err := tx.Query("SELECT id, token FROM push_devices WHERE token_hash IS NULL")
135	if err != nil {
136		return 0, err
137	}
138	var missing []secretRow
139	for rows.Next() {
140		var r secretRow
141		if err := rows.Scan(&r.rowid, &r.value); err != nil {
142			rows.Close()
143			return 0, err
144		}
145		missing = append(missing, r)
146	}
147	rows.Close()
148	if err := rows.Err(); err != nil {
149		return 0, err
150	}
151	for _, r := range missing {
152		if seal.IsSealed(r.value) {
153			return 0, fmt.Errorf("push_devices row %d: sealed token without a token_hash", r.rowid)
154		}
155		if _, err := tx.Exec("UPDATE push_devices SET token_hash = ? WHERE id = ?", tokenHash(r.value), r.rowid); err != nil {
156			return 0, err
157		}
158	}
159
160	n := 0
161	for _, c := range secretColumns {
162		rows, err := secretRows(tx, c)
163		if err != nil {
164			return 0, err
165		}
166		for _, r := range rows {
167			if id, ok := seal.KeyID(r.value); ok && id == cur {
168				continue
169			}
170			plain, err := s.openValue(r.aad, r.value)
171			if err != nil {
172				return 0, fmt.Errorf("%s.%s row %d: %w", c.table, c.column, r.rowid, err)
173			}
174			sealed, err := s.secrets.Seal(r.aad, plain)
175			if err != nil {
176				return 0, err
177			}
178			if _, err := tx.Exec(fmt.Sprintf("UPDATE %s SET %s = ? WHERE rowid = ?", c.table, c.column), sealed, r.rowid); err != nil {
179				return 0, err
180			}
181			n++
182		}
183	}
184	return n, tx.Commit()
185}
186
187// SecretColumnUse is one secret column's values by the id of the key
188// that sealed them ("" for a value still in clear), and the values that
189// do not open under the loaded key file.
190type SecretColumnUse struct {
191	Column string // "<table>.<column>"
192	ByKey  map[string]int
193	Failed []SecretFailure
194}
195
196// SecretFailure is a stored value that does not open.
197type SecretFailure struct {
198	RowID int64
199	Err   error
200}
201
202// SecretReport opens every value in the secret columns and counts them
203// per column by key id. A value that does not open is listed rather than
204// ending the scan.
205func (s *Store) SecretReport() ([]SecretColumnUse, error) {
206	var out []SecretColumnUse
207	for _, c := range secretColumns {
208		rows, err := secretRows(s.DB, c)
209		if err != nil {
210			return nil, err
211		}
212		u := SecretColumnUse{Column: c.table + "." + c.column, ByKey: map[string]int{}}
213		for _, r := range rows {
214			if _, err := s.openValue(r.aad, r.value); err != nil {
215				u.Failed = append(u.Failed, SecretFailure{RowID: r.rowid, Err: err})
216				continue
217			}
218			id, _ := seal.KeyID(r.value)
219			u.ByKey[id]++
220		}
221		out = append(out, u)
222	}
223	return out, nil
224}
225
226// SecretKeyUse counts the values in the secret columns by the id of the
227// key that sealed them ("" for a value still in clear), opening each
228// one, so a wrong or incomplete key file is an error naming the row.
229func (s *Store) SecretKeyUse() (map[string]int, error) {
230	report, err := s.SecretReport()
231	if err != nil {
232		return nil, err
233	}
234	use := map[string]int{}
235	for _, u := range report {
236		if len(u.Failed) > 0 {
237			f := u.Failed[0]
238			return nil, fmt.Errorf("%s row %d: %w", u.Column, f.RowID, f.Err)
239		}
240		for id, n := range u.ByKey {
241			use[id] += n
242		}
243	}
244	return use, nil
245}