mirror of
https://gogs.blitter.com/RLabs/lpasswd
synced 2024-08-14 19:26:42 +00:00
Initial commit
This commit is contained in:
commit
f7e1f859fd
4 changed files with 605 additions and 0 deletions
158
auth_test.go
Executable file
158
auth_test.go
Executable file
|
@ -0,0 +1,158 @@
|
|||
package lpasswd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/user"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type userVerifs struct {
|
||||
user string
|
||||
passwd string
|
||||
good bool
|
||||
}
|
||||
|
||||
var (
|
||||
dummyShadowA = `johndoe:$6$EeQlTtn/KXdSh6CW$UHbFuEw3UA0Jg9/GoPHxgWk6Ws31x3IjqsP22a9pVMOte0yQwX1.K34oI4FACu8GRg9DArJ5RyWUE9m98qwzZ1:18310:0:99999:7:::
|
||||
joebloggs:$6$F.0IXOrb0w0VJHG1$3O4PYyng7F3hlh42mbroEdQZvslybY5etPPiLMQJ1xosjABY.Q4xqAfyIfe03Du61ZjGQIt3nL0j12P9k1fsK/:18310:0:99999:7:::
|
||||
disableduser:!:18310::::::`
|
||||
|
||||
dummyXsPasswdFile = `#username:salt:authCookie
|
||||
bobdobbs:$2a$12$9vqGkFqikspe/2dTARqu1O:$2a$12$9vqGkFqikspe/2dTARqu1OuDKCQ/RYWsnaFjmi.HtmECRkxcZ.kBK
|
||||
notbob:$2a$12$cZpiYaq5U998cOkXzRKdyu:$2a$12$cZpiYaq5U998cOkXzRKdyuJ2FoEQyVLa3QkYdPQk74VXMoAzhvuP6
|
||||
`
|
||||
|
||||
testGoodUsers = []userVerifs{
|
||||
{"johndoe", "testpass", true},
|
||||
{"joebloggs", "testpass2", true},
|
||||
{"johndoe", "badpass", false},
|
||||
}
|
||||
|
||||
userlookup_arg_u string
|
||||
readfile_arg_f string
|
||||
)
|
||||
|
||||
func newMockAuthCtx(reader func(string) ([]byte, error), userlookup func(string) (*user.User, error)) (ret *AuthCtx) {
|
||||
ret = &AuthCtx{reader, userlookup}
|
||||
return
|
||||
}
|
||||
|
||||
func _mock_user_Lookup(username string) (*user.User, error) { // nolint:staticcheck
|
||||
username = userlookup_arg_u
|
||||
if username == "baduser" {
|
||||
return &user.User{}, errors.New("bad user")
|
||||
}
|
||||
urec := &user.User{Uid: "1000", Gid: "1000", Username: username, Name: "Full Name", HomeDir: "/home/user"}
|
||||
fmt.Printf(" [mock user rec:%v]\n", urec)
|
||||
return urec, nil
|
||||
}
|
||||
|
||||
func _mock_ioutil_ReadFile(f string) ([]byte, error) { //nolint:staticcheck
|
||||
f = readfile_arg_f
|
||||
if f == "/etc/shadow" {
|
||||
fmt.Println(" [mocking ReadFile(\"/etc/shadow\")]")
|
||||
return []byte(dummyShadowA), nil
|
||||
}
|
||||
if f == "/etc/xs.passwd" {
|
||||
fmt.Println(" [mocking ReadFile(\"/etc/xs.passwd\")]")
|
||||
return []byte(dummyXsPasswdFile), nil
|
||||
}
|
||||
return []byte{}, errors.New("no readfile_arg_f supplied")
|
||||
}
|
||||
|
||||
func _mock_ioutil_ReadFileEmpty(f string) ([]byte, error) {
|
||||
return []byte{}, io.EOF
|
||||
}
|
||||
|
||||
func _mock_ioutil_ReadFileHasError(f string) ([]byte, error) { // nolint:deadcode,unused
|
||||
return []byte{}, errors.New("IO Error")
|
||||
}
|
||||
|
||||
func TestVerifyPass(t *testing.T) {
|
||||
readfile_arg_f = "/etc/shadow"
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, nil)
|
||||
for idx, rec := range testGoodUsers {
|
||||
stat, e := VerifyPass(ctx, rec.user, rec.passwd)
|
||||
if rec.good && (!stat || e != nil) {
|
||||
t.Fatalf("failed %d\n", idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassFailsOnEmptyFile(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
|
||||
stat, e := VerifyPass(ctx, "johndoe", "somepass")
|
||||
if stat || (e == nil) {
|
||||
t.Fatal("failed to fail w/empty file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassFailsOnFileError(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
|
||||
stat, e := VerifyPass(ctx, "johndoe", "somepass")
|
||||
if stat || (e == nil) {
|
||||
t.Fatal("failed to fail on ioutil.ReadFile error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPassFailsOnDisabledEntry(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, nil)
|
||||
stat, e := VerifyPass(ctx, "disableduser", "!")
|
||||
if stat || (e == nil) {
|
||||
t.Fatal("failed to fail on disabled user entry")
|
||||
}
|
||||
}
|
||||
|
||||
////
|
||||
|
||||
func TestAuthUserByPasswdFailsOnEmptyFile(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFileEmpty, _mock_user_Lookup)
|
||||
userlookup_arg_u = "bobdobbs"
|
||||
readfile_arg_f = "/etc/xs.passwd"
|
||||
stat, _ := AuthUserByPasswd(ctx, false, userlookup_arg_u, "praisebob", readfile_arg_f)
|
||||
if stat {
|
||||
t.Fatal("failed to fail with missing xs.passwd file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthUserByPasswdFailsOnBadAuth(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
|
||||
userlookup_arg_u = "bobdobbs"
|
||||
readfile_arg_f = "/etc/xs.passwd"
|
||||
stat, _ := AuthUserByPasswd(ctx, false, userlookup_arg_u, "wrongpass", readfile_arg_f)
|
||||
if stat {
|
||||
t.Fatal("failed to fail with valid user, incorrect passwd in xs.passwd file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthUserByPasswdFailsOnBadUser(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
|
||||
userlookup_arg_u = "bobdobbs"
|
||||
readfile_arg_f = "/etc/xs.passwd"
|
||||
stat, _ := AuthUserByPasswd(ctx, false, userlookup_arg_u, "theotherbob", readfile_arg_f)
|
||||
if stat {
|
||||
t.Fatal("failed to fail on invalid user vs. xs.passwd file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthUserByPasswdPassesOnGoodAuth(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
|
||||
userlookup_arg_u = "bobdobbs"
|
||||
readfile_arg_f = "/etc/xs.passwd"
|
||||
stat, _ := AuthUserByPasswd(ctx, false, userlookup_arg_u, "praisebob", readfile_arg_f)
|
||||
if !stat {
|
||||
t.Fatal("failed on valid user w/correct passwd in xs.passwd file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthUserByPasswdPassesOnOtherGoodAuth(t *testing.T) {
|
||||
ctx := newMockAuthCtx(_mock_ioutil_ReadFile, _mock_user_Lookup)
|
||||
userlookup_arg_u = "notbob"
|
||||
readfile_arg_f = "/etc/xs.passwd"
|
||||
stat, _ := AuthUserByPasswd(ctx, false, userlookup_arg_u, "imposter", readfile_arg_f)
|
||||
if !stat {
|
||||
t.Fatal("failed on valid user 2nd entry w/correct passwd in xs.passwd file")
|
||||
}
|
||||
}
|
136
internal/termmode/termmode_linux.go
Executable file
136
internal/termmode/termmode_linux.go
Executable file
|
@ -0,0 +1,136 @@
|
|||
// +build linux
|
||||
|
||||
package termmode
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
unix "golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
/* -------------
|
||||
* minimal terminal APIs brought in from ssh/terminal
|
||||
* (they have no real business being there as they aren't specific to
|
||||
* ssh, but as of Go v1.10, late 2019, core go stdlib hasn't yet done
|
||||
* the planned terminal lib reorgs.)
|
||||
* ------------- */
|
||||
|
||||
// From github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go
|
||||
const ioctlReadTermios = unix.TCGETS
|
||||
const ioctlWriteTermios = unix.TCSETS
|
||||
|
||||
// From github.com/golang/crypto/blob/master/ssh/terminal/util.go
|
||||
|
||||
// State contains the state of a terminal.
|
||||
type State struct {
|
||||
termios unix.Termios
|
||||
}
|
||||
|
||||
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
func MakeRaw(fd uintptr) (*State, error) {
|
||||
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldState := State{termios: *termios}
|
||||
|
||||
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||
// the termios(3) manpage.
|
||||
termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
|
||||
termios.Oflag &^= unix.OPOST
|
||||
termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
|
||||
termios.Cflag &^= unix.CSIZE | unix.PARENB
|
||||
termios.Cflag |= unix.CS8
|
||||
termios.Cc[unix.VMIN] = 1
|
||||
termios.Cc[unix.VTIME] = 0
|
||||
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oldState, nil
|
||||
}
|
||||
|
||||
// GetState returns the current state of a terminal which may be useful to
|
||||
// restore the terminal after a signal.
|
||||
func GetState(fd uintptr) (*State, error) {
|
||||
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &State{termios: *termios}, nil
|
||||
}
|
||||
|
||||
// Restore restores the terminal connected to the given file descriptor to a
|
||||
// previous state.
|
||||
func Restore(fd uintptr, state *State) error {
|
||||
if state != nil {
|
||||
return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.termios)
|
||||
} else {
|
||||
return errors.New("nil State")
|
||||
}
|
||||
}
|
||||
|
||||
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||
// returned does not include the \n.
|
||||
func ReadPassword(fd uintptr) ([]byte, error) {
|
||||
termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newState := *termios
|
||||
newState.Lflag &^= unix.ECHO
|
||||
newState.Lflag |= unix.ICANON | unix.ISIG
|
||||
newState.Iflag |= unix.ICRNL
|
||||
if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) // nolint: gosec
|
||||
}()
|
||||
|
||||
return readPasswordLine(passwordReader(fd))
|
||||
}
|
||||
|
||||
// passwordReader is an io.Reader that reads from a specific file descriptor.
|
||||
type passwordReader int
|
||||
|
||||
func (r passwordReader) Read(buf []byte) (int, error) {
|
||||
return unix.Read(int(r), buf)
|
||||
}
|
||||
|
||||
// readPasswordLine reads from reader until it finds \n or io.EOF.
|
||||
// The slice returned does not include the \n.
|
||||
// readPasswordLine also ignores any \r it finds.
|
||||
func readPasswordLine(reader io.Reader) ([]byte, error) {
|
||||
var buf [1]byte
|
||||
var ret []byte
|
||||
|
||||
for {
|
||||
n, err := reader.Read(buf[:])
|
||||
if n > 0 {
|
||||
switch buf[0] {
|
||||
case '\n':
|
||||
return ret, nil
|
||||
case '\r':
|
||||
// remove \r from passwords on Windows
|
||||
default:
|
||||
ret = append(ret, buf[0])
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF && len(ret) > 0 {
|
||||
return ret, nil
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
}
|
||||
}
|
231
lpasswd.go
Executable file
231
lpasswd.go
Executable file
|
@ -0,0 +1,231 @@
|
|||
// lpasswd - lib to handle storage and lookup of app-local users
|
||||
// with passwords akin to /etc/passwd.
|
||||
//
|
||||
// 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 lpasswd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
passlib "gopkg.in/hlandau/passlib.v1"
|
||||
|
||||
"github.com/jameskeane/bcrypt"
|
||||
)
|
||||
|
||||
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) //nolint:errcheck
|
||||
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)
|
||||
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) ----------
|
||||
|
||||
// --------- Local passwd auth routine(s) --------------
|
||||
|
||||
// AuthUserByPasswd checks user login information using a password.
|
||||
// This checks file _fname_ for auth info, and optionally system /etc/passwd
|
||||
// to cross-check the user actually exists, if sysacct == true.
|
||||
// nolint: gocyclo
|
||||
func AuthUserByPasswd(ctx *AuthCtx, sysacct bool, username string, auth string, fname string) (valid bool, err error) {
|
||||
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 {
|
||||
return false, fmt.Errorf("cannot read %s", fname)
|
||||
}
|
||||
r := csv.NewReader(bytes.NewReader(b))
|
||||
|
||||
r.Comma = ':'
|
||||
r.Comment = '#'
|
||||
r.FieldsPerRecord = 3 // username:salt:authCookie
|
||||
for {
|
||||
var record []string
|
||||
record, err = r.Read()
|
||||
if errors.Is(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 {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if username == record[0] {
|
||||
var tmp string
|
||||
tmp, err = bcrypt.Hash(auth, record[1])
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if tmp == record[2] && username != "$nosuchuser$" {
|
||||
valid = true
|
||||
} else {
|
||||
err = fmt.Errorf("auth failure")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// Security scrub
|
||||
for i := range b {
|
||||
b[i] = 0
|
||||
}
|
||||
r = nil
|
||||
runtime.GC()
|
||||
|
||||
if sysacct {
|
||||
_, userErr := ctx.userlookup(username)
|
||||
if userErr != nil {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetPasswd enters _uname_ with the specified _passwd_ into the
|
||||
// local password file _passwdFName_.
|
||||
func SetPasswd(uname, passwd, passwdFName string) (e error) { // nolint: gocyclo
|
||||
if uname == "" {
|
||||
return errors.New("must specify a username")
|
||||
}
|
||||
// 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 {
|
||||
return fmt.Errorf("bcrypt.Salt() failed: %w", err)
|
||||
}
|
||||
// hash and verify a password with explicit (random) salt
|
||||
hash, err := bcrypt.Hash(passwd, salt)
|
||||
if err != nil || !bcrypt.Match(passwd, hash) {
|
||||
return fmt.Errorf("bcrypt.Match() failed: %w", err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(passwdFName) // nolint: gosec
|
||||
if err != nil {
|
||||
return fmt.Errorf("ioutil.ReadFile(): %w", err)
|
||||
}
|
||||
r := csv.NewReader(bytes.NewReader(b))
|
||||
|
||||
r.Comma = ':'
|
||||
r.Comment = '#'
|
||||
r.FieldsPerRecord = 3 // username:salt:authCookie]
|
||||
|
||||
records, err := r.ReadAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("r.ReadAll(): %w", err)
|
||||
}
|
||||
|
||||
recFound := false
|
||||
for i := range records {
|
||||
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 {
|
||||
return fmt.Errorf("ioutil.TempFile(): %w", 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 {
|
||||
return fmt.Errorf("w.Write(): %w", err)
|
||||
}
|
||||
err = w.WriteAll(records)
|
||||
if err != nil {
|
||||
return fmt.Errorf("w.WriteAll: %w", err)
|
||||
}
|
||||
err = os.Remove(passwdFName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Remove(): %w", err)
|
||||
}
|
||||
err = os.Rename(outFile.Name(), passwdFName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Rename(): %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// --------- End Local passwd auth routine(s) --------------
|
80
lpasswd/lpasswd.go
Executable file
80
lpasswd/lpasswd.go
Executable file
|
@ -0,0 +1,80 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"blitter.com/go/lpasswd/internal/termmode"
|
||||
"blitter.com/go/lpasswd"
|
||||
)
|
||||
|
||||
var (
|
||||
version string = "0.1"
|
||||
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", "local.passwd", "passwd file")
|
||||
flag.Parse()
|
||||
|
||||
if vopt {
|
||||
if gitCommit != "" {
|
||||
gitCommit = fmt.Sprintf(" (%s)", gitCommit)
|
||||
}
|
||||
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 := termmode.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 = termmode.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)
|
||||
}
|
||||
|
||||
err = lpasswd.SetPasswd(uname, newpw, pfName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue