@@ -20,6 +20,14 @@ import (
20
20
// ApiKeyTablePK is the primary key in the ApiKey DynamoDB table
21
21
const ApiKeyTablePK = "value"
22
22
23
+ // key rotation request parameters
24
+ const (
25
+ paramNewKeyId = "newKeyId"
26
+ paramNewKeySecret = "newKeySecret"
27
+ paramOldKeyId = "oldKeyId"
28
+ paramOldKeySecret = "oldKeySecret"
29
+ )
30
+
23
31
// ApiKey holds API key data from DynamoDB
24
32
type ApiKey struct {
25
33
Key string `dynamodbav:"value" json:"value"`
@@ -205,23 +213,59 @@ func (k *ApiKey) Activate() error {
205
213
return nil
206
214
}
207
215
216
+ // ReEncryptTOTPs loads each TOTP record that was encrypted using the old key, re-encrypts it using the new
217
+ // key, and writes the updated data back to the database.
218
+ func (k * ApiKey ) ReEncryptTOTPs (storage * Storage , oldKey ApiKey ) (complete , incomplete int , err error ) {
219
+ var records []TOTP
220
+ err = storage .ScanApiKey (envConfig .TotpTable , oldKey .Key , & records )
221
+ if err != nil {
222
+ err = fmt .Errorf ("failed to query %s table for key %s: %w" , envConfig .TotpTable , oldKey .Key , err )
223
+ return
224
+ }
225
+
226
+ incomplete = len (records )
227
+ for _ , r := range records {
228
+ err = k .ReEncryptLegacy (oldKey , & r .EncryptedTotpKey )
229
+ if err != nil {
230
+ err = fmt .Errorf ("failed to re-encrypt TOTP %v: %w" , r .UUID , err )
231
+ return
232
+ }
233
+
234
+ r .ApiKey = k .Key
235
+
236
+ err = storage .Store (envConfig .TotpTable , & r )
237
+ if err != nil {
238
+ err = fmt .Errorf ("failed to store TOTP %v: %w" , r .UUID , err )
239
+ return
240
+ }
241
+ complete ++
242
+ incomplete --
243
+ }
244
+ return
245
+ }
246
+
208
247
// ReEncryptWebAuthnUsers loads each WebAuthn record that was encrypted using the old key, re-encrypts it using the new
209
248
// key, and writes the updated data back to the database.
210
- func (k * ApiKey ) ReEncryptWebAuthnUsers (storage * Storage , oldKey ApiKey ) error {
249
+ func (k * ApiKey ) ReEncryptWebAuthnUsers (storage * Storage , oldKey ApiKey ) ( complete , incomplete int , err error ) {
211
250
var users []WebauthnUser
212
- err : = storage .ScanApiKey (envConfig .WebauthnTable , oldKey .Key , & users )
251
+ err = storage .ScanApiKey (envConfig .WebauthnTable , oldKey .Key , & users )
213
252
if err != nil {
214
- return fmt .Errorf ("failed to query %s table for key %s: %w" , envConfig .WebauthnTable , oldKey .Key , err )
253
+ err = fmt .Errorf ("failed to query %s table for key %s: %w" , envConfig .WebauthnTable , oldKey .Key , err )
254
+ return
215
255
}
216
256
257
+ incomplete = len (users )
217
258
for _ , user := range users {
218
259
user .ApiKey = oldKey
219
260
err = k .ReEncryptWebAuthnUser (storage , user )
220
261
if err != nil {
221
- return err
262
+ err = fmt .Errorf ("failed to re-encrypt Webauthn %v: %w" , user .ID , err )
263
+ return
222
264
}
265
+ complete ++
266
+ incomplete --
223
267
}
224
- return nil
268
+ return
225
269
}
226
270
227
271
// ReEncryptWebAuthnUser re-encrypts a WebAuthnUser using the new key, and writes the updated data back to the database.
@@ -283,15 +327,15 @@ func (k *ApiKey) ReEncryptLegacy(oldKey ApiKey, v *string) error {
283
327
284
328
plaintext , err := oldKey .DecryptLegacy (* v )
285
329
if err != nil {
286
- return err
330
+ return fmt . Errorf ( "failed to decrypt data: %w" , err )
287
331
}
288
332
289
333
newCiphertext , err := k .EncryptLegacy (plaintext )
290
334
if err != nil {
291
- return err
335
+ return fmt . Errorf ( "failed to encrypt data: %w" , err )
292
336
}
293
337
294
- * v = string ( newCiphertext )
338
+ * v = newCiphertext
295
339
return nil
296
340
}
297
341
@@ -305,7 +349,7 @@ func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
305
349
306
350
err := json .NewDecoder (r .Body ).Decode (& requestBody )
307
351
if err != nil {
308
- jsonResponse (w , fmt .Errorf ("invalid request: %s " , err ), http .StatusBadRequest )
352
+ jsonResponse (w , fmt .Errorf ("invalid request: %w " , err ), http .StatusBadRequest )
309
353
return
310
354
}
311
355
@@ -322,19 +366,19 @@ func (a *App) ActivateApiKey(w http.ResponseWriter, r *http.Request) {
322
366
newKey := ApiKey {Key : requestBody .ApiKeyValue , Store : a .db }
323
367
err = newKey .Load ()
324
368
if err != nil {
325
- jsonResponse (w , fmt .Errorf ("key not found: %s " , err ), http .StatusNotFound )
369
+ jsonResponse (w , fmt .Errorf ("key not found: %w " , err ), http .StatusNotFound )
326
370
return
327
371
}
328
372
329
373
err = newKey .Activate ()
330
374
if err != nil {
331
- jsonResponse (w , fmt .Errorf ("failed to activate key: %s " , err ), http .StatusBadRequest )
375
+ jsonResponse (w , fmt .Errorf ("failed to activate key: %w " , err ), http .StatusBadRequest )
332
376
return
333
377
}
334
378
335
379
err = newKey .Save ()
336
380
if err != nil {
337
- jsonResponse (w , fmt .Errorf ("failed to save key: %s " , err ), http .StatusInternalServerError )
381
+ jsonResponse (w , fmt .Errorf ("failed to save key: %w " , err ), http .StatusInternalServerError )
338
382
return
339
383
}
340
384
@@ -349,7 +393,7 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
349
393
350
394
err := json .NewDecoder (r .Body ).Decode (& requestBody )
351
395
if err != nil {
352
- jsonResponse (w , fmt .Errorf ("invalid request: %s " , err ), http .StatusBadRequest )
396
+ jsonResponse (w , fmt .Errorf ("invalid request: %w " , err ), http .StatusBadRequest )
353
397
return
354
398
}
355
399
@@ -360,20 +404,97 @@ func (a *App) CreateApiKey(w http.ResponseWriter, r *http.Request) {
360
404
361
405
key , err := NewApiKey (requestBody .Email )
362
406
if err != nil {
363
- jsonResponse (w , fmt .Errorf ("failed to create a random key: %s " , err ), http .StatusInternalServerError )
407
+ jsonResponse (w , fmt .Errorf ("failed to create a random key: %w " , err ), http .StatusInternalServerError )
364
408
return
365
409
}
366
410
367
411
key .Store = a .db
368
412
err = key .Save ()
369
413
if err != nil {
370
- jsonResponse (w , fmt .Errorf ("failed to save key: %s " , err ), http .StatusInternalServerError )
414
+ jsonResponse (w , fmt .Errorf ("failed to save key: %w " , err ), http .StatusInternalServerError )
371
415
return
372
416
}
373
417
374
418
jsonResponse (w , nil , http .StatusNoContent )
375
419
}
376
420
421
+ // RotateApiKey facilitates the rotation of API Keys. All data in webauthn and totp tables that is encrypted by the old
422
+ // key will be re-encrypted using the new key. If the process does not run to completion, this endpoint can be called
423
+ // any number of times to continue the process. A status of 200 does not indicate that all keys were encrypted using the
424
+ // new key. Check the response data to determine if the rotation process is complete.
425
+ func (a * App ) RotateApiKey (w http.ResponseWriter , r * http.Request ) {
426
+ requestBody , err := parseRotateKeyRequestBody (r .Body )
427
+ if err != nil {
428
+ jsonResponse (w , fmt .Errorf ("invalid request: %w" , err ), http .StatusBadRequest )
429
+ return
430
+ }
431
+
432
+ oldKey := ApiKey {Key : requestBody [paramOldKeyId ], Store : a .GetDB ()}
433
+ err = oldKey .loadAndCheck (requestBody [paramOldKeySecret ])
434
+ if err != nil {
435
+ jsonResponse (w , fmt .Errorf ("old key is not valid: %w" , err ), http .StatusNotFound )
436
+ return
437
+ }
438
+
439
+ newKey := ApiKey {Key : requestBody [paramNewKeyId ], Store : a .GetDB ()}
440
+ err = newKey .loadAndCheck (requestBody [paramNewKeySecret ])
441
+ if err != nil {
442
+ jsonResponse (w , fmt .Errorf ("new key is not valid: %w" , err ), http .StatusNotFound )
443
+ return
444
+ }
445
+
446
+ totpComplete , totpIncomplete , err := newKey .ReEncryptTOTPs (a .GetDB (), oldKey )
447
+ if err != nil {
448
+ jsonResponse (w , fmt .Errorf ("failed to re-encrypt TOTP data: %w" , err ), http .StatusInternalServerError )
449
+ return
450
+ }
451
+
452
+ webauthnComplete , webauthnIncomplete , err := newKey .ReEncryptWebAuthnUsers (a .GetDB (), oldKey )
453
+ if err != nil {
454
+ jsonResponse (w , fmt .Errorf ("failed to re-encrypt WebAuthn data: %w" , err ), http .StatusInternalServerError )
455
+ return
456
+ }
457
+
458
+ responseBody := map [string ]int {
459
+ "totpComplete" : totpComplete ,
460
+ "totpIncomplete" : totpIncomplete ,
461
+ "webauthnComplete" : webauthnComplete ,
462
+ "webauthnIncomplete" : webauthnIncomplete ,
463
+ }
464
+
465
+ jsonResponse (w , responseBody , http .StatusOK )
466
+ }
467
+
468
+ func parseRotateKeyRequestBody (body io.Reader ) (map [string ]string , error ) {
469
+ var requestBody map [string ]string
470
+ err := json .NewDecoder (body ).Decode (& requestBody )
471
+ if err != nil {
472
+ return nil , fmt .Errorf ("invalid request in RotateApiKey: %w" , err )
473
+ }
474
+
475
+ fields := []string {paramNewKeyId , paramNewKeySecret , paramOldKeyId , paramOldKeySecret }
476
+ for _ , field := range fields {
477
+ if _ , ok := requestBody [field ]; ! ok {
478
+ return nil , fmt .Errorf ("%s is required" , field )
479
+ }
480
+ }
481
+ return requestBody , nil
482
+ }
483
+
484
+ func (k * ApiKey ) loadAndCheck (secret string ) error {
485
+ err := k .Load ()
486
+ if err != nil {
487
+ return fmt .Errorf ("failed to load key: %w" , err )
488
+ }
489
+
490
+ err = k .IsCorrect (secret )
491
+ if err != nil {
492
+ return fmt .Errorf ("key is not valid: %w" , err )
493
+ }
494
+ k .Secret = secret
495
+ return nil
496
+ }
497
+
377
498
// NewApiKey creates a new key with a random value
378
499
func NewApiKey (email string ) (ApiKey , error ) {
379
500
random := make ([]byte , 20 )
@@ -405,3 +526,8 @@ func newCipherBlock(key string) (cipher.Block, error) {
405
526
}
406
527
return block , nil
407
528
}
529
+
530
+ // debugString is used by the debugger to show useful ApiKey information in watched variables
531
+ func (k * ApiKey ) debugString () string {
532
+ return fmt .Sprintf ("key: %s, secret: %s" , k .Key , k .Secret )
533
+ }
0 commit comments