mirror of
https://github.com/juanfont/headscale.git
synced 2025-07-13 13:49:18 +02:00
db/pak: make authkeys have identifiable hash
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
parent
bdcd5496fc
commit
0152597c50
@ -1,16 +1,17 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
v2 "github.com/juanfont/headscale/hscontrol/policy/v2"
|
||||||
"github.com/juanfont/headscale/hscontrol/types"
|
"github.com/juanfont/headscale/hscontrol/types"
|
||||||
|
"github.com/juanfont/headscale/hscontrol/util"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/util/set"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -19,72 +20,105 @@ var (
|
|||||||
ErrSingleUseAuthKeyHasBeenUsed = errors.New("AuthKey has already been used")
|
ErrSingleUseAuthKeyHasBeenUsed = errors.New("AuthKey has already been used")
|
||||||
ErrUserMismatch = errors.New("user mismatch")
|
ErrUserMismatch = errors.New("user mismatch")
|
||||||
ErrPreAuthKeyACLTagInvalid = errors.New("AuthKey tag is invalid")
|
ErrPreAuthKeyACLTagInvalid = errors.New("AuthKey tag is invalid")
|
||||||
|
ErrPreAuthKeyFailedToParse = errors.New("failed to parse AuthKey")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const authKeyPrefix = "hskey-auth-"
|
||||||
|
const authKeyPrefixLength = 12
|
||||||
|
const authKeyLength = 64
|
||||||
|
|
||||||
func (hsdb *HSDatabase) CreatePreAuthKey(
|
func (hsdb *HSDatabase) CreatePreAuthKey(
|
||||||
uid types.UserID,
|
uid *types.UserID,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
tags []string,
|
||||||
) (*types.PreAuthKey, error) {
|
) (string, error) {
|
||||||
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
return Write(hsdb.DB, func(tx *gorm.DB) (string, error) {
|
||||||
return CreatePreAuthKey(tx, uid, reusable, ephemeral, expiration, aclTags)
|
return CreatePreAuthKey(tx, uid, reusable, ephemeral, expiration, tags)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
|
// CreatePreAuthKey creates a new PreAuthKey in a user, and returns it.
|
||||||
|
// A PreAuthKey can be tagged or owned by a user, but not both.
|
||||||
func CreatePreAuthKey(
|
func CreatePreAuthKey(
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
uid types.UserID,
|
uid *types.UserID,
|
||||||
reusable bool,
|
reusable bool,
|
||||||
ephemeral bool,
|
ephemeral bool,
|
||||||
expiration *time.Time,
|
expiration *time.Time,
|
||||||
aclTags []string,
|
tags []string,
|
||||||
) (*types.PreAuthKey, error) {
|
) (string, error) {
|
||||||
user, err := GetUserByID(tx, uid)
|
var err error
|
||||||
if err != nil {
|
var user *types.User
|
||||||
return nil, err
|
var userID *uint
|
||||||
|
|
||||||
|
if uid == nil && len(tags) == 0 {
|
||||||
|
return "", errors.New("preauthkey must be either tagged or owned by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove duplicates
|
if uid != nil && len(tags) > 0 {
|
||||||
aclTags = set.SetOf(aclTags).Slice()
|
return "", errors.New("preauthkey cannot be both tagged and owned by user")
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(kradalby): factor out and create a reusable tag validation,
|
if uid != nil {
|
||||||
// check if there is one in Tailscale's lib.
|
user, err = GetUserByID(tx, *uid)
|
||||||
for _, tag := range aclTags {
|
if err != nil {
|
||||||
if !strings.HasPrefix(tag, "tag:") {
|
return "", err
|
||||||
return nil, fmt.Errorf(
|
}
|
||||||
"%w: '%s' did not begin with 'tag:'",
|
|
||||||
ErrPreAuthKeyACLTagInvalid,
|
userID = &user.ID
|
||||||
tag,
|
}
|
||||||
)
|
|
||||||
|
if len(tags) > 0 {
|
||||||
|
slices.Sort(tags)
|
||||||
|
tags = slices.Compact(tags)
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
t := v2.Tag(tag)
|
||||||
|
if err := t.Validate(); err != nil {
|
||||||
|
return "", fmt.Errorf("invalid tag: %w", tag, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
// TODO(kradalby): unify the key generations spread all over the code.
|
|
||||||
kstr, err := generateKey()
|
prefix, err := util.GenerateRandomStringURLSafe(apiPrefixLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
toBeHashed, err := util.GenerateRandomStringURLSafe(apiKeyLength)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key to return to user, this will only be visible _once_
|
||||||
|
keyStr := authKeyPrefix + "-" + prefix + "-" + toBeHashed
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(toBeHashed), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
key := types.PreAuthKey{
|
key := types.PreAuthKey{
|
||||||
Key: kstr,
|
|
||||||
UserID: user.ID,
|
|
||||||
User: *user,
|
|
||||||
Reusable: reusable,
|
Reusable: reusable,
|
||||||
Ephemeral: ephemeral,
|
Ephemeral: ephemeral,
|
||||||
CreatedAt: &now,
|
CreatedAt: &now,
|
||||||
Expiration: expiration,
|
Expiration: expiration,
|
||||||
Tags: aclTags,
|
UserID: userID,
|
||||||
|
User: user,
|
||||||
|
Tags: tags,
|
||||||
|
Prefix: prefix,
|
||||||
|
Hash: hash,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Save(&key).Error; err != nil {
|
if err := tx.Save(&key).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to create key in the database: %w", err)
|
return "", fmt.Errorf("failed to create key in the database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &key, nil
|
return keyStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) ListPreAuthKeys(uid types.UserID) ([]types.PreAuthKey, error) {
|
func (hsdb *HSDatabase) ListPreAuthKeys(uid types.UserID) ([]types.PreAuthKey, error) {
|
||||||
@ -101,28 +135,68 @@ func ListPreAuthKeysByUser(tx *gorm.DB, uid types.UserID) ([]types.PreAuthKey, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
keys := []types.PreAuthKey{}
|
keys := []types.PreAuthKey{}
|
||||||
if err := tx.Preload("User").Where(&types.PreAuthKey{UserID: user.ID}).Find(&keys).Error; err != nil {
|
if err := tx.Preload("User").Where(&types.PreAuthKey{UserID: &user.ID}).Find(&keys).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys, nil
|
return keys, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hsdb *HSDatabase) GetPreAuthKey(key string) (*types.PreAuthKey, error) {
|
// GetPreAuthKey returns a PreAuthKey by its key string.
|
||||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.PreAuthKey, error) {
|
// It will return an error if the key is not found, or if it is expired, used or invalid.
|
||||||
return GetPreAuthKey(rx, key)
|
func (hsdb *HSDatabase) GetPreAuthKey(keyStr string) (*types.PreAuthKey, error) {
|
||||||
|
return Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKey, error) {
|
||||||
|
return GetPreAuthKey(tx, keyStr)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPreAuthKey returns a PreAuthKey for a given key. The caller is responsible
|
// GetPreAuthKey returns a PreAuthKey by its key string.
|
||||||
// for checking if the key is usable (expired or used).
|
// It will return an error if the key is not found, or if it is expired, used or invalid.
|
||||||
func GetPreAuthKey(tx *gorm.DB, key string) (*types.PreAuthKey, error) {
|
func GetPreAuthKey(tx *gorm.DB, keyStr string) (*types.PreAuthKey, error) {
|
||||||
pak := types.PreAuthKey{}
|
pak, err := findAuthKey(tx, keyStr)
|
||||||
if err := tx.Preload("User").First(&pak, "key = ?", key).Error; err != nil {
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
|
||||||
|
return nil, ErrPreAuthKeyExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if pak.Used {
|
||||||
|
return nil, ErrSingleUseAuthKeyHasBeenUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return pak, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAuthKey(tx *gorm.DB, keyStr string) (*types.PreAuthKey, error) {
|
||||||
|
var pak *types.PreAuthKey
|
||||||
|
_, prefixAndHash, found := strings.Cut(keyStr, authKeyPrefix)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if err := tx.Preload("User").First(pak, "key = ?", keyStr).Error; err != nil {
|
||||||
|
return nil, ErrPreAuthKeyNotFound
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
prefix, hash, found := strings.Cut(prefixAndHash, "-")
|
||||||
|
if !found {
|
||||||
|
return nil, ErrPreAuthKeyFailedToParse
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Preload("User").First(pak, "prefix = ?", prefix).Error; err != nil {
|
||||||
return nil, ErrPreAuthKeyNotFound
|
return nil, ErrPreAuthKeyNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pak, nil
|
if err := bcrypt.CompareHashAndPassword(pak.Hash, []byte(hash)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pak == nil {
|
||||||
|
return nil, ErrPreAuthKeyNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return pak, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
|
// DestroyPreAuthKey destroys a preauthkey. Returns error if the PreAuthKey
|
||||||
@ -161,13 +235,3 @@ func ExpirePreAuthKey(tx *gorm.DB, k *types.PreAuthKey) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateKey() (string, error) {
|
|
||||||
size := 24
|
|
||||||
bytes := make([]byte, size)
|
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.EncodeToString(bytes), nil
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user