// Util to generate/store passwords for users in a file akin to /etc/passwd // suitable for the xs server, using bcrypt. // // 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) package main import ( "bytes" "encoding/csv" "flag" "fmt" "io/ioutil" "log" "os" xs "blitter.com/go/xs" "github.com/jameskeane/bcrypt" ) var ( version string gitCommit string ) // nolint: gocyclo func main() { var vopt bool var pfName string var newpw string var confirmpw string var userName string flag.BoolVar(&vopt, "v", false, "show version") flag.StringVar(&userName, "u", "", "username") flag.StringVar(&pfName, "f", "/etc/xs.passwd", "passwd file") flag.Parse() if vopt { fmt.Printf("version %s (%s)\n", version, gitCommit) os.Exit(0) } var uname string if len(userName) == 0 { log.Println("specify username with -u") os.Exit(1) } //u, err := user.Lookup(userName) //if err != nil { // log.Printf("Invalid user %s\n", userName) // log.Fatal(err) //} //uname = u.Username uname = userName fmt.Printf("New Password:") ab, err := xs.ReadPassword(os.Stdin.Fd()) fmt.Printf("\r\n") if err != nil { log.Fatal(err) os.Exit(1) } newpw = string(ab) fmt.Printf("Confirm:") ab, err = xs.ReadPassword(os.Stdin.Fd()) fmt.Printf("\r\n") if err != nil { log.Fatal(err) os.Exit(1) } confirmpw = string(ab) if confirmpw != newpw { log.Println("New passwords do not match.") os.Exit(1) } // generate a random salt with specific rounds of complexity // (default in jameskeane/bcrypt is 12 but we'll be explicit here) salt, err := bcrypt.Salt(12) if err != nil { fmt.Println("ERROR: bcrypt.Salt() failed.") os.Exit(2) } // hash and verify a password with explicit (random) salt hash, err := bcrypt.Hash(newpw, salt) if err != nil || !bcrypt.Match(newpw, hash) { fmt.Println("ERROR: bcrypt.Match() failed.") log.Fatal(err) } //fmt.Println("Salt:", salt, "Hash:", hash) b, err := ioutil.ReadFile(pfName) // nolint: gosec if err != nil { log.Fatal(err) } r := csv.NewReader(bytes.NewReader(b)) r.Comma = ':' r.Comment = '#' r.FieldsPerRecord = 3 // username:salt:authCookie [TODO:disallowedCmdList (a,b,...)] records, err := r.ReadAll() if err != nil { log.Fatal(err) } recFound := false for i := range records { //fmt.Println(records[i]) if records[i][0] == uname { recFound = true records[i][1] = salt records[i][2] = hash } //// csv lib doesn't preserve comment in record, so put it back //if records[i][0][0] == '!' { // records[i][0] = "#" + records[i][0] //} } if !recFound { newRec := []string{uname, salt, hash} records = append(records, newRec) } outFile, err := ioutil.TempFile("", "xs-passwd") if err != nil { log.Fatal(err) } w := csv.NewWriter(outFile) w.Comma = ':' //w.FieldsPerRecord = 4 // username:salt:authCookie:disallowedCmdList (a,b,...) err = w.Write([]string{"#username", "salt", "authCookie" /*, "disallowedCmdList"*/}) if err != nil { log.Fatal(err) } err = w.WriteAll(records) if err != nil { log.Fatal(err) } if err = w.Error(); err != nil { log.Fatal(err) } err = os.Remove(pfName) if err != nil { log.Fatal(err) } err = os.Rename(outFile.Name(), pfName) if err != nil { log.Fatal(err) } }