Mercurial > go > multipass
comparison 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 |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:c18bc7b9d1d9 |
|---|---|
| 1 // Package file handles I/O for single password files. | |
| 2 // | |
| 3 // A password file contains multiple passwords for a single user. | |
| 4 // It starts with a banner that indicates the version of the file, | |
| 5 // then has entries in the format specified by auth.Entry. | |
| 6 | |
| 7 package file | |
| 8 | |
| 9 import ( | |
| 10 "bufio" | |
| 11 "errors" | |
| 12 "os" | |
| 13 "os/user" | |
| 14 "syscall" | |
| 15 | |
| 16 "path" | |
| 17 | |
| 18 "golang.org/x/sys/unix" | |
| 19 "pfish.zone/go/multipass/auth" | |
| 20 ) | |
| 21 | |
| 22 const ( | |
| 23 // the Banner acts as a version indicator | |
| 24 Banner = "# Multipass v0.1 password file" | |
| 25 ) | |
| 26 | |
| 27 var ( | |
| 28 // Raised when | |
| 29 ErrorBadFile = errors.New("multipass/file: Invalid file format") | |
| 30 ) | |
| 31 | |
| 32 type ShadowFile struct { | |
| 33 name string | |
| 34 } | |
| 35 | |
| 36 // ForUser gets the given user's ShadowFile. | |
| 37 func ForUser(username string) (*ShadowFile, error) { | |
| 38 u, err := user.Lookup(username) | |
| 39 if err != nil { | |
| 40 return nil, err | |
| 41 } | |
| 42 return New(path.Join(u.HomeDir, MultipassFile)), nil | |
| 43 } | |
| 44 | |
| 45 // ForMe gets the current user's ShadowFile. | |
| 46 func ForMe() (*ShadowFile, error) { | |
| 47 u, err := user.Current() | |
| 48 if err != nil { | |
| 49 return nil, err | |
| 50 } | |
| 51 return New(path.Join(u.HomeDir, MultipassFile)), nil | |
| 52 } | |
| 53 | |
| 54 // New creates a ShadowFile for reading at the given path. | |
| 55 // If a file needs to be created, uses the given GID to create it. | |
| 56 func New(name string) *ShadowFile { | |
| 57 f := new(ShadowFile) | |
| 58 f.name = name | |
| 59 return f | |
| 60 } | |
| 61 | |
| 62 func (f *ShadowFile) newFilename() string { | |
| 63 return f.name + ".new" | |
| 64 } | |
| 65 | |
| 66 func (f *ShadowFile) open() (*os.File, *bufio.Scanner, error) { | |
| 67 file, err := os.Open(f.name) | |
| 68 if err != nil { | |
| 69 return nil, nil, err | |
| 70 } | |
| 71 scanner := bufio.NewScanner(file) | |
| 72 if !scanner.Scan() { | |
| 73 file.Close() | |
| 74 return nil, nil, err | |
| 75 } | |
| 76 if scanner.Text() != Banner { | |
| 77 file.Close() | |
| 78 return nil, nil, ErrorBadFile | |
| 79 } | |
| 80 return file, scanner, nil | |
| 81 } | |
| 82 | |
| 83 func (f *ShadowFile) Authenticate(password string) (bool, error) { | |
| 84 file, scanner, err := f.open() | |
| 85 if err != nil { | |
| 86 return false, err | |
| 87 } | |
| 88 defer file.Close() | |
| 89 | |
| 90 for scanner.Scan() { | |
| 91 entry, err := auth.EntryFromShadow(scanner.Text()) | |
| 92 // Skip invalid lines. | |
| 93 if err != nil { | |
| 94 continue | |
| 95 } | |
| 96 if entry.Authenticate(password) { | |
| 97 return true, nil | |
| 98 } | |
| 99 } | |
| 100 return false, nil | |
| 101 } | |
| 102 | |
| 103 func (f *ShadowFile) Add(entry *auth.Entry) error { | |
| 104 handle, err := f.openWrite() | |
| 105 if err != nil { | |
| 106 return err | |
| 107 } | |
| 108 err = handle.write(entry) | |
| 109 if err != nil { | |
| 110 handle.bail() | |
| 111 return err | |
| 112 } | |
| 113 for handle.next() { | |
| 114 if entry, err := handle.entry(); err == nil { | |
| 115 // If we get an invalid entry, just skip it. | |
| 116 if err := handle.write(entry); err != nil { | |
| 117 handle.bail() | |
| 118 return err | |
| 119 } | |
| 120 } | |
| 121 } | |
| 122 return handle.finalize() | |
| 123 } | |
| 124 | |
| 125 func (f *ShadowFile) openWrite() (*writeHandle, error) { | |
| 126 return openWriteHandle(f.newFilename(), f.name) | |
| 127 } | |
| 128 | |
| 129 type writeHandle struct { | |
| 130 // We write to a file handle pointing to tempName. | |
| 131 tempName string | |
| 132 tempFile *os.File | |
| 133 writer *bufio.Writer | |
| 134 | |
| 135 // It will eventually move to fileName, which is our source file. | |
| 136 fileName string | |
| 137 // If inFile is nil, we're creating a new multipass file. | |
| 138 inFile *os.File | |
| 139 scanner *bufio.Scanner | |
| 140 } | |
| 141 | |
| 142 func openWriteHandle(tempName, fileName string) (*writeHandle, error) { | |
| 143 h := new(writeHandle) | |
| 144 h.tempName = tempName | |
| 145 h.fileName = fileName | |
| 146 // Open the output file, readable only by the current user. | |
| 147 oldUmask := unix.Umask(077) | |
| 148 tempFile, err := os.Create(tempName) | |
| 149 unix.Umask(oldUmask) | |
| 150 if err != nil { | |
| 151 return nil, err | |
| 152 } | |
| 153 h.tempFile = tempFile | |
| 154 // Open the input file. | |
| 155 inFile, err := os.Open(fileName) | |
| 156 if err == nil { | |
| 157 // Prepare to read in the input file, if it exists. | |
| 158 h.inFile = inFile | |
| 159 h.scanner = bufio.NewScanner(h.inFile) | |
| 160 if !h.scanner.Scan() { | |
| 161 return nil, ErrorBadFile | |
| 162 } | |
| 163 if h.scanner.Text() != Banner { | |
| 164 return nil, ErrorBadFile | |
| 165 } | |
| 166 // Change the owner and group of the new file to that of the old. | |
| 167 inStat, err := h.inFile.Stat() | |
| 168 if err != nil { | |
| 169 h.bail() | |
| 170 return nil, err | |
| 171 } | |
| 172 inStatUnix := inStat.Sys().(*syscall.Stat_t) | |
| 173 if err := h.tempFile.Chown(int(inStatUnix.Uid), int(inStatUnix.Gid)); err != nil { | |
| 174 h.bail() | |
| 175 return nil, err | |
| 176 } | |
| 177 if err := h.tempFile.Chmod(inStat.Mode()); err != nil { | |
| 178 h.bail() | |
| 179 return nil, err | |
| 180 } | |
| 181 } | |
| 182 // TODO(pfish): If there is no input file, set the right permissions + group on the output file. | |
| 183 h.writer = bufio.NewWriter(h.tempFile) | |
| 184 if _, err := h.writer.WriteString(Banner + "\n"); err != nil { | |
| 185 return nil, err | |
| 186 } | |
| 187 return h, nil | |
| 188 } | |
| 189 | |
| 190 func (h *writeHandle) bail() { | |
| 191 h.tempFile.Close() | |
| 192 h.inFile.Close() | |
| 193 os.Remove(h.tempName) | |
| 194 } | |
| 195 | |
| 196 func (h *writeHandle) next() bool { | |
| 197 // If the scanner is nil, then we have no input file and therefore no next. | |
| 198 return h.scanner != nil && h.scanner.Scan() | |
| 199 } | |
| 200 | |
| 201 func (h *writeHandle) entry() (*auth.Entry, error) { | |
| 202 return auth.EntryFromShadow(h.scanner.Text()) | |
| 203 } | |
| 204 | |
| 205 func (h *writeHandle) write(entry *auth.Entry) error { | |
| 206 if _, err := h.writer.WriteString(entry.Encode()); err != nil { | |
| 207 return err | |
| 208 } | |
| 209 _, err := h.writer.WriteString("\n") | |
| 210 return err | |
| 211 } | |
| 212 | |
| 213 func (h *writeHandle) finalize() error { | |
| 214 h.inFile.Close() | |
| 215 // Make absolutely completely sure we're synced. | |
| 216 if err := h.writer.Flush(); err != nil { | |
| 217 h.tempFile.Close() | |
| 218 os.Remove(h.tempName) | |
| 219 return err | |
| 220 } | |
| 221 if err := h.tempFile.Sync(); err != nil { | |
| 222 h.tempFile.Close() | |
| 223 os.Remove(h.tempName) | |
| 224 return err | |
| 225 } | |
| 226 // Close the file. | |
| 227 if err := h.tempFile.Close(); err != nil { | |
| 228 os.Remove(h.tempName) | |
| 229 return err | |
| 230 } | |
| 231 // And atomically write it over the new one. | |
| 232 err := os.Rename(h.tempName, h.fileName) | |
| 233 if err != nil { | |
| 234 return err | |
| 235 } | |
| 236 // If we get here, everything succeeded, and only in this case | |
| 237 // will the multipass shadow file have been updated. | |
| 238 return nil | |
| 239 } |
