Skip to content

Commit 915e22b

Browse files
authored
Merge pull request #161 from sil-org/develop
Release 3.2.0 - Final changes on the key rotation process
2 parents 8d63a5a + f3dea81 commit 915e22b

File tree

7 files changed

+241
-187
lines changed

7 files changed

+241
-187
lines changed

apikey.go

Lines changed: 77 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package mfa
22

33
import (
4+
"context"
45
"crypto/aes"
56
"crypto/cipher"
67
"crypto/rand"
@@ -25,8 +26,6 @@ const ApiKeyTablePK = "value"
2526
const (
2627
paramNewKeyId = "newKeyId"
2728
paramNewKeySecret = "newKeySecret"
28-
paramOldKeyId = "oldKeyId"
29-
paramOldKeySecret = "oldKeySecret"
3029
)
3130

3231
const (
@@ -48,14 +47,26 @@ type ApiKey struct {
4847
Store *Storage `dynamodbav:"-" json:"-"`
4948
}
5049

50+
// CompletionStats stores the number of records processed in a batch operation
51+
type CompletionStats struct {
52+
Complete int `json:"complete"`
53+
Incomplete int `json:"incomplete"`
54+
}
55+
56+
// BatchStats stores the number of TOTP and Webauth records processed in a batch operation
57+
type BatchStats struct {
58+
TOTP CompletionStats `json:"totp"`
59+
Webauthn CompletionStats `json:"webauthn"`
60+
}
61+
5162
// Load refreshes an ApiKey from the database record
5263
func (k *ApiKey) Load() error {
5364
return k.Store.Load(envConfig.ApiKeyTable, ApiKeyTablePK, k.Key, k)
5465
}
5566

5667
// Save an ApiKey to the database
57-
func (k *ApiKey) Save() error {
58-
return k.Store.Store(envConfig.ApiKeyTable, k)
68+
func (k *ApiKey) Save(ctx context.Context) error {
69+
return k.Store.StoreCtx(ctx, envConfig.ApiKeyTable, k)
5970
}
6071

6172
// Hash generates a bcrypt hash from the Secret field and stores it in HashedSecret
@@ -223,61 +234,60 @@ func (k *ApiKey) Activate() error {
223234

224235
// ReEncryptTOTPs loads each TOTP record that was encrypted using the old key, re-encrypts it using the new
225236
// key, and writes the updated data back to the database.
226-
func (k *ApiKey) ReEncryptTOTPs(storage *Storage, oldKey ApiKey) (complete, incomplete int, err error) {
237+
func (k *ApiKey) ReEncryptTOTPs(ctx context.Context, storage *Storage, oldKey ApiKey) (CompletionStats, error) {
227238
var records []TOTP
228-
err = storage.ScanApiKey(envConfig.TotpTable, oldKey.Key, &records)
239+
err := storage.ScanApiKey(envConfig.TotpTable, oldKey.Key, &records)
229240
if err != nil {
230241
err = fmt.Errorf("failed to query %s table for key %s: %w", envConfig.TotpTable, oldKey.Key, err)
231-
return
242+
return CompletionStats{}, err
232243
}
233244

234-
incomplete = len(records)
245+
stats := CompletionStats{Incomplete: len(records)}
235246
for _, r := range records {
236247
err = k.ReEncryptLegacy(oldKey, &r.EncryptedTotpKey)
237248
if err != nil {
238-
err = fmt.Errorf("failed to re-encrypt TOTP %v: %w", r.UUID, err)
239-
return
249+
return stats, fmt.Errorf("failed to re-encrypt TOTP %v: %w", r.UUID, err)
240250
}
241251

242252
r.ApiKey = k.Key
243253

244-
err = storage.Store(envConfig.TotpTable, &r)
254+
err = storage.StoreCtx(ctx, envConfig.TotpTable, &r)
245255
if err != nil {
246-
err = fmt.Errorf("failed to store TOTP %v: %w", r.UUID, err)
247-
return
256+
return stats, fmt.Errorf("failed to store TOTP %v: %w", r.UUID, err)
248257
}
249-
complete++
250-
incomplete--
258+
259+
stats.Complete++
260+
stats.Incomplete--
251261
}
252-
return
262+
return stats, nil
253263
}
254264

255265
// ReEncryptWebAuthnUsers loads each WebAuthn record that was encrypted using the old key, re-encrypts it using the new
256266
// key, and writes the updated data back to the database.
257-
func (k *ApiKey) ReEncryptWebAuthnUsers(storage *Storage, oldKey ApiKey) (complete, incomplete int, err error) {
267+
func (k *ApiKey) ReEncryptWebAuthnUsers(ctx context.Context, storage *Storage, oldKey ApiKey) (CompletionStats, error) {
258268
var users []WebauthnUser
259-
err = storage.ScanApiKey(envConfig.WebauthnTable, oldKey.Key, &users)
269+
err := storage.ScanApiKey(envConfig.WebauthnTable, oldKey.Key, &users)
260270
if err != nil {
261271
err = fmt.Errorf("failed to query %s table for key %s: %w", envConfig.WebauthnTable, oldKey.Key, err)
262-
return
272+
return CompletionStats{}, err
263273
}
264274

265-
incomplete = len(users)
275+
stats := CompletionStats{Incomplete: len(users)}
266276
for _, user := range users {
267277
user.ApiKey = oldKey
268-
err = k.ReEncryptWebAuthnUser(storage, user)
278+
err = k.ReEncryptWebAuthnUser(ctx, storage, user)
269279
if err != nil {
270-
err = fmt.Errorf("failed to re-encrypt Webauthn %v: %w", user.ID, err)
271-
return
280+
return stats, fmt.Errorf("reencryption failed %v: %w", user.ID, err)
272281
}
273-
complete++
274-
incomplete--
282+
283+
stats.Complete++
284+
stats.Incomplete--
275285
}
276-
return
286+
return stats, nil
277287
}
278288

279289
// ReEncryptWebAuthnUser re-encrypts a WebAuthnUser using the new key, and writes the updated data back to the database.
280-
func (k *ApiKey) ReEncryptWebAuthnUser(storage *Storage, user WebauthnUser) error {
290+
func (k *ApiKey) ReEncryptWebAuthnUser(ctx context.Context, storage *Storage, user WebauthnUser) error {
281291
oldKey := user.ApiKey
282292
err := k.ReEncrypt(oldKey, &user.EncryptedSessionData)
283293
if err != nil {
@@ -299,7 +309,7 @@ func (k *ApiKey) ReEncryptWebAuthnUser(storage *Storage, user WebauthnUser) erro
299309
user.ApiKey = *k
300310
user.ApiKeyValue = k.Key
301311

302-
err = storage.Store(envConfig.WebauthnTable, &user)
312+
err = storage.StoreCtx(ctx, envConfig.WebauthnTable, &user)
303313
if err != nil {
304314
return err
305315
}
@@ -350,6 +360,8 @@ func (k *ApiKey) ReEncryptLegacy(oldKey ApiKey, v *string) error {
350360
// ActivateApiKey is the handler for the POST /api-key/activate endpoint. It creates the key secret and updates the
351361
// database record.
352362
func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
363+
ctx := r.Context()
364+
353365
var requestBody struct {
354366
ApiKeyValue string `json:"apiKeyValue"`
355367
Email string `json:"email"`
@@ -395,18 +407,27 @@ func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
395407
return
396408
}
397409

398-
err = newKey.Save()
410+
err = newKey.Save(ctx)
399411
if err != nil {
400412
log.Printf("failed to save key: %s", err)
401413
jsonResponse(w, internalServerError, http.StatusInternalServerError)
402414
return
403415
}
404416

405-
jsonResponse(w, map[string]string{"apiSecret": newKey.Secret}, http.StatusOK)
417+
response := map[string]string{
418+
"email": newKey.Email,
419+
"apiKeyValue": newKey.Key,
420+
"apiSecret": newKey.Secret,
421+
"activatedAt": time.Unix(int64(newKey.ActivatedAt)/1000, 0).UTC().Format(time.RFC3339),
422+
"createdAt": time.Unix(int64(newKey.CreatedAt)/1000, 0).UTC().Format(time.RFC3339),
423+
}
424+
jsonResponse(w, response, http.StatusOK)
406425
}
407426

408427
// CreateApiKey is the handler for the POST /api-key endpoint. It creates a new API Key and saves it to the database.
409428
func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
429+
ctx := r.Context()
430+
410431
var requestBody struct {
411432
Email string `json:"email"`
412433
}
@@ -431,7 +452,7 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
431452
}
432453

433454
key.Store = a.db
434-
err = key.Save()
455+
err = key.Save(ctx)
435456
if err != nil {
436457
log.Printf("failed to save key: %s", err)
437458
jsonResponse(w, internalServerError, http.StatusInternalServerError)
@@ -451,71 +472,56 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
451472
// any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the
452473
// new key. Check the response data to determine if the rotation process is complete.
453474
func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
454-
requestBody, err := parseRotateKeyRequestBody(r.Body)
475+
ctx := r.Context()
476+
var requestBody map[string]string
477+
err := json.NewDecoder(r.Body).Decode(&requestBody)
455478
if err != nil {
456-
if strings.HasSuffix(err.Error(), "is required") {
457-
jsonResponse(w, err, http.StatusBadRequest)
458-
} else {
459-
log.Printf("invalid request in RotateApiKey: %s", err)
460-
jsonResponse(w, invalidRequest, http.StatusBadRequest)
461-
}
479+
log.Printf("invalid request in ActivateApiKey: %s", err)
480+
jsonResponse(w, invalidRequest, http.StatusBadRequest)
462481
return
463482
}
464483

465-
oldKey := ApiKey{Key: requestBody[paramOldKeyId], Store: a.GetDB()}
466-
err = oldKey.loadAndCheck(requestBody[paramOldKeySecret])
467-
if err != nil {
468-
log.Printf("old key is not valid: %s", err)
469-
jsonResponse(w, apiKeyNotFound, http.StatusNotFound)
484+
if requestBody[paramNewKeyId] == "" {
485+
jsonResponse(w, paramNewKeyId+" is required", http.StatusBadRequest)
470486
return
471487
}
472488

473-
newKey := ApiKey{Key: requestBody[paramNewKeyId], Store: a.GetDB()}
474-
err = newKey.loadAndCheck(requestBody[paramNewKeySecret])
475-
if err != nil {
476-
log.Printf("new key is not valid: %s", err)
477-
jsonResponse(w, apiKeyNotFound, http.StatusNotFound)
489+
if requestBody[paramNewKeySecret] == "" {
490+
jsonResponse(w, paramNewKeySecret+" is required", http.StatusBadRequest)
478491
return
479492
}
480493

481-
totpComplete, totpIncomplete, err := newKey.ReEncryptTOTPs(a.GetDB(), oldKey)
494+
oldKey, err := getAPIKey(r)
482495
if err != nil {
483-
log.Printf("failed to re-encrypt TOTP data: %s", err)
496+
log.Printf("Rotate API key error: %v", err)
484497
jsonResponse(w, internalServerError, http.StatusInternalServerError)
485498
return
486499
}
487500

488-
webauthnComplete, webauthnIncomplete, err := newKey.ReEncryptWebAuthnUsers(a.GetDB(), oldKey)
501+
newKey := ApiKey{Key: requestBody[paramNewKeyId], Store: a.GetDB()}
502+
err = newKey.loadAndCheck(requestBody[paramNewKeySecret])
489503
if err != nil {
490-
log.Printf("failed to re-encrypt WebAuthn data: %s", err)
491-
jsonResponse(w, internalServerError, http.StatusInternalServerError)
504+
log.Printf("new key is not valid: %s", err)
505+
jsonResponse(w, apiKeyNotFound, http.StatusNotFound)
492506
return
493507
}
494508

495-
responseBody := map[string]int{
496-
"totpComplete": totpComplete,
497-
"totpIncomplete": totpIncomplete,
498-
"webauthnComplete": webauthnComplete,
499-
"webauthnIncomplete": webauthnIncomplete,
509+
webauthnStats, err := newKey.ReEncryptWebAuthnUsers(ctx, a.GetDB(), oldKey)
510+
if err != nil {
511+
log.Printf("failed to re-encrypt one or more WebAuthn record: %s", err)
500512
}
501513

502-
jsonResponse(w, responseBody, http.StatusOK)
503-
}
504-
505-
func parseRotateKeyRequestBody(body io.Reader) (map[string]string, error) {
506-
var requestBody map[string]string
507-
err := json.NewDecoder(body).Decode(&requestBody)
514+
totpStats, err := newKey.ReEncryptTOTPs(ctx, a.GetDB(), oldKey)
508515
if err != nil {
509-
return nil, fmt.Errorf("invalid request in RotateApiKey: %w", err)
516+
log.Printf("failed to re-encrypt one or more TOTP record: %s", err)
510517
}
511518

512-
fields := []string{paramNewKeyId, paramNewKeySecret, paramOldKeyId, paramOldKeySecret}
513-
for _, field := range fields {
514-
if _, ok := requestBody[field]; !ok {
515-
return nil, fmt.Errorf("%s is required", field)
516-
}
519+
responseBody := BatchStats{
520+
TOTP: totpStats,
521+
Webauthn: webauthnStats,
517522
}
518-
return requestBody, nil
523+
524+
jsonResponse(w, responseBody, http.StatusOK)
519525
}
520526

521527
func (k *ApiKey) loadAndCheck(secret string) error {

0 commit comments

Comments
 (0)