github.com/devops-filetransfer/sshego@v7.0.4+incompatible/user.go (about)

     1  package sshego
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  	"sync"
    10  	"time"
    11  
    12  	scrypt "github.com/elithrar/simple-scrypt"
    13  	"github.com/glycerine/greenpack/msgp"
    14  	"github.com/glycerine/sshego/xendor/github.com/glycerine/xcryptossh"
    15  	"github.com/pquerna/otp"
    16  )
    17  
    18  //go:generate greenpack
    19  
    20  // LoginRecord is per public key.
    21  type LoginRecord struct {
    22  	FirstTm       time.Time
    23  	LastTm        time.Time
    24  	SeenCount     int64
    25  	AcceptedCount int64
    26  	PubFinger     string
    27  }
    28  
    29  func (r LoginRecord) String() string {
    30  	return fmt.Sprintf(`LoginRecord{ FirstTm:"%s", LastTm:"%s", SeenCount:%v, AcceptedCount: %v, PubFinger:"%s"}`,
    31  		r.FirstTm, r.LastTm, r.SeenCount, r.AcceptedCount, r.PubFinger)
    32  }
    33  
    34  // User represents a user authorized
    35  // to login to the embedded sshd.
    36  type User struct {
    37  	MyEmail    string
    38  	MyFullname string
    39  	MyLogin    string
    40  
    41  	PublicKeyPath  string
    42  	PrivateKeyPath string
    43  	TOTPpath       string
    44  	QrPath         string
    45  
    46  	Issuer     string
    47  	PublicKey  ssh.PublicKey `msg:"-"`
    48  	SeenPubKey map[string]LoginRecord
    49  
    50  	ScryptedPassword []byte
    51  	ClearPw          string // only on network, never on disk.
    52  	TOTPorig         string
    53  	oneTime          *TOTP
    54  
    55  	FirstLoginTime time.Time
    56  	LastLoginTime  time.Time
    57  	LastLoginAddr  string
    58  	IPwhitelist    []string
    59  	DisabledAcct   bool
    60  
    61  	mut sync.Mutex
    62  }
    63  
    64  func (u *User) String() string {
    65  	var buf bytes.Buffer
    66  	err := msgp.Encode(&buf, u)
    67  	panicOn(err)
    68  	var js bytes.Buffer
    69  	_, err = msgp.CopyToJSON(&js, &buf)
    70  	panicOn(err)
    71  	return js.String()
    72  }
    73  
    74  func NewUser() *User {
    75  	u := &User{
    76  		SeenPubKey: make(map[string]LoginRecord),
    77  	}
    78  	return u
    79  }
    80  
    81  // only these fields are actually saved/restored.
    82  type HostDbPersist struct {
    83  	// Users: key is MyLogin; value is *User.
    84  	Users              *AtomicUserMap `zid:"0"`
    85  	HostPrivateKeyPath string         `zid:"1"`
    86  }
    87  
    88  type HostDb struct {
    89  	UserHomePrefix string
    90  
    91  	HostSshSigner ssh.Signer `msg:"-"`
    92  	cfg           *SshegoConfig
    93  
    94  	Persist HostDbPersist
    95  
    96  	loadedFromDisk bool
    97  
    98  	saveMut sync.Mutex
    99  
   100  	userTcp TcpPort
   101  
   102  	db Filedb
   103  }
   104  
   105  func (h *HostDb) String() string {
   106  	return h.Persist.Users.String()
   107  }
   108  
   109  func (cfg *SshegoConfig) NewHostDb() error {
   110  	p("SshegoConfig.NewHostDB() called...")
   111  	h := &HostDb{
   112  		UserHomePrefix: "",
   113  		cfg:            cfg,
   114  		Persist: HostDbPersist{
   115  			Users: NewAtomicUserMap(),
   116  		},
   117  		userTcp: TcpPort{Port: cfg.SshegoSystemMutexPort},
   118  	}
   119  	cfg.HostDb = h
   120  	return h.init()
   121  }
   122  
   123  func (h *HostDb) privpath() string {
   124  	return h.cfg.EmbeddedSSHdHostDbPath + ".hostkey"
   125  }
   126  
   127  func (h *HostDb) init() error {
   128  	h.Persist.HostPrivateKeyPath = h.privpath()
   129  	p("HostDb.init(): h.Persist.HostPrivateKeyPath = '%v'", h.Persist.HostPrivateKeyPath)
   130  	err := h.loadOrCreate()
   131  	return err
   132  }
   133  
   134  func (h *HostDb) generateHostKey() error {
   135  	p("generateHostKey called.")
   136  	err := h.gendir()
   137  	if err != nil {
   138  		return err
   139  	}
   140  	path := h.privpath()
   141  	bits := h.cfg.BitLenRSAkeys // default 4096
   142  
   143  	p("\n bits = %v\n", bits)
   144  	host, _ := os.Hostname()
   145  	_, signer, err := GenRSAKeyPair(path, bits, host)
   146  	if err != nil {
   147  		return err
   148  	}
   149  	h.HostSshSigner = signer
   150  	h.Persist.HostPrivateKeyPath = path
   151  	return nil
   152  }
   153  
   154  func (h *HostDb) gendir() error {
   155  	path := h.cfg.EmbeddedSSHdHostDbPath
   156  	p("HostDb.gendir has h.cfg.EmbeddedSSHdHostDbPath='%s'", h.cfg.EmbeddedSSHdHostDbPath)
   157  	if dirExists(path) {
   158  		return nil
   159  	}
   160  	err := os.MkdirAll(path, 0777)
   161  	if err != nil {
   162  		return fmt.Errorf("HostDb: MkdirAll on '%s' failed: %v",
   163  			path, err)
   164  	}
   165  	return nil
   166  }
   167  
   168  func makeway(path string) error {
   169  	dir := filepath.Dir(path)
   170  	return os.MkdirAll(dir, 0777)
   171  }
   172  
   173  func (h *HostDb) msgpath() string {
   174  	return h.cfg.EmbeddedSSHdHostDbPath + "/msgp.db"
   175  }
   176  
   177  func (h *HostDb) userpath(username string) string {
   178  	return h.cfg.EmbeddedSSHdHostDbPath + "/users/" + username
   179  }
   180  
   181  func (h *HostDb) Rsapath(username string) string {
   182  	return h.cfg.EmbeddedSSHdHostDbPath + "/users/" + username + "/id_rsa"
   183  }
   184  
   185  func (h *HostDb) toptpath(username string) string {
   186  	return h.cfg.EmbeddedSSHdHostDbPath + "/users/" + username + "/topt"
   187  }
   188  
   189  const skiplock = false
   190  const lockit = true
   191  
   192  // always opens h.msgpath()
   193  func (h *HostDb) opendb() error {
   194  	if h.cfg.EmbeddedSSHdHostDbPath == "" {
   195  		panic("opendb() called on empty h.cfg.EmbeddedSSHdHostDbPath")
   196  	}
   197  	p("HostDb.opendb() has h.cfg.EmbeddedSSHdHostDbPath='%s'", h.cfg.EmbeddedSSHdHostDbPath)
   198  	if h.db.HostDb == nil {
   199  		err := h.gendir()
   200  		if err != nil {
   201  			return err
   202  		}
   203  
   204  		filedb, err := NewFiledb(h.msgpath())
   205  		if err != nil {
   206  			return fmt.Errorf("HostDb.opendb: create newFiledb at '%s' failed: %v",
   207  				h.msgpath(), err)
   208  		}
   209  		if filedb.HostDb != nil {
   210  			h.Persist = filedb.HostDb.Persist
   211  			filedb.HostDb = h
   212  		}
   213  	}
   214  	return nil
   215  }
   216  
   217  // There should only one writer to disk at a time...
   218  // Let this be the main handshake/user auth goroutine
   219  // that listens for sshd connections.
   220  func (h *HostDb) save(lock bool) error {
   221  	if lock == lockit {
   222  		h.saveMut.Lock()
   223  		defer h.saveMut.Unlock()
   224  	}
   225  
   226  	h.db.filepath = h.msgpath()
   227  	err := h.db.storeHostDb(h)
   228  	if err != nil {
   229  		return fmt.Errorf("HostDb: h.db.storeHostDb(h) gave error = '%v'", err)
   230  	}
   231  	return nil
   232  }
   233  
   234  func (h *HostDb) loadOrCreate() error {
   235  	p("top of HostDb.loadOrCreate()...")
   236  	p("HostDb.loadOrCreate has h.cfg.EmbeddedSSHdHostDbPath='%s'", h.cfg.EmbeddedSSHdHostDbPath)
   237  	err := h.opendb()
   238  	if err != nil {
   239  		panic(err)
   240  		return fmt.Errorf("HostDb.loadOrCreate(): opendb() at path '%s' gave error '%v'",
   241  			h.msgpath(), err)
   242  	}
   243  
   244  	if h.Persist.HostPrivateKeyPath != "" && fileExists(h.Persist.HostPrivateKeyPath) {
   245  		p("h.Persist.HostPrivateKeyPath exists already... loaded HostDb from msgpath()='%s'. db = '%s'", h.msgpath(), h)
   246  
   247  	} else {
   248  
   249  		p("h.Persist.HostPrivateKeyPath = '%s' doesn't exist; make a host key...", h.msgpath())
   250  
   251  		// no db, so make a host key
   252  		err := h.generateHostKey()
   253  		if err != nil {
   254  			return err
   255  		}
   256  
   257  		err = h.save(skiplock)
   258  
   259  		if err != nil {
   260  			return fmt.Errorf("HostDb.Save MarshalMsg failed: %v", err)
   261  		}
   262  
   263  	}
   264  	h.loadedFromDisk = true
   265  
   266  	if fileExists(h.Persist.HostPrivateKeyPath) {
   267  		_, err := h.adoptNewHostKeyFromPath(h.Persist.HostPrivateKeyPath)
   268  		if err != nil {
   269  			return err
   270  		}
   271  	} else {
   272  		panic(fmt.Sprintf("missing h.Persist.HostPrivateKeyPath='%s'", h.Persist.HostPrivateKeyPath))
   273  	}
   274  	return nil
   275  }
   276  
   277  func (h *HostDb) adoptNewHostKeyFromPath(path string) (ssh.PublicKey, error) {
   278  	if !fileExists(path) {
   279  		return nil, fmt.Errorf("error in adoptNewHostKeyFromPath: path '%s' does not exist", path)
   280  	}
   281  
   282  	sshPrivKey, err := LoadRSAPrivateKey(path)
   283  	if err != nil {
   284  		return nil, fmt.Errorf("error in adoptNewHostKeyFromPath: loading"+
   285  			" path '%s' with LoadRSAPrivateKey() resulted in error '%v'", path, err)
   286  	}
   287  
   288  	// avoid data race:
   289  	h.saveMut.Lock()
   290  	h.HostSshSigner = sshPrivKey
   291  	h.saveMut.Unlock()
   292  
   293  	h.Persist.HostPrivateKeyPath = path
   294  	return sshPrivKey.PublicKey(), nil
   295  }
   296  
   297  func ScryptHash(password string) []byte {
   298  	hash, err := scrypt.GenerateFromPassword([]byte(password), scrypt.DefaultParams)
   299  	panicOn(err)
   300  	return hash
   301  }
   302  
   303  func (user *User) MatchingHashAndPw(password string) bool {
   304  	return nil == scrypt.CompareHashAndPassword(user.ScryptedPassword, []byte(password))
   305  }
   306  
   307  // emailAddressRE matches the mail addresses
   308  // we admit. Since we are writing out
   309  // to file system paths that include the email,
   310  // we want to be restrictive.
   311  //
   312  var emailAddressREstring = `^([a-zA-Z0-9][\+-_.a-zA-Z0-9]{0,63})@([-_.a-zA-Z0-9]{1,255})$`
   313  var emailAddressRE = regexp.MustCompile(emailAddressREstring)
   314  
   315  // AddUser will use an existing extantRsaPath path to private key if provided, otherwise
   316  // we make a new private/public key pair.
   317  //
   318  func (h *HostDb) AddUser(mylogin, myemail, pw, issuer, fullname, extantPrivateKeyPath string) (toptPath, qrPath, rsaPath string, err error) {
   319  
   320  	p("AddUser mylogin:'%v' pw:'%v' myemail:'%v'", mylogin, pw, myemail)
   321  
   322  	var valid bool
   323  	valid, err = h.ValidLogin(mylogin)
   324  	if !valid {
   325  		// err already set
   326  		return
   327  	}
   328  
   329  	p("h = %#v", h)
   330  	_, ok := h.Persist.Users.Get2(mylogin)
   331  	if ok {
   332  		err = fmt.Errorf("user already exists; manually -deluser '%s' first!",
   333  			mylogin)
   334  		return
   335  	} else {
   336  		p("brand new user '%s'", mylogin)
   337  	}
   338  	if extantPrivateKeyPath != "" {
   339  		rsaPath = extantPrivateKeyPath
   340  	} else {
   341  		rsaPath = h.Rsapath(mylogin)
   342  	}
   343  
   344  	//	path := h.userpath(mylogin)
   345  
   346  	user := NewUser()
   347  	user.MyLogin = mylogin
   348  	user.MyEmail = myemail
   349  	user.ClearPw = pw
   350  	user.Issuer = issuer
   351  	user.MyFullname = fullname
   352  	if !h.cfg.SkipRSA {
   353  		user.PrivateKeyPath = rsaPath
   354  		user.PublicKeyPath = rsaPath + ".pub"
   355  	}
   356  	return h.finishUserBuildout(user)
   357  }
   358  
   359  func (h *HostDb) finishUserBuildout(user *User) (toptPath, qrPath, rsaPath string, err error) {
   360  	p("finishUserBuildout started: user.MyLogin:'%v' user.ClearPw:'%v' user.MyEmail:'%v' toptPath='%v'",
   361  		user.MyLogin, user.ClearPw, user.MyEmail, toptPath)
   362  
   363  	if !h.cfg.SkipPassphrase {
   364  		user.ScryptedPassword = ScryptHash(user.ClearPw)
   365  	}
   366  
   367  	if !h.cfg.SkipTOTP {
   368  		var w *TOTP
   369  		w, err = NewTOTP(user.MyEmail, fmt.Sprintf("%s/%s", user.MyLogin, user.Issuer))
   370  		if err != nil {
   371  			panic(err)
   372  		}
   373  		toptPath = h.toptpath(user.MyLogin)
   374  		user.TOTPpath = toptPath
   375  		makeway(toptPath)
   376  
   377  		user.TOTPorig = w.Key.String()
   378  		_, qrPath, err = w.SaveToFile(toptPath)
   379  		panicOn(err)
   380  		user.oneTime = w
   381  		user.QrPath = qrPath
   382  	}
   383  
   384  	if !h.cfg.SkipRSA {
   385  		// rsa private key already exists and supplied above?
   386  		if user.PrivateKeyPath != "" && fileExists(user.PrivateKeyPath) {
   387  			rsaPath = user.PrivateKeyPath
   388  		} else {
   389  
   390  			// need to make a new
   391  			rsaPath = h.Rsapath(user.MyLogin)
   392  			user.PrivateKeyPath = rsaPath
   393  			user.PublicKeyPath = rsaPath + ".pub"
   394  
   395  			makeway(rsaPath)
   396  			bits := h.cfg.BitLenRSAkeys // default 4096
   397  
   398  			var signer ssh.Signer
   399  			_, signer, err = GenRSAKeyPair(rsaPath, bits, user.MyEmail)
   400  			if err != nil {
   401  				return
   402  			}
   403  			user.PublicKeyPath = rsaPath + ".pub"
   404  			user.PublicKey = signer.PublicKey()
   405  		}
   406  	}
   407  
   408  	// don't save ClearPw to disk, and no need
   409  	// to ship it back b/c they supplied it in
   410  	// the first place (and we can't change it
   411  	// after the fact).
   412  	user.ClearPw = ""
   413  
   414  	//	p("user = %#v", user)
   415  	h.Persist.Users.Set(user.MyLogin, user)
   416  
   417  	err = h.save(lockit)
   418  	return
   419  }
   420  
   421  func (h *HostDb) DelUser(mylogin string) error {
   422  
   423  	ok, err := h.ValidLogin(mylogin)
   424  	if !ok {
   425  		return err
   426  	}
   427  	/*
   428  		if !emailAddressRE.MatchString(mylogin) {
   429  			return fmt.Errorf("We are restrictive about what we "+
   430  				"accept as user email, and '%s' doesn't match "+
   431  				"our permitted regex '%s'", myemail, emailAddressREstring)
   432  		}
   433  	*/
   434  
   435  	p("DelUser %v", mylogin)
   436  	_, ok = h.Persist.Users.Get2(mylogin)
   437  
   438  	if ok {
   439  		// cleanup old
   440  		path := h.userpath(mylogin)
   441  		err := os.RemoveAll(path)
   442  		h.Persist.Users.Del(mylogin)
   443  		if err != nil {
   444  			panicOn(err)
   445  		}
   446  		return h.save(lockit)
   447  	}
   448  	return fmt.Errorf("error in -userdel '%s': user not found.", mylogin)
   449  }
   450  
   451  func (user *User) RestoreTotp() {
   452  	if user.oneTime == nil && user.TOTPorig != "" {
   453  		user.oneTime = &TOTP{}
   454  		w, err := otp.NewKeyFromURL(user.TOTPorig)
   455  		panicOn(err)
   456  		user.oneTime.Key = w
   457  	}
   458  }
   459  
   460  // UserExists is used by sshego/cmd/gosshtun/main.go
   461  func (h *HostDb) UserExists(mylogin string) bool {
   462  	_, ok := h.Persist.Users.Get2(mylogin)
   463  	return ok
   464  }
   465  
   466  func (h *HostDb) ValidEmail(myemail string) (bool, error) {
   467  	if !emailAddressRE.MatchString(myemail) {
   468  		return false, fmt.Errorf("bad email: '%s' did not "+
   469  			"conform to '%s'. Please provide a conforming "+
   470  			"email if you wish to opt-in to passphrase "+
   471  			"backup to email.", myemail, emailAddressREstring)
   472  	}
   473  	return true, nil
   474  }
   475  
   476  var loginREstring = `^[a-z][-_a-z0-9]{0,31}$`
   477  var loginRE = regexp.MustCompile(loginREstring)
   478  
   479  func (h *HostDb) ValidLogin(login string) (bool, error) {
   480  	if !loginRE.MatchString(login) {
   481  		return false, fmt.Errorf("bad login: '%s' did not conform to '%s'",
   482  			login, loginREstring)
   483  	}
   484  	return true, nil
   485  }