// Copyright (C) 2022 Storj Labs, Inc. // See LICENSE for copying information. package restkeys import ( "context" "crypto/sha256" "database/sql" "errors" "time" "github.com/spacemonkeygo/monkit/v3" "github.com/zeebo/errs" "storj.io/common/uuid" "storj.io/storj/satellite/oidc" ) var mon = monkit.Package() var ( // Error describes internal rest keys error. Error = errs.Class("rest keys service") // ErrDuplicateKey is error type that occurs when a generated account // management api key already exists. ErrDuplicateKey = errs.Class("duplicate key") // ErrInvalidKey is an error type that occurs when a user submits a key // that does not match anything in the database. ErrInvalidKey = errs.Class("invalid key") ) // Config contains configuration parameters for rest keys. type Config struct { DefaultExpiration time.Duration `help:"expiration to use if user does not specify an rest key expiration" default:"720h"` } // Service handles operations regarding rest keys. type Service struct { db oidc.OAuthTokens config Config } // NewService creates a new rest keys service. func NewService(db oidc.OAuthTokens, config Config) *Service { return &Service{ db: db, config: config, } } // Create creates and inserts an rest key into the db. func (s *Service) Create(ctx context.Context, userID uuid.UUID, expiration time.Duration) (apiKey string, expiresAt time.Time, err error) { defer mon.Task()(&ctx)(&err) apiKey, hash, err := s.GenerateNewKey(ctx) if err != nil { return "", time.Time{}, Error.Wrap(err) } expiresAt, err = s.InsertIntoDB(ctx, oidc.OAuthToken{ UserID: userID, Kind: oidc.KindRESTTokenV0, Token: hash, }, time.Now(), expiration) if err != nil { return "", time.Time{}, Error.Wrap(err) } return apiKey, expiresAt, nil } // GenerateNewKey generates a new account management api key. func (s *Service) GenerateNewKey(ctx context.Context) (apiKey, hash string, err error) { defer mon.Task()(&ctx)(&err) id, err := uuid.New() if err != nil { return "", "", Error.Wrap(err) } apiKey = id.String() hash = hashKeyFromUUID(ctx, id) return apiKey, hash, nil } // This is used for hashing during key creation so we don't need to convert from a string back to a uuid. func hashKeyFromUUID(ctx context.Context, apiKeyUUID uuid.UUID) string { mon.Task()(&ctx)(nil) hashBytes := sha256.Sum256(apiKeyUUID.Bytes()) return string(hashBytes[:]) } // HashKey returns a hash of api key. This is used for hashing inside GetUserFromKey. func (s *Service) HashKey(ctx context.Context, apiKey string) (hash string, err error) { defer mon.Task()(&ctx)(&err) id, err := uuid.FromString(apiKey) if err != nil { return "", Error.Wrap(err) } hashBytes := sha256.Sum256(id.Bytes()) return string(hashBytes[:]), nil } // InsertIntoDB checks OAuthTokens DB for a token before inserting. This is because OAuthTokens DB allows // duplicate tokens, but we can't have duplicate api keys. func (s *Service) InsertIntoDB(ctx context.Context, oAuthToken oidc.OAuthToken, now time.Time, expiration time.Duration) (expiresAt time.Time, err error) { defer mon.Task()(&ctx)(&err) // The token column is the key to the OAuthTokens table, but the Create method does not return an error if a duplicate token insert is attempted. // We need to make sure a unique api key is created, so check that the value doesn't already exist. _, err = s.db.Get(ctx, oidc.KindRESTTokenV0, oAuthToken.Token) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return time.Time{}, Error.Wrap(err) } } else if err == nil { return time.Time{}, Error.Wrap(ErrDuplicateKey.New("failed to generate a unique account management api key")) } if expiration <= 0 { expiration = s.config.DefaultExpiration } expiresAt = now.Add(expiration) oAuthToken.CreatedAt = now oAuthToken.ExpiresAt = expiresAt err = s.db.Create(ctx, oAuthToken) if err != nil { return time.Time{}, Error.Wrap(err) } return expiresAt, nil } // GetUserAndExpirationFromKey gets the userID and expiration date attached to an account management api key. func (s *Service) GetUserAndExpirationFromKey(ctx context.Context, apiKey string) (userID uuid.UUID, exp time.Time, err error) { defer mon.Task()(&ctx)(&err) hash, err := s.HashKey(ctx, apiKey) if err != nil { return uuid.UUID{}, time.Now(), err } keyInfo, err := s.db.Get(ctx, oidc.KindRESTTokenV0, hash) if err != nil { if errors.Is(err, sql.ErrNoRows) { return uuid.UUID{}, time.Now(), Error.Wrap(ErrInvalidKey.New("invalid account management api key")) } return uuid.UUID{}, time.Now(), err } return keyInfo.UserID, keyInfo.ExpiresAt, err } // Revoke revokes an account management api key. func (s *Service) Revoke(ctx context.Context, apiKey string) (err error) { defer mon.Task()(&ctx)(&err) hash, err := s.HashKey(ctx, apiKey) if err != nil { return Error.Wrap(err) } _, err = s.db.Get(ctx, oidc.KindRESTTokenV0, hash) if err != nil { return Error.Wrap(err) } err = s.db.RevokeRESTTokenV0(ctx, hash) if err != nil { return Error.Wrap(err) } return nil }