123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- // Copyright (C) 2019 Nicola Murino
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published
- // by the Free Software Foundation, version 3.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- package dataprovider
- import (
- "encoding/json"
- "fmt"
- "strings"
- "time"
- "github.com/alexedwards/argon2id"
- "golang.org/x/crypto/bcrypt"
- "github.com/drakkan/sftpgo/v2/internal/logger"
- "github.com/drakkan/sftpgo/v2/internal/util"
- )
- // APIKeyScope defines the supported API key scopes
- type APIKeyScope int
- // Supported API key scopes
- const (
- // the API key will be used for an admin
- APIKeyScopeAdmin APIKeyScope = iota + 1
- // the API key will be used for a user
- APIKeyScopeUser
- )
- // APIKey defines a SFTPGo API key.
- // API keys can be used as authentication alternative to short lived tokens
- // for REST API
- type APIKey struct {
- // Database unique identifier
- ID int64 `json:"-"`
- // Unique key identifier, used for key lookups.
- // The generated key is in the format `KeyID.hash(Key)` so we can split
- // and lookup by KeyID and then verify if the key matches the recorded hash
- KeyID string `json:"id"`
- // User friendly key name
- Name string `json:"name"`
- // we store the hash of the key, this is just like a password
- Key string `json:"key,omitempty"`
- Scope APIKeyScope `json:"scope"`
- CreatedAt int64 `json:"created_at"`
- UpdatedAt int64 `json:"updated_at"`
- // 0 means never used
- LastUseAt int64 `json:"last_use_at,omitempty"`
- // 0 means never expire
- ExpiresAt int64 `json:"expires_at,omitempty"`
- Description string `json:"description,omitempty"`
- // Username associated with this API key.
- // If empty and the scope is APIKeyScopeUser the key is valid for any user
- User string `json:"user,omitempty"`
- // Admin username associated with this API key.
- // If empty and the scope is APIKeyScopeAdmin the key is valid for any admin
- Admin string `json:"admin,omitempty"`
- // these fields are for internal use
- userID int64
- adminID int64
- plainKey string
- }
- func (k *APIKey) getACopy() APIKey {
- return APIKey{
- ID: k.ID,
- KeyID: k.KeyID,
- Name: k.Name,
- Key: k.Key,
- Scope: k.Scope,
- CreatedAt: k.CreatedAt,
- UpdatedAt: k.UpdatedAt,
- LastUseAt: k.LastUseAt,
- ExpiresAt: k.ExpiresAt,
- Description: k.Description,
- User: k.User,
- Admin: k.Admin,
- userID: k.userID,
- adminID: k.adminID,
- }
- }
- // RenderAsJSON implements the renderer interface used within plugins
- func (k *APIKey) RenderAsJSON(reload bool) ([]byte, error) {
- if reload {
- apiKey, err := provider.apiKeyExists(k.KeyID)
- if err != nil {
- providerLog(logger.LevelError, "unable to reload api key before rendering as json: %v", err)
- return nil, err
- }
- apiKey.HideConfidentialData()
- return json.Marshal(apiKey)
- }
- k.HideConfidentialData()
- return json.Marshal(k)
- }
- // HideConfidentialData hides API key confidential data
- func (k *APIKey) HideConfidentialData() {
- k.Key = ""
- }
- func (k *APIKey) hashKey() error {
- if k.Key != "" && !util.IsStringPrefixInSlice(k.Key, internalHashPwdPrefixes) {
- if config.PasswordHashing.Algo == HashingAlgoBcrypt {
- hashed, err := bcrypt.GenerateFromPassword([]byte(k.Key), config.PasswordHashing.BcryptOptions.Cost)
- if err != nil {
- return err
- }
- k.Key = string(hashed)
- } else {
- hashed, err := argon2id.CreateHash(k.Key, argon2Params)
- if err != nil {
- return err
- }
- k.Key = hashed
- }
- }
- return nil
- }
- func (k *APIKey) generateKey() {
- if k.KeyID != "" || k.Key != "" {
- return
- }
- k.KeyID = util.GenerateUniqueID()
- k.Key = util.GenerateUniqueID()
- k.plainKey = k.Key
- }
- // DisplayKey returns the key to show to the user
- func (k *APIKey) DisplayKey() string {
- return fmt.Sprintf("%v.%v", k.KeyID, k.plainKey)
- }
- func (k *APIKey) validate() error {
- if k.Name == "" {
- return util.NewValidationError("name is mandatory")
- }
- if k.Scope != APIKeyScopeAdmin && k.Scope != APIKeyScopeUser {
- return util.NewValidationError(fmt.Sprintf("invalid scope: %v", k.Scope))
- }
- k.generateKey()
- if err := k.hashKey(); err != nil {
- return err
- }
- if k.User != "" && k.Admin != "" {
- return util.NewValidationError("an API key can be related to a user or an admin, not both")
- }
- if k.Scope == APIKeyScopeAdmin {
- k.User = ""
- }
- if k.Scope == APIKeyScopeUser {
- k.Admin = ""
- }
- if k.User != "" {
- _, err := provider.userExists(k.User, "")
- if err != nil {
- return util.NewValidationError(fmt.Sprintf("unable to check API key user %v: %v", k.User, err))
- }
- }
- if k.Admin != "" {
- _, err := provider.adminExists(k.Admin)
- if err != nil {
- return util.NewValidationError(fmt.Sprintf("unable to check API key admin %v: %v", k.Admin, err))
- }
- }
- return nil
- }
- // Authenticate tries to authenticate the provided plain key
- func (k *APIKey) Authenticate(plainKey string) error {
- if k.ExpiresAt > 0 && k.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now()) {
- return fmt.Errorf("API key %q is expired, expiration timestamp: %v current timestamp: %v", k.KeyID,
- k.ExpiresAt, util.GetTimeAsMsSinceEpoch(time.Now()))
- }
- if config.PasswordCaching {
- found, match := cachedAPIKeys.Check(k.KeyID, plainKey, k.Key)
- if found {
- if !match {
- return ErrInvalidCredentials
- }
- return nil
- }
- }
- if strings.HasPrefix(k.Key, bcryptPwdPrefix) {
- if err := bcrypt.CompareHashAndPassword([]byte(k.Key), []byte(plainKey)); err != nil {
- return ErrInvalidCredentials
- }
- } else if strings.HasPrefix(k.Key, argonPwdPrefix) {
- match, err := argon2id.ComparePasswordAndHash(plainKey, k.Key)
- if err != nil || !match {
- return ErrInvalidCredentials
- }
- }
- cachedAPIKeys.Add(k.KeyID, plainKey, k.Key)
- return nil
- }
|