improve doc, more lint

This commit is contained in:
kayos@tcp.direct 2021-09-28 15:26:09 -07:00
parent c236f43e92
commit 1f4cefd972
14 changed files with 309 additions and 642 deletions

View File

@ -1,281 +1,303 @@
package auth
import (
"crypto/sha256"
"crypto/subtle"
"encoding/csv"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"git.tcp.direct/tcp.direct/bitcask-mirror"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"git.tcp.direct/kayos/sh3lly/set"
"git.tcp.direct/kayos/sh3lly/config"
)
// ErrNotWhitelisted Is the error returned when a key is checked that is not whitelisted,
// when whitelisting is enabled.
var ErrNotWhitelisted = errors.New("not whitelisted")
// ErrBanned is the error returned when a client is banned.
var ErrBanned = errors.New("banned")
// ErrIncorrectPassphrase is the error returned when a provided passphrase is incorrect.
var ErrIncorrectPassphrase = errors.New("incorrect passphrase")
// newAuthKey returns string from an ssh.PublicKey used to index the key in our lookup.
func newAuthKey(key ssh.PublicKey) string {
if key == nil {
return ""
}
// FIXME: Is there a better way to index pubkeys without marshal'ing them into strings?
return Fingerprint(key)
// RegisteredUser represents a user from our user system.
type RegisteredUser struct {
// ID is a users identifying number.
ID uint32
// Username is the friendly nickname of the user.
Username string
// Hash is a users hashed password.
Hash []byte
// PublicKey is the byte slice form of the SSH public key bound to the user's account.
PublicKey []byte
// Privs represents the PrivLevel (permissions) of the user's account.
Privs PrivLevel
// Notes is a place for admins to store arbitrary information about an account.
Notes []string
// AuthLog stores the time of the last 10 logins that were successful. We will not store any further info.
AuthLog [10]time.Time
}
func newAuthItem(key ssh.PublicKey) set.Item {
return set.StringItem(newAuthKey(key))
// Authenticator is used for pluggable authentication backends
type Authenticator interface {
// GetUser returns user type from username string.
GetUser(user string) (*RegisteredUser, error)
// Register registers a new user to our user system.
Register(user, pass string) (*RegisteredUser, error)
// Ban bans a user's account from logging in.
Ban(user string) error
// Delete delete's a user from our user system.
Delete(user string) error
// PassphraseLogin checks the provided passphrase against the provided account's stored credentials.
PassphraseLogin(user, pass string) error
// KeyLogin checks the provided public key against the provided account's stored public key.
KeyLogin(user string, publickey ssh.PublicKey) error
// GetPrivs checks the provided account's privileges and returns a Privilege level.
GetPrivs(user *RegisteredUser) PrivLevel
// SetPrivLevel changes a user's PrivLevel.
SetPrivLevel(user *RegisteredUser, level PrivLevel) error
// UserExists checks our user system for a user and returns true if they exist, false if not.
UserExists(user string) bool
// KeyExists checks the entire user system for any duplicate public keys.
KeyExists(pubkey ssh.PublicKey) (*RegisteredUser, bool)
// CheckPublicKey checks a given a public key, returns nil if the connection should be allowed.
CheckPublicKey(user string, key ssh.PublicKey) error
keyboardInteractive(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error)
publicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
}
// newAuthAddr returns a string from a net.Addr used to index the address the key in our lookup.
func newAuthAddr(addr net.Addr) string {
if addr == nil {
return ""
// UserDB is our account database for passphrase authentication.
type UserDB struct {
DB *bitcask.Bitcask
mu *sync.RWMutex
}
// AllowAnonymous determines if we allow anonymous connection to our server.
func (users *UserDB) AllowAnonymous() bool {
if config.AllowAnon {
println("allowanon")
}
host, _, err := net.SplitHostPort(addr.String())
return config.AllowAnon
}
// AcceptPassphrase determines if we allow password based authentication.
func (users *UserDB) AcceptPassphrase() bool {
return true
}
// CheckPublicKey compares a public key to the target users stored/accepted public key.
func (users *UserDB) CheckPublicKey(user string, key ssh.PublicKey) error {
u, err := users.GetUser(user)
if err != nil {
log.Fatal().Caller().Err(err).Msg("wtf?")
return errors.New("user does not exist")
}
return host
}
// Auth stores lookups for bans, whitelists, and ops. It implements the sshd.Auth interface.
// If the contained passphrase is not empty, it complements a whitelist.
type Auth struct {
passphraseHash []byte
bannedAddr *set.Set
bannedClient *set.Set
banned *set.Set
whitelist *set.Set
ops *set.Set
}
// NewAuth creates a new empty Auth.
func NewAuth() *Auth {
return &Auth{
bannedAddr: set.New(),
bannedClient: set.New(),
banned: set.New(),
whitelist: set.New(),
ops: set.New(),
}
}
// SetPassphrase enables passphrase authentication with the given passphrase.
// If an empty passphrase is given, disable passphrase authentication.
func (a *Auth) SetPassphrase(passphrase string) {
if passphrase == "" {
a.passphraseHash = nil
} else {
hashArray := HashPassword(passphrase)
a.passphraseHash = []byte(hashArray)
}
}
// AllowAnonymous determines if anonymous users are permitted.
func (a *Auth) AllowAnonymous() bool {
return a.whitelist.Len() == 0 && a.passphraseHash == nil
}
// AcceptPassphrase determines if passphrase authentication is accepted.
func (a *Auth) AcceptPassphrase() bool {
return a.passphraseHash != nil
}
// CheckBans checks IP, key and client bans.
func (a *Auth) CheckBans(addr net.Addr, key ssh.PublicKey, clientVersion string) error {
authkey := newAuthKey(key)
var banned bool
if authkey != "" {
banned = a.banned.In(authkey)
}
if !banned {
banned = a.bannedAddr.In(newAuthAddr(addr))
}
if !banned {
banned = a.bannedClient.In(clientVersion)
}
// Ops can bypass bans, just in case we ban ourselves.
if banned && !a.IsOp(key) {
return ErrBanned
}
return nil
}
// CheckPublicKey determines if a pubkey fingerprint is permitted.
func (a *Auth) CheckPublicKey(key ssh.PublicKey) error {
authkey := newAuthKey(key)
whitelisted := a.whitelist.In(authkey)
if a.AllowAnonymous() || whitelisted {
if string(key.Marshal()) == string(u.PublicKey) {
return nil
} else {
return ErrNotWhitelisted
}
return errors.New("invalid key")
}
// CheckPassphrase determines if a passphrase is permitted.
func (a *Auth) CheckPassphrase(passphrase string) error {
if !a.AcceptPassphrase() {
return errors.New("passphrases not accepted") // this should never happen
}
passedPassphraseHash := sha256.Sum256([]byte(passphrase))
if subtle.ConstantTimeCompare(passedPassphraseHash[:], a.passphraseHash) == 0 {
return ErrIncorrectPassphrase
}
return nil
}
// Op sets a public key as a known operator.
func (a *Auth) Op(key ssh.PublicKey, d time.Duration) {
if key == nil {
// NewUserDB returns a new UserDB with a bitcask database that stores it's data in path.
func NewUserDB(path string) (db *UserDB, err error) {
var b *bitcask.Bitcask
if b, err = bitcask.Open(path); err != nil {
return
}
authItem := newAuthItem(key)
if d != 0 {
a.ops.Set(set.Expire(authItem, d))
} else {
a.ops.Set(authItem)
}
log.Debug().Msgf("Added to ops: %q (for %s)", authItem.Key(), d)
}
// IsOp checks if a public key is an op.
func (a *Auth) IsOp(key ssh.PublicKey) bool {
if key == nil {
return false
}
authkey := newAuthKey(key)
return a.ops.In(authkey)
}
// Whitelist will set a public key as a whitelisted user.
func (a *Auth) Whitelist(key ssh.PublicKey, d time.Duration) {
if key == nil {
return
}
authItem := newAuthItem(key)
if d != 0 {
a.whitelist.Set(set.Expire(authItem, d))
} else {
a.whitelist.Set(authItem)
}
log.Debug().Msgf("Added to whitelist: %q (for %s)", authItem.Key(), d)
}
// Ban will set a public key as banned.
func (a *Auth) Ban(key ssh.PublicKey, d time.Duration) {
if key == nil {
return
}
a.BanFingerprint(newAuthKey(key), d)
}
// BanFingerprint will set a public key fingerprint as banned.
func (a *Auth) BanFingerprint(authkey string, d time.Duration) {
// FIXME: This is a case insensitive key, which isn't great...
authItem := set.StringItem(authkey)
if d != 0 {
a.banned.Set(set.Expire(authItem, d))
} else {
a.banned.Set(authItem)
}
log.Debug().Msgf("Added to banned: %q (for %s)", authItem.Key(), d)
}
// BanClient will set client version as banned. Useful for misbehaving bots.
func (a *Auth) BanClient(client string, d time.Duration) {
item := set.StringItem(client)
if d != 0 {
a.bannedClient.Set(set.Expire(item, d))
} else {
a.bannedClient.Set(item)
}
log.Debug().Msgf("BanList: %q (for %s)", item.Key(), d)
}
// BanList returns the list of banned keys.
func (a *Auth) Banned() (ip []string, fingerprint []string, client []string) {
a.banned.Each(func(key string, _ set.Item) error {
fingerprint = append(fingerprint, key)
return nil
})
a.bannedAddr.Each(func(key string, _ set.Item) error {
ip = append(ip, key)
return nil
})
a.bannedClient.Each(func(key string, _ set.Item) error {
client = append(client, key)
return nil
})
db = &UserDB{DB: b, mu: &sync.RWMutex{}}
return
}
// BanAddr will set an IP address as banned.
func (a *Auth) BanAddr(addr net.Addr, d time.Duration) {
authItem := set.StringItem(newAuthAddr(addr))
if d != 0 {
a.bannedAddr.Set(set.Expire(authItem, d))
} else {
a.bannedAddr.Set(authItem)
}
log.Debug().Msgf("Added to bannedAddr: %q (for %s)", authItem.Key(), d)
func uint32ToBytes(ui uint32) []byte {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, ui)
return buf
}
// BanQuery takes space-separated key="value" pairs to ban, including ip, fingerprint, client.
// Fields without an = will be treated as a duration, applied to the next field.
// For example: 5s client=foo 10min ip=1.1.1.1
// Will ban client foo for 5 seconds, and ip 1.1.1.1 for 10min.
func (a *Auth) BanQuery(q string) error {
r := csv.NewReader(strings.NewReader(q))
r.Comma = ' '
fields, err := r.Read()
func (users *UserDB) getNewID() uint32 {
users.mu.Lock()
defer users.mu.Unlock()
newid := uint32(10 + users.DB.Len())
for users.DB.Has(uint32ToBytes(newid)) {
// we choose the 10 offset because we store bans in the earlier ID spaces
newid = uint32(10 + users.DB.Len())
}
return newid
}
// Register registers a new user into our database.
func (users *UserDB) Register(user, pass string) (*RegisteredUser, error) {
var err error
var ubytes []byte
if users.UserExists(user) {
return nil, errors.New("username already exists: " + user)
}
if len(pass) < 5 {
return nil, errors.New("password must be at least 5 characters")
}
u := &RegisteredUser{ID: users.getNewID(), Username: user, Hash: HashPassword(pass), Privs: Chatter}
if ubytes, err = json.Marshal(u); err != nil {
return nil, err
}
err = users.DB.Put(uint32ToBytes(u.ID), ubytes)
return u, err
}
// AssignPublicKeyToUser attaches an SSH public key to the target registered user.
func (users *UserDB) AssignPublicKeyToUser(user *RegisteredUser, key ssh.PublicKey) error {
users.mu.Lock()
defer users.mu.Unlock()
user.PublicKey = key.Marshal()
return users.Sync(user)
}
// Delete removes a user from our database.
func (users *UserDB) Delete(user string) error {
users.mu.Lock()
defer users.mu.Unlock()
u, err := users.GetUser(user)
if err != nil {
return err
}
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, u.ID)
return users.DB.Delete(buf)
}
// PassphraseLogin attempts to validate the provided credentials against our UserDB
func (users *UserDB) PassphraseLogin(user, pass string) error {
var u *RegisteredUser
var err error
users.mu.RLock()
defer users.mu.RUnlock()
if u, err = users.GetUser(user); err != nil {
return err
}
if !CheckPasswordHash(pass, u.Hash) {
return errors.New("incorrect credentials")
}
return nil
}
// KeyLogin attempts to validate a user with their ssh public key
func (users *UserDB) KeyLogin(user string, pubkey ssh.PublicKey) error {
users.mu.RLock()
defer users.mu.RUnlock()
var (
u *RegisteredUser
ukey ssh.PublicKey
err error
)
if u, err = users.GetUser(user); err != nil {
return err
}
if ukey, err = ssh.ParsePublicKey(u.PublicKey); err != nil {
log.Warn().Err(err).Msg("failed to parse public key from database")
return errors.New("internal server error")
}
if ukey == pubkey {
return nil
}
return errors.New("invalid key for user")
}
// UserExists checks for the existence of the given username in our database.
func (users *UserDB) UserExists(user string) bool {
_, err := users.GetUser(user)
return err == nil
}
// Sync synchronizes the in-memory admin map into our bitcask database.
func (users *UserDB) Sync(user *RegisteredUser) error {
newuser, err := json.Marshal(user)
if err != nil {
return err
}
var d time.Duration
if last := fields[len(fields)-1]; !strings.Contains(last, "=") {
d, err = time.ParseDuration(last)
if err != nil {
return err
}
fields = fields[:len(fields)-1]
}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid query: %q", q)
}
key, value := parts[0], parts[1]
switch key {
case "client":
a.BanClient(value, d)
case "fingerprint":
// TODO: Add a validity check?
a.BanFingerprint(value, d)
case "ip":
ip := net.ParseIP(value)
if ip.String() == "" {
return fmt.Errorf("invalid ip value: %q", ip)
}
a.BanAddr(&net.TCPAddr{IP: ip}, d)
default:
return fmt.Errorf("unknown query field: %q", field)
}
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, user.ID)
return users.DB.Put(buf, newuser)
}
// GetUser iterates through all RegisteredUser instances in the database and returns a pointer to the one that matches the requested username.
func (users *UserDB) GetUser(user string) (usr *RegisteredUser, err error) {
var (
usrbytes []byte
)
if users.DB.Len() < 1 {
return &RegisteredUser{}, errors.New("no users in database")
}
return nil
ukeys := users.DB.Keys()
for {
key, ok := <-ukeys
if !ok {
break
}
if usrbytes, err = users.DB.Get(key); err != nil {
continue
}
if err := json.Unmarshal(usrbytes, &usr); err != nil {
log.Error().Err(err).Msg("failed to unmarshal")
continue
}
if usr.Username == user {
return usr, nil
}
}
return nil, errors.New("user does not exist: " + user)
}
// KeyExists iterates through all RegisteredUser instances in the database and returns the corresponding RegisteredUser and true if a public key is present.
func (users *UserDB) KeyExists(pubkey ssh.PublicKey) (*RegisteredUser, bool) {
var (
err error
usr *RegisteredUser
usrbytes []byte
k ssh.PublicKey
)
if users.DB.Len() < 1 {
return &RegisteredUser{}, false
}
ukeys := users.DB.Keys()
for {
key, ok := <-ukeys
if !ok {
break
}
if usrbytes, err = users.DB.Get(key); err != nil {
continue
}
if err := json.Unmarshal(usrbytes, &usr); err != nil {
log.Error().Err(err).Msg("failed to unmarshal")
continue
}
if k, err = ssh.ParsePublicKey(usr.PublicKey); err != nil {
log.Warn().Err(err).Msg("failed to parse public key from database")
continue
}
if k == pubkey {
return usr, true
}
}
return &RegisteredUser{}, false
}
// HashPassword hashes the given password string using bcrypt.
func HashPassword(password string) []byte {
var bytes []byte
var err error
if bytes, err = bcrypt.GenerateFromPassword([]byte(password), 14); err != nil {
panic(err)
}
return bytes
}
// CheckPasswordHash checks if a given password string, when hashed, matches the given hash.
func CheckPasswordHash(password string, hash []byte) bool {
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
return err == nil
}

View File

@ -11,17 +11,23 @@ import (
"golang.org/x/crypto/ssh"
)
// BanType is used to categorize what a particular ban targets.
type BanType uint32
const (
// Client is a ban targetting the users SSH client.
Client BanType = iota
// Name is a ban targetting nicknames.
Name
// Key is a ban targetting SSH public keys.
Key
// IP is a ban targetting IP addresses.
IP
)
var banCache *cache.Cache
// BanList represents our list of banned items.
type BanList struct {
Items []string
}
@ -30,6 +36,8 @@ func init() {
banCache = cache.New(2*time.Hour, 10*time.Minute)
}
// BanQuery is used to ingest strings that contain various types of bans seperated by an equals sign.
// e.g: /ban name=douchebag
func (users *UserDB) BanQuery(query string) (err error) {
request := strings.Split(query, "=")
switch strings.ToLower(request[0]) {
@ -47,6 +55,7 @@ func (users *UserDB) BanQuery(query string) (err error) {
return
}
// Banned returns the current banned items list.
func (users *UserDB) Banned() ([]string, []string, []string, []string) {
users.mu.RLock()
defer users.mu.RUnlock()
@ -124,6 +133,7 @@ func (users *UserDB) CheckBans(user string, addr net.Addr, key ssh.PublicKey, s
return nil
}
// BanOther creates a ban on various types of client attributes.
func (users *UserDB) BanOther(target string, bantype BanType) error {
bans := uint32ToBytes(uint32(bantype))
bad := &BanList{Items: []string{}}
@ -157,14 +167,13 @@ func (users *UserDB) BanOther(target string, bantype BanType) error {
return users.DB.Put(bans, newbads)
}
// CheckBanOther checks a given target of bantype against our banned items.
func (users *UserDB) CheckBanOther(target string, bantype BanType) bool {
bans := uint32ToBytes(uint32(bantype))
if bantype == IP {
ip, _, err := net.SplitHostPort(target)
if err != nil {
target = target
} else {
if err == nil {
target = ip
}
}

View File

@ -1,301 +0,0 @@
package auth
import (
"encoding/binary"
"encoding/json"
"errors"
"sync"
"time"
"git.tcp.direct/tcp.direct/bitcask-mirror"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"git.tcp.direct/kayos/sh3lly/config"
)
// RegisteredUser represents a user from our user system.
type RegisteredUser struct {
// ID is a users identifying number.
ID uint32
// Username is the friendly nickname of the user.
Username string
// Hash is a users hashed password.
Hash []byte
// PublicKey is the byte slice form of the SSH public key bound to the user's account.
PublicKey []byte
// Privs represents the PrivLevel (permissions) of the user's account.
Privs PrivLevel
// Notes is a place for admins to store arbitrary information about an account.
Notes []string
// AuthLog stores the time of the last 10 logins that were successful. We will not store any further info.
AuthLog [10]time.Time
}
// Authenticator is used for pluggable authentication backends
type Authenticator interface {
// GetUser returns user type from username string.
GetUser(user string) (*RegisteredUser, error)
// Register registers a new user to our user system.
Register(user, pass string) (*RegisteredUser, error)
// Ban bans a user's account from logging in.
Ban(user string) error
// Delete delete's a user from our user system.
Delete(user string) error
// PassphraseLogin checks the provided passphrase against the provided account's stored credentials.
PassphraseLogin(user, pass string) error
// KeyLogin checks the provided public key against the provided account's stored public key.
KeyLogin(user string, publickey ssh.PublicKey) error
// GetPrivs checks the provided account's privileges and returns a Privilege level.
GetPrivs(user *RegisteredUser) PrivLevel
// SetPrivLevel changes a user's PrivLevel.
SetPrivLevel(user *RegisteredUser, level PrivLevel) error
// UserExists checks our user system for a user and returns true if they exist, false if not.
UserExists(user string) bool
// KeyExists checks the entire user system for any duplicate public keys.
KeyExists(pubkey ssh.PublicKey) (*RegisteredUser, bool)
// CheckPublicKey checks a given a public key, returns nil if the connection should be allowed.
CheckPublicKey(user string, key ssh.PublicKey) error
keyboardInteractive(conn ssh.ConnMetadata, challenge ssh.KeyboardInteractiveChallenge) (*ssh.Permissions, error)
publicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
}
// UserDB is our account database for passphrase authentication.
type UserDB struct {
DB *bitcask.Bitcask
mu *sync.RWMutex
}
// AllowAnonymous determines if we allow anonymous connection to our server.
func (users *UserDB) AllowAnonymous() bool {
if config.AllowAnon {
println("allowanon")
}
return config.AllowAnon
}
// AcceptPassphrase determines if we allow password based authentication.
func (users *UserDB) AcceptPassphrase() bool {
return true
}
func (users *UserDB) CheckPublicKey(user string, key ssh.PublicKey) error {
u, err := users.GetUser(user)
if err != nil {
return errors.New("user does not exist")
}
if string(key.Marshal()) == string(u.PublicKey) {
return nil
}
return errors.New("invalid key")
}
// NewUserDB returns a new UserDB with a bitcask database that stores it's data in path.
func NewUserDB(path string) (db *UserDB, err error) {
var b *bitcask.Bitcask
if b, err = bitcask.Open(path); err != nil {
return
}
db = &UserDB{DB: b, mu: &sync.RWMutex{}}
return
}
func uint32ToBytes(ui uint32) []byte {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, ui)
return buf
}
func (users *UserDB) getNewID() uint32 {
users.mu.Lock()
defer users.mu.Unlock()
newid := uint32(10 + users.DB.Len())
for users.DB.Has(uint32ToBytes(newid)) {
// we choose the 10 offset because we store bans in the earlier ID spaces
newid = uint32(10 + users.DB.Len())
}
return newid
}
// Register registers a new user into our database.
func (users *UserDB) Register(user, pass string) (*RegisteredUser, error) {
var err error
var ubytes []byte
if users.UserExists(user) {
return nil, errors.New("username already exists: " + user)
}
if len(pass) < 5 {
return nil, errors.New("password must be at least 5 characters")
}
u := &RegisteredUser{ID: users.getNewID(), Username: user, Hash: HashPassword(pass), Privs: Chatter}
if ubytes, err = json.Marshal(u); err != nil {
return nil, err
}
err = users.DB.Put(uint32ToBytes(u.ID), ubytes)
return u, err
}
// AssignPublicKeyToUser attaches an SSH public key to the target registered user.
func (users *UserDB) AssignPublicKeyToUser(user *RegisteredUser, key ssh.PublicKey) error {
users.mu.Lock()
defer users.mu.Unlock()
user.PublicKey = key.Marshal()
return users.Sync(user)
}
// Delete removes a user from our database.
func (users *UserDB) Delete(user string) error {
users.mu.Lock()
defer users.mu.Unlock()
u, err := users.GetUser(user)
if err != nil {
return err
}
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, u.ID)
return users.DB.Delete(buf)
}
// PassphraseLogin attempts to validate the provided credentials against our UserDB
func (users *UserDB) PassphraseLogin(user, pass string) error {
var u *RegisteredUser
var err error
users.mu.RLock()
defer users.mu.RUnlock()
if u, err = users.GetUser(user); err != nil {
return err
}
if !CheckPasswordHash(pass, u.Hash) {
return errors.New("incorrect credentials")
}
return nil
}
// KeyLogin attempts to validate a user with their ssh public key
func (users *UserDB) KeyLogin(user string, pubkey ssh.PublicKey) error {
users.mu.RLock()
defer users.mu.RUnlock()
var (
u *RegisteredUser
ukey ssh.PublicKey
err error
)
if u, err = users.GetUser(user); err != nil {
return err
}
if ukey, err = ssh.ParsePublicKey(u.PublicKey); err != nil {
log.Warn().Err(err).Msg("failed to parse public key from database")
return errors.New("internal server error")
}
if ukey == pubkey {
return nil
}
return errors.New("invalid key for user")
}
// UserExists checks for the existence of the given username in our database.
func (users *UserDB) UserExists(user string) bool {
_, err := users.GetUser(user)
return err == nil
}
// Sync synchronizes the in-memory admin map into our bitcask database.
func (users *UserDB) Sync(user *RegisteredUser) error {
newuser, err := json.Marshal(user)
if err != nil {
return err
}
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, user.ID)
return users.DB.Put(buf, newuser)
}
// GetUser iterates through all RegisteredUser instances in the database and returns a pointer to the one that matches the requested username.
func (users *UserDB) GetUser(user string) (usr *RegisteredUser, err error) {
var (
usrbytes []byte
)
if users.DB.Len() < 1 {
return &RegisteredUser{}, errors.New("no users in database")
}
ukeys := users.DB.Keys()
for {
key, ok := <-ukeys
if !ok {
break
}
if usrbytes, err = users.DB.Get(key); err != nil {
continue
}
if err := json.Unmarshal(usrbytes, &usr); err != nil {
log.Error().Err(err).Msg("failed to unmarshal")
continue
}
if usr.Username == user {
return usr, nil
}
}
return nil, errors.New("user does not exist: " + user)
}
// KeyExists iterates through all RegisteredUser instances in the database and returns the corresponding RegisteredUser and true if a public key is present.
func (users *UserDB) KeyExists(pubkey ssh.PublicKey) (*RegisteredUser, bool) {
var (
err error
usr *RegisteredUser
usrbytes []byte
k ssh.PublicKey
)
if users.DB.Len() < 1 {
return &RegisteredUser{}, false
}
ukeys := users.DB.Keys()
for {
key, ok := <-ukeys
if !ok {
break
}
if usrbytes, err = users.DB.Get(key); err != nil {
continue
}
if err := json.Unmarshal(usrbytes, &usr); err != nil {
log.Error().Err(err).Msg("failed to unmarshal")
continue
}
if k, err = ssh.ParsePublicKey(usr.PublicKey); err != nil {
log.Warn().Err(err).Msg("failed to parse public key from database")
continue
}
if k == pubkey {
return usr, true
}
}
return &RegisteredUser{}, false
}
// HashPassword hashes the given password string using bcrypt.
func HashPassword(password string) []byte {
if bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14); err != nil {
panic(err)
} else {
return bytes
}
}
// CheckPasswordHash checks if a given password string, when hashed, matches the given hash.
func CheckPasswordHash(password string, hash []byte) bool {
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
return err == nil
}

View File

@ -31,7 +31,8 @@ func (users *UserDB) keyboardInteractive(conn ssh.ConnMetadata, challenge ssh.Ke
}
}
}
} else if !users.AllowAnonymous() {
}
if !users.AllowAnonymous() {
err = errors.New("public key authentication required")
}

View File

@ -49,7 +49,7 @@ func (h *help) add(item helpItem) {
}
func (h help) String() string {
r := []string{}
var r []string
format := fmt.Sprintf("%%-%ds - %%s", h.prefixWidth)
for _, item := range h.items {
r = append(r, fmt.Sprintf(format, item.Prefix, item.Text))

View File

@ -22,11 +22,12 @@ func ReadPrivateKey(path string) (ssh.Signer, error) {
pk, err := ssh.ParsePrivateKey(privateKey)
if err == nil {
} else if _, ok := err.(*ssh.PassphraseMissingError); ok {
}
if _, ok := err.(*ssh.PassphraseMissingError); ok {
passphrase := []byte(os.Getenv("IDENTITY_PASSPHRASE"))
if len(passphrase) == 0 {
fmt.Println("Enter passphrase to unlock identity private key:", path)
passphrase, err = term.ReadPassword(int(syscall.Stdin))
passphrase, err = term.ReadPassword(syscall.Stdin)
if err != nil {
return nil, fmt.Errorf("couldn't read passphrase: %v", err)
}

View File

@ -11,10 +11,8 @@ import (
"github.com/spf13/viper"
)
func init() {
if home, err = os.UserHomeDir(); err != nil {
panic(err)
}
prefConfigLocation = "./"
snek = viper.New()
}

View File

@ -51,7 +51,6 @@ var (
var (
noColorForce = false
customconfig = false
home string
configLocations []string
)

View File

@ -5,7 +5,7 @@ import (
"time"
)
// since returns a human-friendly relative time string
// Since returns a human-friendly relative time string
func Since(t time.Time) string {
d := time.Since(t)
switch {

View File

@ -29,10 +29,6 @@ func ListenSSH(laddr string, config *ssh.ServerConfig) (*SSHListener, error) {
func (l *SSHListener) handleConn(conn net.Conn) (*Terminal, error) {
log.Debug().Str("caller", conn.RemoteAddr().String()).Msg("new connection")
if l.RateLimit != nil {
// TODO: Configurable Limiter?
conn = ReadLimitConn(conn, l.RateLimit())
}
// If the connection doesn't write anything back for too long before we get
// a valid session, it should be dropped.

View File

@ -1,71 +0,0 @@
package sshd
import (
"io"
"net"
"time"
"github.com/shazow/rateio"
)
type limitedConn struct {
net.Conn
io.Reader // Our rate-limited io.Reader for net.Conn
}
func (r *limitedConn) Read(p []byte) (n int, err error) {
return r.Reader.Read(p)
}
// ReadLimitConn returns a net.Conn whose io.Reader interface is rate-limited by limiter.
func ReadLimitConn(conn net.Conn, limiter rateio.Limiter) net.Conn {
return &limitedConn{
Conn: conn,
Reader: rateio.NewReader(conn, limiter),
}
}
// Count each read as 1 unless it exceeds some number of bytes.
type inputLimiter struct {
// TODO: Could do all kinds of fancy things here, like be more forgiving of
// connections that have been around for a while.
Amount int
Frequency time.Duration
remaining int
readCap int
numRead int
timeRead time.Time
}
// NewInputLimiter returns a rateio.Limiter with sensible defaults for
// differentiating between humans typing and bots spamming.
func NewInputLimiter() rateio.Limiter {
grace := time.Second * 3
return &inputLimiter{
Amount: 2 << 14, // ~16kb, should be plenty for a high typing rate/copypasta/large key handshakes.
Frequency: time.Minute * 1,
readCap: 128, // Allow up to 128 bytes per read (anecdotally, 1 character = 52 bytes over ssh)
numRead: -1024 * 1024, // Start with a 1mb grace
timeRead: time.Now().Add(grace),
}
}
// Count applies 1 if n<readCap, else n
func (limit *inputLimiter) Count(n int) error {
now := time.Now()
if now.After(limit.timeRead) {
limit.numRead = 0
limit.timeRead = now.Add(limit.Frequency)
}
if n <= limit.readCap {
limit.numRead += 1
} else {
limit.numRead += n
}
if limit.numRead > limit.Amount {
return rateio.ErrRateExceeded
}
return nil
}

View File

@ -93,8 +93,9 @@ type Terminal struct {
term string
}
// Make new terminal from a session channel
// TODO: For v2, make a separate `Serve(ctx context.Context) error` method to activate the Terminal
// NewTerminal makes a new terminal from a session channel.
func NewTerminal(conn *ssh.ServerConn, ch ssh.NewChannel) (*Terminal, error) {
if ch.ChannelType() != "session" {
return nil, ErrNotSessionChannel
@ -233,7 +234,7 @@ func (t *Terminal) listen(requests <-chan *ssh.Request, ready chan<- struct{}) {
func (t *Terminal) Env() Env {
t.mu.Lock()
defer t.mu.Unlock()
return Env(t.env)
return t.env
}
// Term returns the terminal string value as set by the pty.

View File

@ -294,14 +294,16 @@ func (t *Terminal) moveCursorToPos(pos int) {
}
func (t *Terminal) move(up, down, left, right int) {
m := []rune{}
var m []rune
// 1 unit up can be expressed as ^[A
// 5 units up can be expressed as ^[[5A
if up == 1 {
m = append(m, keyEscape, '[', 'A')
} else if up > 1 {
}
if up > 1 {
m = append(m, keyEscape, '[')
m = append(m, []rune(strconv.Itoa(up))...)
m = append(m, 'A')
@ -309,7 +311,8 @@ func (t *Terminal) move(up, down, left, right int) {
if down == 1 {
m = append(m, keyEscape, '[', 'B')
} else if down > 1 {
}
if down > 1 {
m = append(m, keyEscape, '[')
m = append(m, []rune(strconv.Itoa(down))...)
m = append(m, 'B')
@ -317,7 +320,8 @@ func (t *Terminal) move(up, down, left, right int) {
if right == 1 {
m = append(m, keyEscape, '[', 'C')
} else if right > 1 {
}
if right > 1 {
m = append(m, keyEscape, '[')
m = append(m, []rune(strconv.Itoa(right))...)
m = append(m, 'C')
@ -325,7 +329,8 @@ func (t *Terminal) move(up, down, left, right int) {
if left == 1 {
m = append(m, keyEscape, '[', 'D')
} else if left > 1 {
}
if left > 1 {
m = append(m, keyEscape, '[')
m = append(m, []rune(strconv.Itoa(left))...)
m = append(m, 'D')
@ -796,7 +801,8 @@ func (t *Terminal) readLine() (line string, err error) {
}
continue
}
} else if key == keyPasteEnd {
}
if key == keyPasteEnd {
t.pasteActive = false
continue
}

View File

@ -27,6 +27,7 @@ type State struct {
}
// IsTerminal returns whether the given file descriptor is a terminal.
//goland:noinspection Annotator
func IsTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
@ -35,7 +36,9 @@ func IsTerminal(fd int) bool {
// MakeRaw put the terminal connected to the given file descriptor into raw
// mode and returns the previous state of the terminal so that it can be
// restored.
//goland:noinspection Annotator
func MakeRaw(fd int) (*State, error) {
//goland:noinspection Annotator
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
if err != nil {
return nil, err
@ -61,6 +64,7 @@ func MakeRaw(fd int) (*State, error) {
// GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal.
//goland:noinspection Annotator
func GetState(fd int) (*State, error) {
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
if err != nil {
@ -72,6 +76,7 @@ func GetState(fd int) (*State, error) {
// Restore restores the terminal connected to the given file descriptor to a
// previous state.
//goland:noinspection Annotator
func Restore(fd int, state *State) error {
return unix.IoctlSetTermios(fd, ioctlWriteTermios, &state.termios)
}
@ -95,6 +100,7 @@ func (r passwordReader) Read(buf []byte) (int, error) {
// ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n.
//goland:noinspection Annotator,Annotator,Annotator
func ReadPassword(fd int) ([]byte, error) {
termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
if err != nil {