auth.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. package xs
  2. // Package xs - a secure terminal client/server written from scratch in Go
  3. //
  4. // Copyright (c) 2017-2020 Russell Magee
  5. // Licensed under the terms of the MIT license (see LICENSE.mit in this
  6. // distribution)
  7. //
  8. // golang implementation by Russ Magee (rmagee_at_gmail.com)
  9. // Authentication routines for the HKExSh
  10. import (
  11. "bytes"
  12. "encoding/csv"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "io/ioutil"
  17. "log"
  18. "os"
  19. "os/user"
  20. "runtime"
  21. "strings"
  22. "github.com/jameskeane/bcrypt"
  23. passlib "gopkg.in/hlandau/passlib.v1"
  24. )
  25. type AuthCtx struct {
  26. reader func(string) ([]byte, error) // eg. ioutil.ReadFile()
  27. userlookup func(string) (*user.User, error) // eg. os/user.Lookup()
  28. }
  29. func NewAuthCtx( /*reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)*/ ) (ret *AuthCtx) {
  30. ret = &AuthCtx{ioutil.ReadFile, user.Lookup}
  31. return
  32. }
  33. // --------- System passwd/shadow auth routine(s) --------------
  34. // VerifyPass verifies a password against system standard shadow file
  35. // Note auxilliary fields for expiry policy are *not* inspected.
  36. func VerifyPass(ctx *AuthCtx, user, password string) (bool, error) {
  37. if ctx.reader == nil {
  38. ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
  39. }
  40. passlib.UseDefaults(passlib.Defaults20180601)
  41. var pwFileName string
  42. if runtime.GOOS == "linux" {
  43. pwFileName = "/etc/shadow"
  44. } else if runtime.GOOS == "freebsd" {
  45. pwFileName = "/etc/master.passwd"
  46. } else {
  47. pwFileName = "unsupported"
  48. }
  49. pwFileData, e := ctx.reader(pwFileName)
  50. if e != nil {
  51. return false, e
  52. }
  53. pwLines := strings.Split(string(pwFileData), "\n")
  54. if len(pwLines) < 1 {
  55. return false, errors.New("Empty shadow file!")
  56. } else {
  57. var line string
  58. var hash string
  59. var idx int
  60. for idx = range pwLines {
  61. line = pwLines[idx]
  62. lFields := strings.Split(line, ":")
  63. if lFields[0] == user {
  64. hash = lFields[1]
  65. break
  66. }
  67. }
  68. if len(hash) == 0 {
  69. return false, errors.New("nil hash!")
  70. } else {
  71. pe := passlib.VerifyNoUpgrade(password, hash)
  72. if pe != nil {
  73. return false, pe
  74. }
  75. }
  76. }
  77. return true, nil
  78. }
  79. // --------- End System passwd/shadow auth routine(s) ----------
  80. // ------------- xs-local passwd auth routine(s) ---------------
  81. // AuthUserByPasswd checks user login information using a password.
  82. // This checks /etc/xs.passwd for auth info, and system /etc/passwd
  83. // to cross-check the user actually exists.
  84. // nolint: gocyclo
  85. func AuthUserByPasswd(ctx *AuthCtx, username string, auth string, fname string) (valid bool, allowedCmds string) {
  86. if ctx.reader == nil {
  87. ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
  88. }
  89. if ctx.userlookup == nil {
  90. ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
  91. }
  92. b, e := ctx.reader(fname) // nolint: gosec
  93. if e != nil {
  94. valid = false
  95. log.Printf("ERROR: Cannot read %s!\n", fname)
  96. }
  97. r := csv.NewReader(bytes.NewReader(b))
  98. r.Comma = ':'
  99. r.Comment = '#'
  100. r.FieldsPerRecord = 3 // username:salt:authCookie [TODO:disallowedCmdList (a,b,...)]
  101. for {
  102. record, err := r.Read()
  103. if err == io.EOF {
  104. // Use dummy entry if user not found
  105. // (prevent user enumeration attack via obvious timing diff;
  106. // ie., not attempting any auth at all)
  107. record = []string{"$nosuchuser$",
  108. "$2a$12$l0coBlRDNEJeQVl6GdEPbU",
  109. "$2a$12$l0coBlRDNEJeQVl6GdEPbUC/xmuOANvqgmrMVum6S4i.EXPgnTXy6"}
  110. username = "$nosuchuser$"
  111. err = nil
  112. }
  113. if err != nil {
  114. log.Fatal(err)
  115. }
  116. if username == record[0] {
  117. tmp, err := bcrypt.Hash(auth, record[1])
  118. if err != nil {
  119. break
  120. }
  121. if tmp == record[2] && username != "$nosuchuser$" {
  122. valid = true
  123. }
  124. break
  125. }
  126. }
  127. // Security scrub
  128. for i := range b {
  129. b[i] = 0
  130. }
  131. r = nil
  132. runtime.GC()
  133. _, userErr := ctx.userlookup(username)
  134. if userErr != nil {
  135. valid = false
  136. }
  137. return
  138. }
  139. // ------------- End xs-local passwd auth routine(s) -----------
  140. // AuthUserByToken checks user login information against an auth token.
  141. // Auth tokens are stored in each user's $HOME/.xs_id and are requested
  142. // via the -g option.
  143. // The function also check system /etc/passwd to cross-check the user
  144. // actually exists.
  145. func AuthUserByToken(ctx *AuthCtx, username string, connhostname string, auth string) (valid bool) {
  146. if ctx.reader == nil {
  147. ctx.reader = ioutil.ReadFile // dependency injection hides that this is required
  148. }
  149. if ctx.userlookup == nil {
  150. ctx.userlookup = user.Lookup // again for dependency injection as dep is now hidden
  151. }
  152. auth = strings.TrimSpace(auth)
  153. u, ue := ctx.userlookup(username)
  154. if ue != nil {
  155. return false
  156. }
  157. b, e := ctx.reader(fmt.Sprintf("%s/.xs_id", u.HomeDir))
  158. if e != nil {
  159. log.Printf("INFO: Cannot read %s/.xs_id\n", u.HomeDir)
  160. return false
  161. }
  162. r := csv.NewReader(bytes.NewReader(b))
  163. r.Comma = ':'
  164. r.Comment = '#'
  165. r.FieldsPerRecord = 3 // connhost:username:authtoken
  166. for {
  167. record, err := r.Read()
  168. if err == io.EOF {
  169. return false
  170. }
  171. if len(record) < 3 ||
  172. len(record[0]) < 1 ||
  173. len(record[1]) < 1 ||
  174. len(record[2]) < 1 {
  175. return false
  176. }
  177. record[0] = strings.TrimSpace(record[0])
  178. record[1] = strings.TrimSpace(record[1])
  179. record[2] = strings.TrimSpace(record[2])
  180. //fmt.Println("auth:", auth, "record:",
  181. // strings.Join([]string{record[0], record[1], record[2]}, ":"))
  182. if (connhostname == record[0]) &&
  183. username == record[1] &&
  184. (auth == strings.Join([]string{record[0], record[1], record[2]}, ":")) {
  185. valid = true
  186. break
  187. }
  188. }
  189. _, userErr := ctx.userlookup(username)
  190. if userErr != nil {
  191. valid = false
  192. }
  193. return
  194. }
  195. func GetTool(tool string) (ret string) {
  196. ret = "/bin/" + tool
  197. _, err := os.Stat(ret)
  198. if err == nil {
  199. return ret
  200. }
  201. ret = "/usr/bin/" + tool
  202. _, err = os.Stat(ret)
  203. if err == nil {
  204. return ret
  205. }
  206. ret = "/usr/local/bin/" + tool
  207. _, err = os.Stat(ret)
  208. if err == nil {
  209. return ret
  210. }
  211. return ""
  212. }