From 13c0d03f324c61e12708ee7184cc37554edc92f7 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Fri, 17 Jun 2016 16:59:45 -0400 Subject: password tracking and changing in etc --- Makefile | 19 +++++- src/parabola_hackers/.gitignore | 1 + src/parabola_hackers/nslcd_backend/db_pam.go | 68 +++++++++++++++++++ src/parabola_hackers/nslcd_backend/hackers.go | 13 ++-- src/parabola_hackers/password.go | 64 ------------------ src/parabola_hackers/passwords.go.in | 94 +++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 69 deletions(-) delete mode 100644 src/parabola_hackers/password.go create mode 100644 src/parabola_hackers/passwords.go.in diff --git a/Makefile b/Makefile index 3540d90..8eaf4f3 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ PACKAGE = parabola-hackers sysusersdir=$(prefix)/lib/sysusers.d systemunitdir=$(prefix)/lib/systemd/system conf_file = $(sysconfdir)/$(PACKAGE).yml +shadow_file = $(sysconfdir)/nshd/shadow NET ?= #NET ?= FORCE user = nshd @@ -43,7 +44,7 @@ scripts = $(filter-out common.rb common.rb.in,$(notdir $(wildcard $(srcdir)/scri std.gen_files += LICENSE.lgpl-2.1.txt LICENSE.gpl-2.txt LICENSE.apache-2.0.txt std.out_files += bin/cmd-nshd nshd.service nshd.sysusers scripts/common.rb test/runner -std.sys_files += $(addprefix $(bindir)/,nshd $(scripts)) $(systemunitdir)/nshd.socket $(systemunitdir)/nshd.service $(sysusersdir)/nshd.conf $(conf_file) +std.sys_files += $(addprefix $(bindir)/,nshd $(scripts)) $(systemunitdir)/nshd.socket $(systemunitdir)/nshd.service $(sysusersdir)/nshd.conf $(conf_file) $(shadow_file) std.clean_files += test/*.o pkg/ .tmp* .var* $(_out) $(srcdir)/LICENSE.lgpl-2.1.txt: $(NET) @@ -59,6 +60,7 @@ _gen += src/lukeshu.com/git/go/libnslcd.git/proto/server/interface_backend.go _gen += src/lukeshu.com/git/go/libnslcd.git/proto/server/func_handlerequest.go _gen += src/lukeshu.com/git/go/libnslcd.git/proto/server/type_nilbackend.go _out += src/parabola_hackers/users.go +_out += src/parabola_hackers/passwords.go _out += src/cmd-nshd/main.go $(outdir)/bin/%-nshd: $(call golang.src,$(srcdir)) $(_gen) $(_out) $(call golang.install,$(topsrcdir),cmd-nshd) @@ -76,21 +78,36 @@ $(outdir)/nshd.sysusers: $(var)user $(outdir)/scripts/common.rb: $(var)conf_file $(outdir)/src/cmd-nshd/main.go: $(var)conf_file $(outdir)/src/parabola_hackers/users.go: $(var)bindir +$(outdir)/src/parabola_hackers/passwords.go: $(var)shadow_file $(DESTDIR)$(bindir)/%: $(outdir)/bin/cmd-% + $(NORMAL_INSTALL) install -TDm755 $< $@ $(DESTDIR)$(bindir)/%: $(srcdir)/scripts/% + $(NORMAL_INSTALL) install -TDm755 $< $@ $(DESTDIR)$(bindir)/common.rb: $(srcdir)/scripts/common.rb + $(NORMAL_INSTALL) install -TDm644 $< $@ $(DESTDIR)$(systemunitdir)/%.socket: $(outdir)/%.socket + $(NORMAL_INSTALL) install -TDm644 $< $@ $(DESTDIR)$(systemunitdir)/%.service: $(outdir)/%.service + $(NORMAL_INSTALL) install -TDm644 $< $@ $(DESTDIR)$(sysusersdir)/%.conf: $(outdir)/%.sysusers + $(NORMAL_INSTALL) install -TDm644 $< $@ $(DESTDIR)$(conf_file): $(srcdir)/parabola-hackers.yml + $(NORMAL_INSTALL) install -TDm644 $< $@ +$(DESTDIR)$(shadow_file): $(var)user $(DESTDIR)$(sysusersdir)/nshd.conf + $(NORMAL_INSTALL) + install -d $(@D) + touch $@ + $(POST_INSTALL) + -systemd-sysusers + -chown $(user):$(user) $(@D) $@ .PHONY: FORCE .SECONDARY: diff --git a/src/parabola_hackers/.gitignore b/src/parabola_hackers/.gitignore index e93884b..3be3f08 100644 --- a/src/parabola_hackers/.gitignore +++ b/src/parabola_hackers/.gitignore @@ -1 +1,2 @@ /users.go +/passwords.go diff --git a/src/parabola_hackers/nslcd_backend/db_pam.go b/src/parabola_hackers/nslcd_backend/db_pam.go index 303a66c..3374170 100644 --- a/src/parabola_hackers/nslcd_backend/db_pam.go +++ b/src/parabola_hackers/nslcd_backend/db_pam.go @@ -17,17 +17,32 @@ package hackers_nslcd_backend import ( + "fmt" "parabola_hackers" s "syscall" "lukeshu.com/git/go/libgnulinux.git/crypt" p "lukeshu.com/git/go/libnslcd.git/proto" + "lukeshu.com/git/go/libsystemd.git/sd_daemon/logger" ) func checkPassword(password string, hash string) bool { return crypt.Crypt(password, hash) == hash } +func hashPassword(newPassword string, oldHash string) string { + salt := oldHash + if salt == "!" { + str, err := parabola_hackers.RandomString(crypt.SaltAlphabet, 8) + if err != nil { + logger.Err("Could not generate a random string") + str = "" + } + salt = "$6$" + str + "$" + } + return crypt.Crypt(newPassword, salt) +} + func (o *Hackers) PAM_Authentication(cred s.Ucred, req p.Request_PAM_Authentication) <-chan p.PAM_Authentication { o.lock.RLock() ret := make(chan p.PAM_Authentication) @@ -97,3 +112,56 @@ func (o *Hackers) PAM_SessionClose(cred s.Ucred, req p.Request_PAM_SessionClose) go close(ret) return ret } + +func (o *Hackers) PAM_PwMod(cred s.Ucred, req p.Request_PAM_PwMod) <-chan p.PAM_PwMod { + ret := make(chan p.PAM_PwMod) + o.lock.Lock() + go func() { + defer close(ret) + defer o.lock.Unlock() + + uid := o.name2uid(req.UserName) + if uid < 0 { + return + } + user := o.users[uid] + + // Check the OldPassword + if req.AsRoot == 1 { + if !checkPassword(req.OldPassword, user.Passwd.PwHash) { + ret <- p.PAM_PwMod{ + Result: p.NSLCD_PAM_PERM_DENIED, + Error: fmt.Sprintf("password change failed: %s", "Old password did not match"), + } + return + } + } + + // Update the PwHash in memory + user.Passwd.PwHash = hashPassword(req.NewPassword, user.Passwd.PwHash) + if user.Passwd.PwHash == "" { + logger.Err("Password hashing failed") + return + } + + // Update the PwHash on disk + passwords := make(map[string]string, len(o.users)) + for _, ouser := range o.users { + passwords[ouser.Passwd.Name] = ouser.Passwd.PwHash + } + passwords[user.Passwd.Name] = user.Passwd.PwHash + err := parabola_hackers.SaveAllPasswords(passwords) + if err != nil { + logger.Err("Writing passwords to disk: %v", err) + return + } + + // Ok, we're done, commit the changes + o.users[uid] = user + ret <- p.PAM_PwMod{ + Result: p.NSLCD_PAM_SUCCESS, + Error: "", + } + }() + return ret +} diff --git a/src/parabola_hackers/nslcd_backend/hackers.go b/src/parabola_hackers/nslcd_backend/hackers.go index f7d56e3..bb03862 100644 --- a/src/parabola_hackers/nslcd_backend/hackers.go +++ b/src/parabola_hackers/nslcd_backend/hackers.go @@ -82,14 +82,19 @@ func (o *Hackers) Reload() error { return err } + passwords, err := parabola_hackers.LoadAllPasswords() + if err != nil { + return err + } + o.groups = make(map[string]map[string]bool) for uid, user := range o.users { user.Passwd.GID = usersGid - var _err error - user.Passwd.PwHash, _err = parabola_hackers.LoadUserPassword(user.Passwd.HomeDir + "/.password") - if _err != nil { - logger.Debug("hackers.git: Ignoring password: %v", _err) + hash, hasHash := passwords[user.Passwd.Name] + if !hasHash { + hash = "!" } + user.Passwd.PwHash = hash o.users[uid] = user for _, groupname := range user.Groups { o.add_user_to_group(user.Passwd.Name, groupname) diff --git a/src/parabola_hackers/password.go b/src/parabola_hackers/password.go deleted file mode 100644 index 957de1f..0000000 --- a/src/parabola_hackers/password.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2015-2016 Luke Shumaker . -// -// This is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This software is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public -// License along with this manual; if not, see -// . - -package parabola_hackers - -import ( - "fmt" - "io/ioutil" - "os" - "strings" - - "lukeshu.com/git/go/libgnulinux.git/crypt" -) - -/* Note that the password hash value should be one of: - - no password set, allow login without password - ! - used to prevent logins - x - "valid" encrypted password that does not match any valid password - often used to indicate that the password is defined elsewhere - other - encrypted password, in crypt(3) format */ - -func LoadUserPassword(filename string) (hash string, err error) { - file, err := os.Open(filename) - if err != nil { - return - } - contents, err := ioutil.ReadAll(file) - if err != nil { - return - } - lines := strings.Split(string(contents), "\n") - switch len(lines) { - case 1: - hash = lines[0] - case 2: - if lines[1] == "" { - hash = lines[0] - } else { - err = fmt.Errorf("Invalid password format in file: %q", filename) - } - default: - err = fmt.Errorf("Invalid password format in file (number of lines): %q", filename) - return - } - if hash != "!" && !crypt.SaltOk(hash) { - hash = "!" - err = fmt.Errorf("Invalid password format in file (invalid salt): %q", filename) - return - } - return -} diff --git a/src/parabola_hackers/passwords.go.in b/src/parabola_hackers/passwords.go.in new file mode 100644 index 0000000..0d763b9 --- /dev/null +++ b/src/parabola_hackers/passwords.go.in @@ -0,0 +1,94 @@ +// Copyright 2015-2016 Luke Shumaker . +// +// This is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as +// published by the Free Software Foundation; either version 2 of +// the License, or (at your option) any later version. +// +// This software is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public +// License along with this manual; if not, see +// . + +package parabola_hackers + +import ( + "fmt" + "io/ioutil" + "os" + "sort" + "strings" + + "lukeshu.com/git/go/libgnulinux.git/crypt" + "lukeshu.com/git/go/libsystemd.git/sd_daemon/logger" +) + +/* Note that the password hash value should be one of: + - no password set, allow login without password + ! - used to prevent logins + x - "valid" encrypted password that does not match any valid password + often used to indicate that the password is defined elsewhere + other - encrypted password, in crypt(3) format */ + +const shadow_file = "@shadow_file@" + +func LoadAllPasswords() (map[string]string, error) { + file, err := os.Open(shadow_file) + if err != nil { + return nil, err + } + contents, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + lines := strings.Split(string(contents), "\n") + passwords := make(map[string]string, len(lines)) + for i, line := range lines { + cols := strings.SplitN(line, ":", 2) + if len(cols) != 2 { + logger.Err("hackers.git %s:%d: malformed line", shadow_file, i+1) + continue + } + username := cols[0] + hash := cols[1] + if hash != "!" && !crypt.SaltOk(hash) { + hash = "!" + logger.Err("%s:%d: malformed hash for user: %s", shadow_file, i+1, username) + } + passwords[username] = hash + } + return passwords, nil +} + +func SaveAllPasswords(passwords map[string]string) error { + usernames := make([]string, len(passwords)) + i := 0 + for username, _ := range passwords { + usernames[i] = username + i++ + } + sort.Strings(usernames) + + file, err := os.OpenFile(shadow_file+"-", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + + for _, username := range usernames { + fmt.Fprintf(file, "%s:%s\n", username, passwords[username]) + } + err = file.Sync() + if err != nil { + return err + } + err = file.Close() + if err != nil { + return err + } + + return os.Rename(shadow_file+"-", shadow_file) +} -- cgit v1.2.2