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" "blitter.com/go/xs/xsnet" "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 { return false, errors.New("Unsupported platform") } 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/.config/xs/.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/%s", u.HomeDir, xsnet.XS_ID_AUTHTOKFILE)) if e != nil { log.Printf("INFO: Cannot read %s/%s\n", u.HomeDir, xsnet.XS_ID_AUTHTOKFILE) 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 "" }