Mercurial > go > multipass
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 +}