improve doc, more lint
This commit is contained in:
parent
c236f43e92
commit
1f4cefd972
514
auth/auth.go
514
auth/auth.go
|
@ -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
|
||||
}
|
||||
|
|
15
auth/bans.go
15
auth/bans.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
301
auth/newAuth.go
301
auth/newAuth.go
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -11,10 +11,8 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
|
||||
func init() {
|
||||
if home, err = os.UserHomeDir(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prefConfigLocation = "./"
|
||||
snek = viper.New()
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ var (
|
|||
var (
|
||||
noColorForce = false
|
||||
customconfig = false
|
||||
home string
|
||||
configLocations []string
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue