Skip to content

Commit

Permalink
Add key management
Browse files Browse the repository at this point in the history
  • Loading branch information
DJAndries committed Nov 13, 2024
1 parent dab0f2f commit 1d33e91
Show file tree
Hide file tree
Showing 9 changed files with 885 additions and 2 deletions.
2 changes: 1 addition & 1 deletion controllers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (sc *SessionsController) DeleteSession(w http.ResponseWriter, r *http.Reque
return
}

w.WriteHeader(http.StatusNoContent)
render.NoContent(w, r)
}

func (sc *SessionsController) Router(authMiddleware func(http.Handler) http.Handler) chi.Router {
Expand Down
186 changes: 186 additions & 0 deletions controllers/user_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package controllers

import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"time"

"github.com/brave-experiments/accounts/datastore"
"github.com/brave-experiments/accounts/middleware"
"github.com/brave-experiments/accounts/util"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)

const keyNameURLParam = "name"

// UserKey represents the HTTP request format for a key
type UserKeyStoreRequest struct {
// Name identifies the type of key (wrapping_key or sync_enc_seed)
Name string `json:"name" validate:"required,oneof=wrapping_key sync_enc_seed"`
// EncryptedKey contains the encrypted key data as hex bytes
EncryptedKey string `json:"encryptedKey" validate:"required,min=16,max=128"`
}

// UserKey represents the HTTP response format for a key
type UserKey struct {
// Name identifies the type of key (wrapping_key or sync_enc_seed)
Name string `json:"name"`
// EncryptedKey contains the encrypted key data as hex bytes
EncryptedKey string `json:"encryptedKey"`
// UpdatedAt is the timestamp when the key was last updated
UpdatedAt time.Time `json:"updatedAt"`
}

// ToUserKey converts a UserKeyRequest to a UserKey
func (r *UserKeyStoreRequest) ToDBUserKey(accountID uuid.UUID) (*datastore.DBUserKey, error) {
encKey, err := hex.DecodeString(r.EncryptedKey)
if err != nil {
return nil, fmt.Errorf("invalid hex encoding: %w", err)
}

return &datastore.DBUserKey{
AccountID: accountID,
Name: r.Name,
EncryptedKey: encKey,
UpdatedAt: time.Now().UTC(),
}, nil
}

// FromDBUserKey converts a datastore.DBUserKey to a UserKey
func FromDBUserKey(dbKey *datastore.DBUserKey) UserKey {
return UserKey{
Name: dbKey.Name,
EncryptedKey: hex.EncodeToString(dbKey.EncryptedKey),
UpdatedAt: dbKey.UpdatedAt,
}
}

type UserKeysController struct {
ds *datastore.Datastore
validate *validator.Validate
}

func NewUserKeysController(ds *datastore.Datastore) *UserKeysController {
return &UserKeysController{
ds: ds,
validate: validator.New(validator.WithRequiredStructEnabled()),
}
}

func (uc *UserKeysController) Router(authMiddleware func(http.Handler) http.Handler) chi.Router {
r := chi.NewRouter()
r.Use(authMiddleware)

r.Get("/", uc.ListKeys)
r.Get("/{name}", uc.GetKey)
r.Post("/", uc.SaveKey)

return r
}

// @Summary List user keys
// @Description Get all keys for the authenticated user
// @Tags User keys
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer + auth token"
// @Param Brave-Key header string false "Brave services key (if one is configured)"
// @Success 200 {array} UserKey
// @Failure 401 {object} util.ErrorResponse
// @Failure 403 {object} util.ErrorResponse
// @Failure 500 {object} util.ErrorResponse
// @Router /v2/keys [get]
func (uc *UserKeysController) ListKeys(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(middleware.ContextSession).(*datastore.Session)

keys, err := uc.ds.GetUserKeys(session.AccountID)
if err != nil {
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err)
return
}

response := make([]UserKey, len(keys))
for i := range keys {
response[i] = FromDBUserKey(&keys[i])
}

render.JSON(w, r, response)
}

// @Summary Get user key
// @Description Get a specific key by name for the authenticated user
// @Tags User keys
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer + auth token"
// @Param Brave-Key header string false "Brave services key (if one is configured)"
// @Param name path string true "Key name"
// @Success 200 {object} UserKey
// @Failure 401 {object} util.ErrorResponse
// @Failure 403 {object} util.ErrorResponse
// @Failure 404 {object} util.ErrorResponse
// @Failure 500 {object} util.ErrorResponse
// @Router /v2/keys/{name} [get]
func (uc *UserKeysController) GetKey(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(middleware.ContextSession).(*datastore.Session)
name := chi.URLParam(r, keyNameURLParam)

key, err := uc.ds.GetUserKey(session.AccountID, name)
if err != nil {
if errors.Is(err, datastore.ErrKeyNotFound) {
util.RenderErrorResponse(w, r, http.StatusNotFound, err)
return
}
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err)
return
}

render.JSON(w, r, FromDBUserKey(key))
}

// @Summary Save user key
// @Description Save a new key or update existing key for the authenticated user
// @Tags User keys
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer + auth token"
// @Param Brave-Key header string false "Brave services key (if one is configured)"
// @Param key body UserKeyStoreRequest true "Key to save"
// @Success 204 "Key saved"
// @Failure 400 {object} util.ErrorResponse
// @Failure 401 {object} util.ErrorResponse
// @Failure 403 {object} util.ErrorResponse
// @Failure 500 {object} util.ErrorResponse
// @Router /v2/keys [post]
func (uc *UserKeysController) SaveKey(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value(middleware.ContextSession).(*datastore.Session)

var requestData UserKeyStoreRequest
if err := render.DecodeJSON(r.Body, &requestData); err != nil {
util.RenderErrorResponse(w, r, http.StatusBadRequest, err)
return
}

if err := uc.validate.Struct(requestData); err != nil {
util.RenderErrorResponse(w, r, http.StatusBadRequest, err)
return
}

dbKey, err := requestData.ToDBUserKey(session.AccountID)
if err != nil {
util.RenderErrorResponse(w, r, http.StatusBadRequest, err)
return
}

if err := uc.ds.StoreUserKey(dbKey); err != nil {
util.RenderErrorResponse(w, r, http.StatusInternalServerError, err)
return
}

render.NoContent(w, r)
}
61 changes: 61 additions & 0 deletions datastore/user_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package datastore

import (
"errors"
"fmt"
"time"

"github.com/google/uuid"
"gorm.io/gorm"
)

var ErrKeyNotFound = errors.New("key not found")

// DBUserKey represents a key in the database
type DBUserKey struct {
// AccountID is the UUID of the account that owns this key
AccountID uuid.UUID `json:"-" gorm:"primaryKey"`
// Name identifies the type of key (wrapping_key, sync_enc_seed, or sync_device_seed)
Name string `json:"name" gorm:"primaryKey"`
// EncryptedKey contains the encrypted key data as bytes
EncryptedKey []byte `json:"encryptedKey"`
// UpdatedAt is the timestamp when the key was last updated
UpdatedAt time.Time `json:"updatedAt" gorm:"autoUpdateTime:false"`
}

// TableName overrides the default table name for DBUserKey
func (DBUserKey) TableName() string {
return "user_keys"
}

// StoreUserKey saves a user key to the database
func (d *Datastore) StoreUserKey(key *DBUserKey) error {
result := d.db.Save(key)
if result.Error != nil {
return fmt.Errorf("failed to store user key: %w", result.Error)
}
return nil
}

// GetUserKey retrieves a user key from the database
func (d *Datastore) GetUserKey(accountID uuid.UUID, name string) (*DBUserKey, error) {
var key DBUserKey
result := d.db.Where("account_id = ? AND name = ?", accountID, name).First(&key)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
return nil, ErrKeyNotFound
}
return nil, fmt.Errorf("failed to retrieve user key: %w", result.Error)
}
return &key, nil
}

// GetUserKeys retrieves all keys for an account
func (d *Datastore) GetUserKeys(accountID uuid.UUID) ([]DBUserKey, error) {
var keys []DBUserKey
result := d.db.Where("account_id = ?", accountID).Find(&keys)
if result.Error != nil {
return nil, fmt.Errorf("failed to retrieve user keys: %w", result.Error)
}
return keys, nil
}
Loading

0 comments on commit 1d33e91

Please sign in to comment.