ziggs/internal/data/users.go

338 lines
7.6 KiB
Go

package data
import (
"encoding/json"
"errors"
"sync"
"git.tcp.direct/kayos/common/entropy"
"git.tcp.direct/kayos/common/squish"
"github.com/davecgh/go-spew/spew"
"github.com/gliderlabs/ssh"
"github.com/rs/zerolog/log"
)
type StringMapper interface {
Map() map[string]string
}
type AuthMethod interface {
json.Marshaler
StringMapper
// Authenticate authenticates the user.
Authenticate() error
// Name returns the name of the authentication method.
Name() string
}
func AuthMethodFromMap(m map[string]string) AuthMethod {
switch m["type"] {
case "password":
return &UserPass{
Username: m["pass_username"],
Password: m["password"],
}
case "publickey":
pkParsed, err := ssh.ParsePublicKey(squish.B64d(m["pubkey"]))
if err != nil {
return nil
}
return &PubKey{
Username: m["pub_username"],
Pub: pkParsed,
}
}
return nil
}
var ErrAccessDenied = errors.New("access denied")
type User struct {
Username string `json:"username"`
AuthMethods []map[string]string `json:"auth_methods"`
*sync.Mutex
}
type UserPass struct {
Username string `json:"pass_username"`
Password string `json:"password"`
}
func (up *UserPass) Name() string {
return "password"
}
func (up *UserPass) Map() map[string]string {
return map[string]string{
"type": "password",
"pass_username": up.Username,
"password": up.Password,
}
}
func (up *UserPass) MarshalJSON() ([]byte, error) {
return json.Marshal(up.Map())
}
func NewUserPass(hashIt bool, username, password string) *UserPass {
var input = password
var err error
if hashIt {
input, err = HashPassword(password)
if err != nil {
panic(err)
}
}
return &UserPass{
Username: username,
Password: input,
}
}
func (up *UserPass) Authenticate() error {
if up.Username == "" {
return errors.New("username cannot be empty")
}
user, err := GetUser(up.Username)
if err != nil {
log.Warn().Err(err).Str("username", up.Username).Msg("error getting user")
FakeCycle()
return ErrAccessDenied
}
for _, method := range user.AuthMethods {
switch method["type"] {
case "password":
if method["pass_username"] == up.Username && CheckPasswordHash(up.Password, method["password"]) {
return nil
}
default:
continue
}
}
return ErrAccessDenied
}
type PubKey struct {
Username string `json:"pub_username"`
Pub ssh.PublicKey `json:"pubkey"`
}
func NewPubKey(username string, pubkey ssh.PublicKey) *PubKey {
return &PubKey{
Username: username,
Pub: pubkey,
}
}
func (pk *PubKey) Name() string {
return "publickey"
}
func (pk *PubKey) Map() map[string]string {
return map[string]string{
"type": "publickey",
"pub_username": pk.Username,
"pubkey": squish.B64e(pk.Pub.Marshal()),
}
}
func (pk *PubKey) MarshalJSON() ([]byte, error) {
return json.Marshal(pk.Map())
}
func (pk *PubKey) Authenticate() error {
user, err := GetUser(pk.Username)
if err != nil {
log.Warn().Err(err).Str("username", pk.Username).Msg("error getting user")
FakeCycle()
return ErrAccessDenied
}
for _, method := range user.AuthMethods {
switch method["type"] {
case "publickey":
if method["pub_username"] != pk.Username {
log.Warn().Str("username", pk.Username).Msg("username mismatch")
continue
}
pkdat, ok := method["pubkey"]
if !ok {
log.Warn().Str("username", pk.Username).Msg("pubkey not found")
continue
}
pubkeyParsed, err := ssh.ParsePublicKey(squish.B64d(pkdat))
if err != nil {
log.Warn().Err(err).Str("username", pk.Username).Msg("error parsing public key")
spew.Dump(method)
continue
}
if ssh.KeysEqual(pubkeyParsed, pk.Pub) {
return nil
}
default:
continue
}
}
return ErrAccessDenied
}
func GetUser(username string) (*User, error) {
res, err := db.With("users").Get([]byte(username))
if err != nil {
return nil, err
}
var user User
if err = json.Unmarshal(res, &user); err != nil {
return nil, err
}
user.Mutex = &sync.Mutex{}
return &user, nil
}
func NewUser(username string, authMethods ...AuthMethod) (*User, error) {
if len(username) == 0 {
return nil, errors.New("username cannot be empty")
}
if len(authMethods) == 0 {
return nil, errors.New("at least one authentication method must be provided")
}
var methods []map[string]string
for _, method := range authMethods {
if method == nil {
return nil, errors.New("authentication method cannot be nil")
}
switch method.Name() {
case "password":
usableMethod := method.(*UserPass)
if len(usableMethod.Username) == 0 {
return nil, errors.New("username cannot be empty")
}
if len(usableMethod.Password) == 0 {
return nil, errors.New("password cannot be empty")
}
methods = append(methods, method.Map())
case "publickey":
usableMethod := method.(*PubKey)
if len(usableMethod.Username) == 0 {
return nil, errors.New("username cannot be empty")
}
if len(usableMethod.Pub.Marshal()) == 0 {
return nil, errors.New("public key cannot be empty")
}
methods = append(methods, method.Map())
}
}
if len(methods) == 0 {
return nil, errors.New("at least one authentication method must be provided")
}
user := &User{
Username: username,
AuthMethods: methods,
Mutex: &sync.Mutex{},
}
b, err := json.Marshal(user)
if err != nil {
return nil, err
}
return user, db.With("users").Put([]byte(username), b)
}
func (user *User) AddAuthMethod(method AuthMethod) (*User, error) {
user.Lock()
defer user.Unlock()
if method == nil {
return user, errors.New("authentication method cannot be nil")
}
user.AuthMethods = append(user.AuthMethods, method.Map())
b, err := json.Marshal(user)
if err != nil {
return user, err
}
return user, db.With("users").Put([]byte(user.Username), b)
}
func DelUser(username string) error {
return db.With("users").Delete([]byte(username))
}
func (user *User) DelPubKey(pubkey ssh.PublicKey) (*User, error) {
user.Lock()
defer user.Unlock()
var found = false
var methods []map[string]string
for _, method := range user.AuthMethods {
m := AuthMethodFromMap(method)
if m == nil {
continue
}
if m.Name() == "publickey" {
pubKey := m.(*PubKey)
if ssh.KeysEqual(pubKey.Pub, pubkey) {
found = true
continue
}
}
methods = append(methods, method)
}
if !found {
return user, errors.New("public key not found")
}
user.AuthMethods = methods
if b, err := json.Marshal(user); err == nil {
return user, db.With("users").Put([]byte(user.Username), b)
} else {
return user, err
}
}
func (user *User) ChangePassword(newPassword string) (*User, error) {
user.Lock()
defer user.Unlock()
var ponce = &sync.Once{}
var methods []map[string]string
for _, method := range user.AuthMethods {
m := AuthMethodFromMap(method)
if m == nil {
continue
}
if m.Name() == "password" {
ponce.Do(func() {
hashed, err := HashPassword(newPassword)
if err != nil {
panic(err)
}
m.(*UserPass).Password = hashed
})
}
methods = append(methods, m.Map())
}
user.AuthMethods = methods
b, err := json.Marshal(user)
if err != nil {
return user, err
}
return user, db.With("users").Put([]byte(user.Username), b)
}
func provisionFakeUser() *User {
user, err := NewUser("0", NewUserPass(true, "0", entropy.RandStrWithUpper(32)))
if err != nil {
log.Panic().Err(err).Msg("error creating fake user")
}
return user
}
// FakeCycle chooses the first known user and cycles through all their auth methods to avoid time based user enumeration.
func FakeCycle() {
user, err := GetUser("0")
if err != nil {
user = provisionFakeUser()
}
for n, method := range user.AuthMethods {
if n > 2 {
break
}
_ = AuthMethodFromMap(method).Authenticate()
}
}