11package mfa
22
33import (
4+ "context"
45 "crypto/aes"
56 "crypto/cipher"
67 "crypto/rand"
@@ -46,14 +47,26 @@ type ApiKey struct {
4647 Store * Storage `dynamodbav:"-" json:"-"`
4748}
4849
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+
4962// Load refreshes an ApiKey from the database record
5063func (k * ApiKey ) Load () error {
5164 return k .Store .Load (envConfig .ApiKeyTable , ApiKeyTablePK , k .Key , k )
5265}
5366
5467// Save an ApiKey to the database
55- func (k * ApiKey ) Save () error {
56- 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 )
5770}
5871
5972// Hash generates a bcrypt hash from the Secret field and stores it in HashedSecret
@@ -221,61 +234,60 @@ func (k *ApiKey) Activate() error {
221234
222235// ReEncryptTOTPs loads each TOTP record that was encrypted using the old key, re-encrypts it using the new
223236// key, and writes the updated data back to the database.
224- 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 ) {
225238 var records []TOTP
226- err = storage .ScanApiKey (envConfig .TotpTable , oldKey .Key , & records )
239+ err : = storage .ScanApiKey (envConfig .TotpTable , oldKey .Key , & records )
227240 if err != nil {
228241 err = fmt .Errorf ("failed to query %s table for key %s: %w" , envConfig .TotpTable , oldKey .Key , err )
229- return
242+ return CompletionStats {}, err
230243 }
231244
232- incomplete = len (records )
245+ stats := CompletionStats { Incomplete : len (records )}
233246 for _ , r := range records {
234247 err = k .ReEncryptLegacy (oldKey , & r .EncryptedTotpKey )
235248 if err != nil {
236- err = fmt .Errorf ("failed to re-encrypt TOTP %v: %w" , r .UUID , err )
237- return
249+ return stats , fmt .Errorf ("failed to re-encrypt TOTP %v: %w" , r .UUID , err )
238250 }
239251
240252 r .ApiKey = k .Key
241253
242- err = storage .Store ( envConfig .TotpTable , & r )
254+ err = storage .StoreCtx ( ctx , envConfig .TotpTable , & r )
243255 if err != nil {
244- err = fmt .Errorf ("failed to store TOTP %v: %w" , r .UUID , err )
245- return
256+ return stats , fmt .Errorf ("failed to store TOTP %v: %w" , r .UUID , err )
246257 }
247- complete ++
248- incomplete --
258+
259+ stats .Complete ++
260+ stats .Incomplete --
249261 }
250- return
262+ return stats , nil
251263}
252264
253265// ReEncryptWebAuthnUsers loads each WebAuthn record that was encrypted using the old key, re-encrypts it using the new
254266// key, and writes the updated data back to the database.
255- 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 ) {
256268 var users []WebauthnUser
257- err = storage .ScanApiKey (envConfig .WebauthnTable , oldKey .Key , & users )
269+ err : = storage .ScanApiKey (envConfig .WebauthnTable , oldKey .Key , & users )
258270 if err != nil {
259271 err = fmt .Errorf ("failed to query %s table for key %s: %w" , envConfig .WebauthnTable , oldKey .Key , err )
260- return
272+ return CompletionStats {}, err
261273 }
262274
263- incomplete = len (users )
275+ stats := CompletionStats { Incomplete : len (users )}
264276 for _ , user := range users {
265277 user .ApiKey = oldKey
266- err = k .ReEncryptWebAuthnUser (storage , user )
278+ err = k .ReEncryptWebAuthnUser (ctx , storage , user )
267279 if err != nil {
268- err = fmt .Errorf ("failed to re-encrypt Webauthn %v: %w" , user .ID , err )
269- return
280+ return stats , fmt .Errorf ("reencryption failed %v: %w" , user .ID , err )
270281 }
271- complete ++
272- incomplete --
282+
283+ stats .Complete ++
284+ stats .Incomplete --
273285 }
274- return
286+ return stats , nil
275287}
276288
277289// ReEncryptWebAuthnUser re-encrypts a WebAuthnUser using the new key, and writes the updated data back to the database.
278- func (k * ApiKey ) ReEncryptWebAuthnUser (storage * Storage , user WebauthnUser ) error {
290+ func (k * ApiKey ) ReEncryptWebAuthnUser (ctx context. Context , storage * Storage , user WebauthnUser ) error {
279291 oldKey := user .ApiKey
280292 err := k .ReEncrypt (oldKey , & user .EncryptedSessionData )
281293 if err != nil {
@@ -297,7 +309,7 @@ func (k *ApiKey) ReEncryptWebAuthnUser(storage *Storage, user WebauthnUser) erro
297309 user .ApiKey = * k
298310 user .ApiKeyValue = k .Key
299311
300- err = storage .Store ( envConfig .WebauthnTable , & user )
312+ err = storage .StoreCtx ( ctx , envConfig .WebauthnTable , & user )
301313 if err != nil {
302314 return err
303315 }
@@ -348,6 +360,8 @@ func (k *ApiKey) ReEncryptLegacy(oldKey ApiKey, v *string) error {
348360// ActivateApiKey is the handler for the POST /api-key/activate endpoint. It creates the key secret and updates the
349361// database record.
350362func (a * App ) ActivateApiKey (w http.ResponseWriter , r * http.Request ) {
363+ ctx := r .Context ()
364+
351365 var requestBody struct {
352366 ApiKeyValue string `json:"apiKeyValue"`
353367 Email string `json:"email"`
@@ -393,7 +407,7 @@ func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
393407 return
394408 }
395409
396- err = newKey .Save ()
410+ err = newKey .Save (ctx )
397411 if err != nil {
398412 log .Printf ("failed to save key: %s" , err )
399413 jsonResponse (w , internalServerError , http .StatusInternalServerError )
@@ -412,6 +426,8 @@ func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
412426
413427// CreateApiKey is the handler for the POST /api-key endpoint. It creates a new API Key and saves it to the database.
414428func (a * App ) CreateApiKey (w http.ResponseWriter , r * http.Request ) {
429+ ctx := r .Context ()
430+
415431 var requestBody struct {
416432 Email string `json:"email"`
417433 }
@@ -436,7 +452,7 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
436452 }
437453
438454 key .Store = a .db
439- err = key .Save ()
455+ err = key .Save (ctx )
440456 if err != nil {
441457 log .Printf ("failed to save key: %s" , err )
442458 jsonResponse (w , internalServerError , http .StatusInternalServerError )
@@ -456,6 +472,7 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
456472// any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the
457473// new key. Check the response data to determine if the rotation process is complete.
458474func (a * App ) RotateApiKey (w http.ResponseWriter , r * http.Request ) {
475+ ctx := r .Context ()
459476 var requestBody map [string ]string
460477 err := json .NewDecoder (r .Body ).Decode (& requestBody )
461478 if err != nil {
@@ -489,25 +506,19 @@ func (a *App) RotateApiKey(w http.ResponseWriter, r *http.Request) {
489506 return
490507 }
491508
492- totpComplete , totpIncomplete , err := newKey .ReEncryptTOTPs ( a .GetDB (), oldKey )
509+ webauthnStats , err := newKey .ReEncryptWebAuthnUsers ( ctx , a .GetDB (), oldKey )
493510 if err != nil {
494- log .Printf ("failed to re-encrypt TOTP data: %s" , err )
495- jsonResponse (w , internalServerError , http .StatusInternalServerError )
496- return
511+ log .Printf ("failed to re-encrypt one or more WebAuthn record: %s" , err )
497512 }
498513
499- webauthnComplete , webauthnIncomplete , err := newKey .ReEncryptWebAuthnUsers ( a .GetDB (), oldKey )
514+ totpStats , err := newKey .ReEncryptTOTPs ( ctx , a .GetDB (), oldKey )
500515 if err != nil {
501- log .Printf ("failed to re-encrypt WebAuthn data: %s" , err )
502- jsonResponse (w , internalServerError , http .StatusInternalServerError )
503- return
516+ log .Printf ("failed to re-encrypt one or more TOTP record: %s" , err )
504517 }
505518
506- responseBody := map [string ]int {
507- "totpComplete" : totpComplete ,
508- "totpIncomplete" : totpIncomplete ,
509- "webauthnComplete" : webauthnComplete ,
510- "webauthnIncomplete" : webauthnIncomplete ,
519+ responseBody := BatchStats {
520+ TOTP : totpStats ,
521+ Webauthn : webauthnStats ,
511522 }
512523
513524 jsonResponse (w , responseBody , http .StatusOK )
0 commit comments