2019-10-30 03:34:09 +00:00
|
|
|
package xs
|
2018-12-08 19:29:58 +00:00
|
|
|
|
2019-10-30 03:34:09 +00:00
|
|
|
// Package xs - a secure terminal client/server written from scratch in Go
|
2018-04-07 20:04:10 +00:00
|
|
|
//
|
2020-08-08 08:54:46 +00:00
|
|
|
// Copyright (c) 2017-2020 Russell Magee
|
2018-04-07 20:04:10 +00:00
|
|
|
// Licensed under the terms of the MIT license (see LICENSE.mit in this
|
|
|
|
// distribution)
|
|
|
|
//
|
|
|
|
// golang implementation by Russ Magee (rmagee_at_gmail.com)
|
2018-01-22 06:13:35 +00:00
|
|
|
|
2018-12-08 19:29:58 +00:00
|
|
|
// Authentication routines for the HKExSh
|
2018-01-22 06:13:35 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/csv"
|
2019-12-20 04:01:39 +00:00
|
|
|
"errors"
|
2018-09-14 06:51:49 +00:00
|
|
|
"fmt"
|
2018-01-22 06:13:35 +00:00
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
2020-04-26 01:03:29 +00:00
|
|
|
"os"
|
2018-09-14 06:51:49 +00:00
|
|
|
"os/user"
|
2018-01-22 06:13:35 +00:00
|
|
|
"runtime"
|
2018-09-14 18:58:10 +00:00
|
|
|
"strings"
|
2018-01-23 21:53:05 +00:00
|
|
|
|
|
|
|
"github.com/jameskeane/bcrypt"
|
2019-12-20 04:01:39 +00:00
|
|
|
passlib "gopkg.in/hlandau/passlib.v1"
|
2018-01-22 06:13:35 +00:00
|
|
|
)
|
|
|
|
|
2020-02-18 18:53:04 +00:00
|
|
|
type AuthCtx struct {
|
|
|
|
reader func(string) ([]byte, error) // eg. ioutil.ReadFile()
|
|
|
|
userlookup func(string) (*user.User, error) // eg. os/user.Lookup()
|
|
|
|
}
|
|
|
|
|
2020-02-18 21:41:44 +00:00
|
|
|
func NewAuthCtx( /*reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)*/ ) (ret *AuthCtx) {
|
|
|
|
ret = &AuthCtx{ioutil.ReadFile, user.Lookup}
|
|
|
|
return
|
2020-02-18 18:53:04 +00:00
|
|
|
}
|
|
|
|
|
2019-12-20 04:01:39 +00:00
|
|
|
// --------- System passwd/shadow auth routine(s) --------------
|
2020-08-08 08:54:46 +00:00
|
|
|
|
|
|
|
// VerifyPass verifies a password against system standard shadow file
|
2019-12-20 04:01:39 +00:00
|
|
|
// Note auxilliary fields for expiry policy are *not* inspected.
|
2020-02-18 18:53:04 +00:00
|
|
|
func VerifyPass(ctx *AuthCtx, user, password string) (bool, error) {
|
|
|
|
if ctx.reader == nil {
|
|
|
|
ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
|
2020-02-18 07:14:37 +00:00
|
|
|
}
|
2019-12-20 04:01:39 +00:00
|
|
|
passlib.UseDefaults(passlib.Defaults20180601)
|
2020-04-26 01:03:29 +00:00
|
|
|
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)
|
2019-12-20 04:01:39 +00:00
|
|
|
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
|
2018-10-04 05:31:35 +00:00
|
|
|
}
|
|
|
|
|
2019-12-20 04:01:39 +00:00
|
|
|
// --------- End System passwd/shadow auth routine(s) ----------
|
|
|
|
|
|
|
|
// ------------- xs-local passwd auth routine(s) ---------------
|
|
|
|
|
2018-11-25 18:24:10 +00:00
|
|
|
// AuthUserByPasswd checks user login information using a password.
|
2019-10-30 03:34:09 +00:00
|
|
|
// This checks /etc/xs.passwd for auth info, and system /etc/passwd
|
2018-11-25 18:24:10 +00:00
|
|
|
// to cross-check the user actually exists.
|
|
|
|
// nolint: gocyclo
|
2020-02-18 18:53:04 +00:00
|
|
|
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
|
2020-02-18 07:14:37 +00:00
|
|
|
}
|
2020-02-18 18:53:04 +00:00
|
|
|
b, e := ctx.reader(fname) // nolint: gosec
|
2018-01-25 02:14:21 +00:00
|
|
|
if e != nil {
|
|
|
|
valid = false
|
2019-05-20 05:30:32 +00:00
|
|
|
log.Printf("ERROR: Cannot read %s!\n", fname)
|
2018-01-25 02:14:21 +00:00
|
|
|
}
|
2018-01-22 06:13:35 +00:00
|
|
|
r := csv.NewReader(bytes.NewReader(b))
|
|
|
|
|
|
|
|
r.Comma = ':'
|
|
|
|
r.Comment = '#'
|
2018-09-09 05:01:33 +00:00
|
|
|
r.FieldsPerRecord = 3 // username:salt:authCookie [TODO:disallowedCmdList (a,b,...)]
|
2018-01-22 06:13:35 +00:00
|
|
|
for {
|
|
|
|
record, err := r.Read()
|
|
|
|
if err == io.EOF {
|
2018-09-12 05:36:20 +00:00
|
|
|
// 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
|
2018-01-22 06:13:35 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2018-01-23 21:53:05 +00:00
|
|
|
if username == record[0] {
|
2018-11-25 18:24:10 +00:00
|
|
|
tmp, err := bcrypt.Hash(auth, record[1])
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
2018-09-12 05:36:20 +00:00
|
|
|
if tmp == record[2] && username != "$nosuchuser$" {
|
2018-01-23 21:53:05 +00:00
|
|
|
valid = true
|
|
|
|
}
|
2018-01-22 06:13:35 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2018-05-05 06:25:26 +00:00
|
|
|
// Security scrub
|
|
|
|
for i := range b {
|
|
|
|
b[i] = 0
|
|
|
|
}
|
|
|
|
r = nil
|
|
|
|
runtime.GC()
|
|
|
|
|
2020-02-18 18:53:04 +00:00
|
|
|
_, userErr := ctx.userlookup(username)
|
2020-02-18 07:14:37 +00:00
|
|
|
if userErr != nil {
|
2018-10-04 05:31:35 +00:00
|
|
|
valid = false
|
|
|
|
}
|
2018-01-22 06:13:35 +00:00
|
|
|
return
|
|
|
|
}
|
2018-09-14 06:51:49 +00:00
|
|
|
|
2019-12-20 04:01:39 +00:00
|
|
|
// ------------- End xs-local passwd auth routine(s) -----------
|
|
|
|
|
2018-11-25 18:24:10 +00:00
|
|
|
// AuthUserByToken checks user login information against an auth token.
|
2019-10-30 03:34:09 +00:00
|
|
|
// Auth tokens are stored in each user's $HOME/.xs_id and are requested
|
2018-11-25 18:24:10 +00:00
|
|
|
// via the -g option.
|
|
|
|
// The function also check system /etc/passwd to cross-check the user
|
|
|
|
// actually exists.
|
2020-02-18 18:53:04 +00:00
|
|
|
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
|
2020-02-18 07:14:37 +00:00
|
|
|
}
|
2020-02-18 18:53:04 +00:00
|
|
|
if ctx.userlookup == nil {
|
|
|
|
ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
|
2020-02-18 07:14:37 +00:00
|
|
|
}
|
|
|
|
|
2018-09-14 18:58:10 +00:00
|
|
|
auth = strings.TrimSpace(auth)
|
2020-02-18 18:53:04 +00:00
|
|
|
u, ue := ctx.userlookup(username)
|
2018-09-14 06:51:49 +00:00
|
|
|
if ue != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-02-18 18:53:04 +00:00
|
|
|
b, e := ctx.reader(fmt.Sprintf("%s/.xs_id", u.HomeDir))
|
2018-09-14 06:51:49 +00:00
|
|
|
if e != nil {
|
2019-10-30 03:34:09 +00:00
|
|
|
log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir)
|
2018-09-14 06:51:49 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-09-14 18:58:10 +00:00
|
|
|
r := csv.NewReader(bytes.NewReader(b))
|
|
|
|
|
|
|
|
r.Comma = ':'
|
|
|
|
r.Comment = '#'
|
2021-02-01 04:18:48 +00:00
|
|
|
r.FieldsPerRecord = 3 // connhost:username:authtoken
|
2018-09-14 18:58:10 +00:00
|
|
|
for {
|
|
|
|
record, err := r.Read()
|
|
|
|
if err == io.EOF {
|
|
|
|
return false
|
|
|
|
}
|
2021-02-01 04:18:48 +00:00
|
|
|
if len(record) < 3 ||
|
|
|
|
len(record[0]) < 1 ||
|
|
|
|
len(record[1]) < 1 ||
|
|
|
|
len(record[2]) < 1 {
|
|
|
|
return false
|
|
|
|
}
|
2018-09-14 18:58:10 +00:00
|
|
|
record[0] = strings.TrimSpace(record[0])
|
|
|
|
record[1] = strings.TrimSpace(record[1])
|
2021-02-01 04:18:48 +00:00
|
|
|
record[2] = strings.TrimSpace(record[2])
|
2018-09-17 00:30:02 +00:00
|
|
|
//fmt.Println("auth:", auth, "record:",
|
2021-02-01 04:18:48 +00:00
|
|
|
// strings.Join([]string{record[0], record[1], record[2]}, ":"))
|
2018-09-14 18:58:10 +00:00
|
|
|
|
|
|
|
if (connhostname == record[0]) &&
|
2021-02-01 04:18:48 +00:00
|
|
|
username == record[1] &&
|
|
|
|
(auth == strings.Join([]string{record[0], record[1], record[2]}, ":")) {
|
2018-11-25 18:24:10 +00:00
|
|
|
valid = true
|
|
|
|
break
|
2018-09-14 18:58:10 +00:00
|
|
|
}
|
2018-09-14 06:51:49 +00:00
|
|
|
}
|
2020-02-18 18:53:04 +00:00
|
|
|
_, userErr := ctx.userlookup(username)
|
2020-02-18 07:14:37 +00:00
|
|
|
if userErr != nil {
|
2018-10-04 05:31:35 +00:00
|
|
|
valid = false
|
|
|
|
}
|
2018-09-14 06:51:49 +00:00
|
|
|
return
|
|
|
|
}
|
2020-04-26 01:03:29 +00:00
|
|
|
|
|
|
|
func GetTool(tool string) (ret string) {
|
2021-02-01 04:18:48 +00:00
|
|
|
ret = "/bin/" + tool
|
2020-04-26 01:03:29 +00:00
|
|
|
_, err := os.Stat(ret)
|
|
|
|
if err == nil {
|
|
|
|
return ret
|
|
|
|
}
|
2021-02-01 04:18:48 +00:00
|
|
|
ret = "/usr/bin/" + tool
|
2020-04-26 01:03:29 +00:00
|
|
|
_, err = os.Stat(ret)
|
|
|
|
if err == nil {
|
|
|
|
return ret
|
|
|
|
}
|
2021-02-01 04:18:48 +00:00
|
|
|
ret = "/usr/local/bin/" + tool
|
2020-04-26 01:03:29 +00:00
|
|
|
_, err = os.Stat(ret)
|
|
|
|
if err == nil {
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|