diff file/file.go @ 0:c18bc7b9d1d9

Basic binaries. checkpassword doesn't yet work.
author Paul Fisher <paul@pfish.zone>
date Sat, 24 Oct 2015 21:32:03 -0400
parents
children e58bfc7fc207
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/file/file.go	Sat Oct 24 21:32:03 2015 -0400
@@ -0,0 +1,239 @@
+// Package file handles I/O for single password files.
+//
+// A password file contains multiple passwords for a single user.
+// It starts with a banner that indicates the version of the file,
+// then has entries in the format specified by auth.Entry.
+
+package file
+
+import (
+	"bufio"
+	"errors"
+	"os"
+	"os/user"
+	"syscall"
+
+	"path"
+
+	"golang.org/x/sys/unix"
+	"pfish.zone/go/multipass/auth"
+)
+
+const (
+	// the Banner acts as a version indicator
+	Banner = "# Multipass v0.1 password file"
+)
+
+var (
+	// Raised when
+	ErrorBadFile = errors.New("multipass/file: Invalid file format")
+)
+
+type ShadowFile struct {
+	name string
+}
+
+// ForUser gets the given user's ShadowFile.
+func ForUser(username string) (*ShadowFile, error) {
+	u, err := user.Lookup(username)
+	if err != nil {
+		return nil, err
+	}
+	return New(path.Join(u.HomeDir, MultipassFile)), nil
+}
+
+// ForMe gets the current user's ShadowFile.
+func ForMe() (*ShadowFile, error) {
+	u, err := user.Current()
+	if err != nil {
+		return nil, err
+	}
+	return New(path.Join(u.HomeDir, MultipassFile)), nil
+}
+
+// New creates a ShadowFile for reading at the given path.
+// If a file needs to be created, uses the given GID to create it.
+func New(name string) *ShadowFile {
+	f := new(ShadowFile)
+	f.name = name
+	return f
+}
+
+func (f *ShadowFile) newFilename() string {
+	return f.name + ".new"
+}
+
+func (f *ShadowFile) open() (*os.File, *bufio.Scanner, error) {
+	file, err := os.Open(f.name)
+	if err != nil {
+		return nil, nil, err
+	}
+	scanner := bufio.NewScanner(file)
+	if !scanner.Scan() {
+		file.Close()
+		return nil, nil, err
+	}
+	if scanner.Text() != Banner {
+		file.Close()
+		return nil, nil, ErrorBadFile
+	}
+	return file, scanner, nil
+}
+
+func (f *ShadowFile) Authenticate(password string) (bool, error) {
+	file, scanner, err := f.open()
+	if err != nil {
+		return false, err
+	}
+	defer file.Close()
+
+	for scanner.Scan() {
+		entry, err := auth.EntryFromShadow(scanner.Text())
+		// Skip invalid lines.
+		if err != nil {
+			continue
+		}
+		if entry.Authenticate(password) {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+func (f *ShadowFile) Add(entry *auth.Entry) error {
+	handle, err := f.openWrite()
+	if err != nil {
+		return err
+	}
+	err = handle.write(entry)
+	if err != nil {
+		handle.bail()
+		return err
+	}
+	for handle.next() {
+		if entry, err := handle.entry(); err == nil {
+			// If we get an invalid entry, just skip it.
+			if err := handle.write(entry); err != nil {
+				handle.bail()
+				return err
+			}
+		}
+	}
+	return handle.finalize()
+}
+
+func (f *ShadowFile) openWrite() (*writeHandle, error) {
+	return openWriteHandle(f.newFilename(), f.name)
+}
+
+type writeHandle struct {
+	// We write to a file handle pointing to tempName.
+	tempName string
+	tempFile *os.File
+	writer   *bufio.Writer
+
+	// It will eventually move to fileName, which is our source file.
+	fileName string
+	// If inFile is nil, we're creating a new multipass file.
+	inFile  *os.File
+	scanner *bufio.Scanner
+}
+
+func openWriteHandle(tempName, fileName string) (*writeHandle, error) {
+	h := new(writeHandle)
+	h.tempName = tempName
+	h.fileName = fileName
+	// Open the output file, readable only by the current user.
+	oldUmask := unix.Umask(077)
+	tempFile, err := os.Create(tempName)
+	unix.Umask(oldUmask)
+	if err != nil {
+		return nil, err
+	}
+	h.tempFile = tempFile
+	// Open the input file.
+	inFile, err := os.Open(fileName)
+	if err == nil {
+		// Prepare to read in the input file, if it exists.
+		h.inFile = inFile
+		h.scanner = bufio.NewScanner(h.inFile)
+		if !h.scanner.Scan() {
+			return nil, ErrorBadFile
+		}
+		if h.scanner.Text() != Banner {
+			return nil, ErrorBadFile
+		}
+		// Change the owner and group of the new file to that of the old.
+		inStat, err := h.inFile.Stat()
+		if err != nil {
+			h.bail()
+			return nil, err
+		}
+		inStatUnix := inStat.Sys().(*syscall.Stat_t)
+		if err := h.tempFile.Chown(int(inStatUnix.Uid), int(inStatUnix.Gid)); err != nil {
+			h.bail()
+			return nil, err
+		}
+		if err := h.tempFile.Chmod(inStat.Mode()); err != nil {
+			h.bail()
+			return nil, err
+		}
+	}
+	// TODO(pfish): If there is no input file, set the right permissions + group on the output file.
+	h.writer = bufio.NewWriter(h.tempFile)
+	if _, err := h.writer.WriteString(Banner + "\n"); err != nil {
+		return nil, err
+	}
+	return h, nil
+}
+
+func (h *writeHandle) bail() {
+	h.tempFile.Close()
+	h.inFile.Close()
+	os.Remove(h.tempName)
+}
+
+func (h *writeHandle) next() bool {
+	// If the scanner is nil, then we have no input file and therefore no next.
+	return h.scanner != nil && h.scanner.Scan()
+}
+
+func (h *writeHandle) entry() (*auth.Entry, error) {
+	return auth.EntryFromShadow(h.scanner.Text())
+}
+
+func (h *writeHandle) write(entry *auth.Entry) error {
+	if _, err := h.writer.WriteString(entry.Encode()); err != nil {
+		return err
+	}
+	_, err := h.writer.WriteString("\n")
+	return err
+}
+
+func (h *writeHandle) finalize() error {
+	h.inFile.Close()
+	// Make absolutely completely sure we're synced.
+	if err := h.writer.Flush(); err != nil {
+		h.tempFile.Close()
+		os.Remove(h.tempName)
+		return err
+	}
+	if err := h.tempFile.Sync(); err != nil {
+		h.tempFile.Close()
+		os.Remove(h.tempName)
+		return err
+	}
+	// Close the file.
+	if err := h.tempFile.Close(); err != nil {
+		os.Remove(h.tempName)
+		return err
+	}
+	// And atomically write it over the new one.
+	err := os.Rename(h.tempName, h.fileName)
+	if err != nil {
+		return err
+	}
+	// If we get here, everything succeeded, and only in this case
+	// will the multipass shadow file have been updated.
+	return nil
+}