Skip to content

Commit 365ee21

Browse files
committed
feat: add keychain abstraction for fetching rotated keys
1 parent b4f54f3 commit 365ee21

File tree

8 files changed

+779
-13
lines changed

8 files changed

+779
-13
lines changed

coderd/cryptokeys/dbkeycache.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package cryptokeys
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"sync"
7+
"time"
8+
9+
"golang.org/x/xerrors"
10+
11+
"cdr.dev/slog"
12+
"github.com/coder/coder/v2/coderd/database"
13+
"github.com/coder/quartz"
14+
)
15+
16+
// DBKeyCache implements KeyCache for callers with access to the database.
17+
type DBKeyCache struct {
18+
Clock quartz.Clock
19+
db database.Store
20+
feature database.CryptoKeyFeature
21+
logger slog.Logger
22+
23+
// The following are initialized by NewDBKeyCache.
24+
cacheMu sync.RWMutex
25+
cache map[int32]database.CryptoKey
26+
latestKey database.CryptoKey
27+
}
28+
29+
// NewDBKeyCache creates a new DBKeyCache. It starts a background
30+
// process that periodically refreshes the cache. The context should
31+
// be canceled to stop the background process.
32+
func NewDBKeyCache(ctx context.Context, logger slog.Logger, db database.Store, feature database.CryptoKeyFeature, opts ...func(*DBKeyCache)) (*DBKeyCache, error) {
33+
d := &DBKeyCache{
34+
db: db,
35+
feature: feature,
36+
Clock: quartz.NewReal(),
37+
logger: logger,
38+
}
39+
for _, opt := range opts {
40+
opt(d)
41+
}
42+
43+
cache, latest, err := d.newCache(ctx)
44+
if err != nil {
45+
return nil, xerrors.Errorf("new cache: %w", err)
46+
}
47+
d.cache, d.latestKey = cache, latest
48+
49+
go d.refresh(ctx)
50+
return d, nil
51+
}
52+
53+
// Version returns the CryptoKey with the given sequence number, provided that
54+
// it is not deleted or has breached its deletion date.
55+
func (d *DBKeyCache) Version(ctx context.Context, sequence int32) (database.CryptoKey, error) {
56+
now := d.Clock.Now().UTC()
57+
d.cacheMu.RLock()
58+
key, ok := d.cache[sequence]
59+
d.cacheMu.RUnlock()
60+
if ok {
61+
if key.IsInvalid(now) {
62+
return database.CryptoKey{}, ErrKeyNotFound
63+
}
64+
return key, nil
65+
}
66+
67+
d.cacheMu.Lock()
68+
defer d.cacheMu.Unlock()
69+
70+
key, ok = d.cache[sequence]
71+
if ok {
72+
return key, nil
73+
}
74+
75+
key, err := d.db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
76+
Feature: d.feature,
77+
Sequence: sequence,
78+
})
79+
if xerrors.Is(err, sql.ErrNoRows) {
80+
return database.CryptoKey{}, ErrKeyNotFound
81+
}
82+
if err != nil {
83+
return database.CryptoKey{}, err
84+
}
85+
86+
if key.IsInvalid(now) {
87+
return database.CryptoKey{}, ErrKeyInvalid
88+
}
89+
90+
if key.IsActive(now) && key.Sequence > d.latestKey.Sequence {
91+
d.latestKey = key
92+
}
93+
94+
d.cache[sequence] = key
95+
96+
return key, nil
97+
}
98+
99+
func (d *DBKeyCache) Latest(ctx context.Context) (database.CryptoKey, error) {
100+
d.cacheMu.RLock()
101+
latest := d.latestKey
102+
d.cacheMu.RUnlock()
103+
104+
now := d.Clock.Now().UTC()
105+
if latest.IsActive(now) {
106+
return latest, nil
107+
}
108+
109+
d.cacheMu.Lock()
110+
defer d.cacheMu.Unlock()
111+
112+
if latest.IsActive(now) {
113+
return latest, nil
114+
}
115+
116+
cache, latest, err := d.newCache(ctx)
117+
if err != nil {
118+
return database.CryptoKey{}, xerrors.Errorf("new cache: %w", err)
119+
}
120+
121+
if len(cache) == 0 {
122+
return database.CryptoKey{}, ErrKeyNotFound
123+
}
124+
125+
if !latest.IsActive(now) {
126+
return database.CryptoKey{}, ErrKeyInvalid
127+
}
128+
129+
d.cache, d.latestKey = cache, latest
130+
131+
return d.latestKey, nil
132+
}
133+
134+
func (d *DBKeyCache) refresh(ctx context.Context) {
135+
d.Clock.TickerFunc(ctx, time.Minute*10, func() error {
136+
cache, latest, err := d.newCache(ctx)
137+
if err != nil {
138+
d.logger.Error(ctx, "failed to refresh cache", slog.Error(err))
139+
return nil
140+
}
141+
d.cacheMu.Lock()
142+
defer d.cacheMu.Unlock()
143+
144+
d.cache, d.latestKey = cache, latest
145+
return nil
146+
})
147+
}
148+
149+
func (d *DBKeyCache) newCache(ctx context.Context) (map[int32]database.CryptoKey, database.CryptoKey, error) {
150+
now := d.Clock.Now().UTC()
151+
keys, err := d.db.GetCryptoKeysByFeature(ctx, d.feature)
152+
if err != nil {
153+
return nil, database.CryptoKey{}, xerrors.Errorf("get crypto keys by feature: %w", err)
154+
}
155+
cache := toMap(keys)
156+
var latest database.CryptoKey
157+
// Keys are returned in order from highest sequence to lowest.
158+
for _, key := range keys {
159+
if !key.IsActive(now) {
160+
continue
161+
}
162+
latest = key
163+
break
164+
}
165+
166+
return cache, latest, nil
167+
}
168+
169+
func toMap(keys []database.CryptoKey) map[int32]database.CryptoKey {
170+
m := make(map[int32]database.CryptoKey)
171+
for _, key := range keys {
172+
m[key.Sequence] = key
173+
}
174+
return m
175+
}

0 commit comments

Comments
 (0)