11package mfa
22
33import (
4+ "context"
45 "crypto/aes"
56 "crypto/cipher"
67 "crypto/rand"
@@ -25,8 +26,6 @@ const ApiKeyTablePK = "value"
2526const (
2627 paramNewKeyId = "newKeyId"
2728 paramNewKeySecret = "newKeySecret"
28- paramOldKeyId = "oldKeyId"
29- paramOldKeySecret = "oldKeySecret"
3029)
3130
3231const (
@@ -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
5263func (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.
352362func (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.
409428func (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.
453474func (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
521527func (k * ApiKey ) loadAndCheck (secret string ) error {
0 commit comments