types: make pre auth key use bcrypt (#2853)

This commit is contained in:
Kristoffer Dalby
2025-11-12 09:36:36 -06:00
committed by GitHub
parent e3ced80278
commit da9018a0eb
21 changed files with 1450 additions and 225 deletions

View File

@@ -7,6 +7,13 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
// NewAPIKeyPrefixLength is the length of the prefix for new API keys.
NewAPIKeyPrefixLength = 12
// LegacyAPIKeyPrefixLength is the length of the prefix for legacy API keys.
LegacyAPIKeyPrefixLength = 7
)
// APIKey describes the datamodel for API keys used to remotely authenticate with
// headscale.
type APIKey struct {
@@ -21,8 +28,16 @@ type APIKey struct {
func (key *APIKey) Proto() *v1.ApiKey {
protoKey := v1.ApiKey{
Id: key.ID,
Prefix: key.Prefix,
Id: key.ID,
}
// Show prefix format: distinguish between new (12-char) and legacy (7-char) keys
if len(key.Prefix) == NewAPIKeyPrefixLength {
// New format key (12-char prefix)
protoKey.Prefix = "hskey-api-" + key.Prefix + "-***"
} else {
// Legacy format key (7-char prefix) or fallback
protoKey.Prefix = key.Prefix + "***"
}
if key.Expiration != nil {

View File

@@ -14,8 +14,15 @@ func (e PAKError) Error() string { return string(e) }
// PreAuthKey describes a pre-authorization key usable in a particular user.
type PreAuthKey struct {
ID uint64 `gorm:"primary_key"`
Key string
ID uint64 `gorm:"primary_key"`
// Legacy plaintext key (for backwards compatibility)
Key string
// New bcrypt-based authentication
Prefix string
Hash []byte // bcrypt
UserID uint
User User `gorm:"constraint:OnDelete:SET NULL;"`
Reusable bool
@@ -32,17 +39,59 @@ type PreAuthKey struct {
Expiration *time.Time
}
// PreAuthKeyNew is returned once when the key is created.
type PreAuthKeyNew struct {
ID uint64 `gorm:"primary_key"`
Key string
Reusable bool
Ephemeral bool
Tags []string
Expiration *time.Time
CreatedAt *time.Time
User User
}
func (key *PreAuthKeyNew) Proto() *v1.PreAuthKey {
protoKey := v1.PreAuthKey{
Id: key.ID,
Key: key.Key,
User: key.User.Proto(),
Reusable: key.Reusable,
Ephemeral: key.Ephemeral,
AclTags: key.Tags,
}
if key.Expiration != nil {
protoKey.Expiration = timestamppb.New(*key.Expiration)
}
if key.CreatedAt != nil {
protoKey.CreatedAt = timestamppb.New(*key.CreatedAt)
}
return &protoKey
}
func (key *PreAuthKey) Proto() *v1.PreAuthKey {
protoKey := v1.PreAuthKey{
User: key.User.Proto(),
Id: key.ID,
Key: key.Key,
Ephemeral: key.Ephemeral,
Reusable: key.Reusable,
Used: key.Used,
AclTags: key.Tags,
}
// For new keys (with prefix/hash), show the prefix so users can identify the key
// For legacy keys (with plaintext key), show the full key for backwards compatibility
if key.Prefix != "" {
protoKey.Key = "hskey-auth-" + key.Prefix + "-***"
} else if key.Key != "" {
// Legacy key - show full key for backwards compatibility
// TODO: Consider hiding this in a future major version
protoKey.Key = key.Key
}
if key.Expiration != nil {
protoKey.Expiration = timestamppb.New(*key.Expiration)
}

View File

@@ -110,6 +110,7 @@ func (src *PreAuthKey) Clone() *PreAuthKey {
}
dst := new(PreAuthKey)
*dst = *src
dst.Hash = append(src.Hash[:0:0], src.Hash...)
dst.Tags = append(src.Tags[:0:0], src.Tags...)
if dst.CreatedAt != nil {
dst.CreatedAt = ptr.To(*src.CreatedAt)
@@ -124,6 +125,8 @@ func (src *PreAuthKey) Clone() *PreAuthKey {
var _PreAuthKeyCloneNeedsRegeneration = PreAuthKey(struct {
ID uint64
Key string
Prefix string
Hash []byte
UserID uint
User User
Reusable bool

View File

@@ -239,14 +239,16 @@ func (v *PreAuthKeyView) UnmarshalJSON(b []byte) error {
return nil
}
func (v PreAuthKeyView) ID() uint64 { return v.ж.ID }
func (v PreAuthKeyView) Key() string { return v.ж.Key }
func (v PreAuthKeyView) UserID() uint { return v.ж.UserID }
func (v PreAuthKeyView) User() User { return v.ж.User }
func (v PreAuthKeyView) Reusable() bool { return v.ж.Reusable }
func (v PreAuthKeyView) Ephemeral() bool { return v.ж.Ephemeral }
func (v PreAuthKeyView) Used() bool { return v.ж.Used }
func (v PreAuthKeyView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
func (v PreAuthKeyView) ID() uint64 { return v.ж.ID }
func (v PreAuthKeyView) Key() string { return v.ж.Key }
func (v PreAuthKeyView) Prefix() string { return v.ж.Prefix }
func (v PreAuthKeyView) Hash() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Hash) }
func (v PreAuthKeyView) UserID() uint { return v.ж.UserID }
func (v PreAuthKeyView) User() User { return v.ж.User }
func (v PreAuthKeyView) Reusable() bool { return v.ж.Reusable }
func (v PreAuthKeyView) Ephemeral() bool { return v.ж.Ephemeral }
func (v PreAuthKeyView) Used() bool { return v.ж.Used }
func (v PreAuthKeyView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
func (v PreAuthKeyView) CreatedAt() views.ValuePointer[time.Time] {
return views.ValuePointerOf(v.ж.CreatedAt)
}
@@ -259,6 +261,8 @@ func (v PreAuthKeyView) Expiration() views.ValuePointer[time.Time] {
var _PreAuthKeyViewNeedsRegeneration = PreAuthKey(struct {
ID uint64
Key string
Prefix string
Hash []byte
UserID uint
User User
Reusable bool