mirror of https://gogs.blitter.com/RLabs/xs
				
				
				
			
		
			
				
	
	
		
			236 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			236 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
	
| 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 ""
 | |
| }
 |