internal/store/secrets.go
245 lines · 7149 bytes
19 symbols in this file
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}