123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- package xs
- // Package xs - a secure terminal client/server written from scratch in Go
- //
- // Copyright (c) 2017-2020 Russell Magee
- // Licensed under the terms of the MIT license (see LICENSE.mit in this
- // distribution)
- //
- // golang implementation by Russ Magee (rmagee_at_gmail.com)
- // Authentication routines for the HKExSh
- import (
- "bytes"
- "encoding/csv"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "os"
- "os/user"
- "runtime"
- "strings"
- "github.com/jameskeane/bcrypt"
- passlib "gopkg.in/hlandau/passlib.v1"
- )
- type AuthCtx struct {
- reader func(string) ([]byte, error) // eg. ioutil.ReadFile()
- userlookup func(string) (*user.User, error) // eg. os/user.Lookup()
- }
- func NewAuthCtx( /*reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)*/ ) (ret *AuthCtx) {
- ret = &AuthCtx{ioutil.ReadFile, user.Lookup}
- return
- }
- // --------- System passwd/shadow auth routine(s) --------------
- // VerifyPass verifies a password against system standard shadow file
- // Note auxilliary fields for expiry policy are *not* inspected.
- func VerifyPass(ctx *AuthCtx, user, password string) (bool, error) {
- if ctx.reader == nil {
- ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
- }
- passlib.UseDefaults(passlib.Defaults20180601)
- var pwFileName string
- if runtime.GOOS == "linux" {
- pwFileName = "/etc/shadow"
- } else if runtime.GOOS == "freebsd" {
- pwFileName = "/etc/master.passwd"
- } else {
- pwFileName = "unsupported"
- }
- pwFileData, e := ctx.reader(pwFileName)
- if e != nil {
- return false, e
- }
- pwLines := strings.Split(string(pwFileData), "\n")
- if len(pwLines) < 1 {
- return false, errors.New("Empty shadow file!")
- } else {
- var line string
- var hash string
- var idx int
- for idx = range pwLines {
- line = pwLines[idx]
- lFields := strings.Split(line, ":")
- if lFields[0] == user {
- hash = lFields[1]
- break
- }
- }
- if len(hash) == 0 {
- return false, errors.New("nil hash!")
- } else {
- pe := passlib.VerifyNoUpgrade(password, hash)
- if pe != nil {
- return false, pe
- }
- }
- }
- return true, nil
- }
- // --------- End System passwd/shadow auth routine(s) ----------
- // ------------- xs-local passwd auth routine(s) ---------------
- // AuthUserByPasswd checks user login information using a password.
- // This checks /etc/xs.passwd for auth info, and system /etc/passwd
- // to cross-check the user actually exists.
- // nolint: gocyclo
- func AuthUserByPasswd(ctx *AuthCtx, username string, auth string, fname string) (valid bool, allowedCmds string) {
- if ctx.reader == nil {
- ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
- }
- if ctx.userlookup == nil {
- ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
- }
- b, e := ctx.reader(fname) // nolint: gosec
- if e != nil {
- valid = false
- log.Printf("ERROR: Cannot read %s!\n", fname)
- }
- r := csv.NewReader(bytes.NewReader(b))
- r.Comma = ':'
- r.Comment = '#'
- r.FieldsPerRecord = 3 // username:salt:authCookie [TODO:disallowedCmdList (a,b,...)]
- for {
- record, err := r.Read()
- if err == io.EOF {
- // Use dummy entry if user not found
- // (prevent user enumeration attack via obvious timing diff;
- // ie., not attempting any auth at all)
- record = []string{"$nosuchuser$",
- "$2a$12$l0coBlRDNEJeQVl6GdEPbU",
- "$2a$12$l0coBlRDNEJeQVl6GdEPbUC/xmuOANvqgmrMVum6S4i.EXPgnTXy6"}
- username = "$nosuchuser$"
- err = nil
- }
- if err != nil {
- log.Fatal(err)
- }
- if username == record[0] {
- tmp, err := bcrypt.Hash(auth, record[1])
- if err != nil {
- break
- }
- if tmp == record[2] && username != "$nosuchuser$" {
- valid = true
- }
- break
- }
- }
- // Security scrub
- for i := range b {
- b[i] = 0
- }
- r = nil
- runtime.GC()
- _, userErr := ctx.userlookup(username)
- if userErr != nil {
- valid = false
- }
- return
- }
- // ------------- End xs-local passwd auth routine(s) -----------
- // AuthUserByToken checks user login information against an auth token.
- // Auth tokens are stored in each user's $HOME/.xs_id and are requested
- // via the -g option.
- // The function also check system /etc/passwd to cross-check the user
- // actually exists.
- func AuthUserByToken(ctx *AuthCtx, username string, connhostname string, auth string) (valid bool) {
- if ctx.reader == nil {
- ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
- }
- if ctx.userlookup == nil {
- ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
- }
- auth = strings.TrimSpace(auth)
- u, ue := ctx.userlookup(username)
- if ue != nil {
- return false
- }
- b, e := ctx.reader(fmt.Sprintf("%s/.xs_id", u.HomeDir))
- if e != nil {
- log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir)
- return false
- }
- r := csv.NewReader(bytes.NewReader(b))
- r.Comma = ':'
- r.Comment = '#'
- r.FieldsPerRecord = 3 // connhost:username:authtoken
- for {
- record, err := r.Read()
- if err == io.EOF {
- return false
- }
- if len(record) < 3 ||
- len(record[0]) < 1 ||
- len(record[1]) < 1 ||
- len(record[2]) < 1 {
- return false
- }
- record[0] = strings.TrimSpace(record[0])
- record[1] = strings.TrimSpace(record[1])
- record[2] = strings.TrimSpace(record[2])
- //fmt.Println("auth:", auth, "record:",
- // strings.Join([]string{record[0], record[1], record[2]}, ":"))
- if (connhostname == record[0]) &&
- username == record[1] &&
- (auth == strings.Join([]string{record[0], record[1], record[2]}, ":")) {
- valid = true
- break
- }
- }
- _, userErr := ctx.userlookup(username)
- if userErr != nil {
- valid = false
- }
- return
- }
- func GetTool(tool string) (ret string) {
- ret = "/bin/" + tool
- _, err := os.Stat(ret)
- if err == nil {
- return ret
- }
- ret = "/usr/bin/" + tool
- _, err = os.Stat(ret)
- if err == nil {
- return ret
- }
- ret = "/usr/local/bin/" + tool
- _, err = os.Stat(ret)
- if err == nil {
- return ret
- }
- return ""
- }
|