github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/persist/json.go (about) 1 package persist 2 3 // NOTE: The safe json files include a checksum that is allowed to be manually 4 // overwritten by the user. This temporarily exposes the user to corruption, not 5 // just from a json file that has the wrong values, but if the disk fails right 6 // after the user has manually modified their json file, there are edge cases 7 // where because of the manual checksum, the saver will not be able to detect 8 // corruption. 9 10 import ( 11 "bytes" 12 "encoding/json" 13 "io/ioutil" 14 "os" 15 "strings" 16 17 "SiaPrime/build" 18 "SiaPrime/crypto" 19 20 "gitlab.com/NebulousLabs/errors" 21 ) 22 23 // verifyChecksum will disregard the metadata of the saved file, and just verify 24 // that the checksum matches the data below the checksum to be certain that the 25 // file is correct. 26 func verifyChecksum(filename string) bool { 27 // Open the file. 28 file, err := os.Open(filename) 29 if os.IsNotExist(err) { 30 // No file at all means that everything is okay. This is a condition we 31 // are going to hit the first time that we ever save a file. 32 return true 33 } 34 if err != nil { 35 // An error opening the file means that the checksum verification has 36 // failed, we don't have confidence that this a a good file. 37 return false 38 } 39 defer file.Close() 40 41 // Read the metadata from the file. This is not covered by the checksum but 42 // we have to read it anyway to get to the checksum. 43 var header, version string 44 dec := json.NewDecoder(file) 45 if err := dec.Decode(&header); err != nil { 46 return false 47 } 48 if err := dec.Decode(&version); err != nil { 49 return false 50 } 51 52 // Read everything else. 53 remainingBytes, err := ioutil.ReadAll(dec.Buffered()) 54 if err != nil { 55 return false 56 } 57 // The buffer may or may not have read the rest of the file, read the rest 58 // of the file to be certain. 59 remainingBytesExtra, err := ioutil.ReadAll(file) 60 if err != nil { 61 return false 62 } 63 remainingBytes = append(remainingBytes, remainingBytesExtra...) 64 65 // Determine whether the leading bytes contain a checksum. A proper checksum 66 // will be 67 bytes (quote, 64 byte checksum, quote, newline). A manual 67 // checksum will be the characters "manual\n" (9 characters). If neither 68 // decode correctly, it is assumed that there is no checksum at all. 69 var checksum crypto.Hash 70 if len(remainingBytes) >= 67 { 71 err = json.Unmarshal(remainingBytes[:67], &checksum) 72 if err == nil { 73 // The checksum was read successfully. Return 'true' if the checksum 74 // matches the remaining data, and false otherwise. 75 return checksum == crypto.HashBytes(remainingBytes[68:]) 76 } 77 } 78 79 // The checksum was not read correctly, check if the next few bytes are 80 // the "manual" checksum. 81 var manualChecksum string 82 if len(remainingBytes) >= 9 { 83 err = json.Unmarshal(remainingBytes[:9], &manualChecksum) 84 if err == nil && manualChecksum == "manual" { 85 return true 86 } 87 } 88 89 // The checksum could not be decoded. Older versions of the file did not 90 // have a checksum, but the remaining data would still need to be valid 91 // JSON. If we are this far, it means that either the file is corrupt, or it 92 // is an old file where all remaining bytes should be valid json. 93 return json.Valid(remainingBytes) 94 } 95 96 // readJSON will try to read a persisted json object from a file. 97 func readJSON(meta Metadata, object interface{}, filename string) error { 98 // Open the file. 99 file, err := os.Open(filename) 100 if os.IsNotExist(err) { 101 return err 102 } 103 if err != nil { 104 return build.ExtendErr("unable to open persisted json object file", err) 105 } 106 defer file.Close() 107 108 // Read the metadata from the file. 109 var header, version string 110 dec := json.NewDecoder(file) 111 if err := dec.Decode(&header); err != nil { 112 return build.ExtendErr("unable to read header from persisted json object file", err) 113 } 114 if header != meta.Header { 115 return ErrBadHeader 116 } 117 if err := dec.Decode(&version); err != nil { 118 return build.ExtendErr("unable to read version from persisted json object file", err) 119 } 120 if version != meta.Version { 121 return ErrBadVersion 122 } 123 124 // Read everything else. 125 remainingBytes, err := ioutil.ReadAll(dec.Buffered()) 126 if err != nil { 127 return build.ExtendErr("unable to read persisted json object data", err) 128 } 129 // The buffer may or may not have read the rest of the file, read the rest 130 // of the file to be certain. 131 remainingBytesExtra, err := ioutil.ReadAll(file) 132 if err != nil { 133 return build.ExtendErr("unable to read persisted json object data", err) 134 } 135 remainingBytes = append(remainingBytes, remainingBytesExtra...) 136 137 // Determine whether the leading bytes contain a checksum. A proper checksum 138 // will be 67 bytes (quote, 64 byte checksum, quote, newline). A manual 139 // checksum will be the characters "manual\n" (9 characters). If neither 140 // decode correctly, it is assumed that there is no checksum at all. 141 checkManual := len(remainingBytes) >= 9 142 if len(remainingBytes) >= 67 { 143 var checksum crypto.Hash 144 err = json.Unmarshal(remainingBytes[:67], &checksum) 145 checkManual = checkManual && err != nil 146 if err == nil && checksum != crypto.HashBytes(remainingBytes[68:]) { 147 return errors.New("loading a file with a bad checksum") 148 } else if err == nil { 149 remainingBytes = remainingBytes[68:] 150 } 151 } 152 153 // checkManual will be set to true so long as the remainingBytes is at least 154 // 9 bytes long, and also there was an error when parsing the checksum. The 155 // manual checksum is considered correct if the json unmarshalling parses 156 // correctly, and also the bytes match the string "manual". 157 if checkManual { 158 var manualChecksum string 159 err := json.Unmarshal(remainingBytes[:9], &manualChecksum) 160 if err == nil && manualChecksum != "manual" { 161 return errors.New("loading a file with a bad checksum") 162 } else if err == nil { 163 remainingBytes = remainingBytes[10:] 164 } 165 } 166 167 // Any valid checksum has been stripped off. If there was an invalid 168 // checksum, an error has been returned. There is also the case that no 169 // checksum was written at all, which is ignored as a case - it's needed to 170 // preserve compatibility with previous persist files. 171 172 // Parse the json object. 173 return json.Unmarshal(remainingBytes, &object) 174 } 175 176 // LoadJSON will load a persisted json object from disk. 177 func LoadJSON(meta Metadata, object interface{}, filename string) error { 178 // Verify that the filename does not have the persist temp suffix. 179 if strings.HasSuffix(filename, tempSuffix) { 180 return ErrBadFilenameSuffix 181 } 182 183 // Verify that no other thread is using this filename. 184 err := func() error { 185 activeFilesMu.Lock() 186 defer activeFilesMu.Unlock() 187 188 _, exists := activeFiles[filename] 189 if exists { 190 build.Critical(ErrFileInUse, filename) 191 return ErrFileInUse 192 } 193 activeFiles[filename] = struct{}{} 194 return nil 195 }() 196 if err != nil { 197 return err 198 } 199 // Release the lock at the end of the function. 200 defer func() { 201 activeFilesMu.Lock() 202 delete(activeFiles, filename) 203 activeFilesMu.Unlock() 204 }() 205 206 // Try opening the primary file. 207 err = readJSON(meta, object, filename) 208 if err == ErrBadHeader || err == ErrBadVersion || os.IsNotExist(err) { 209 return err 210 } 211 if err != nil { 212 // Try opening the temp file. 213 err := readJSON(meta, object, filename+tempSuffix) 214 if err != nil { 215 return build.ExtendErr("unable to read persisted json object from disk", err) 216 } 217 } 218 219 // Success. 220 return nil 221 } 222 223 // SaveJSON will save a json object to disk in a durable, atomic way. The 224 // resulting file will have a checksum of the data as the third line. If 225 // manually editing files, the checksum line can be replaced with the 8 226 // characters "manual". This will cause the reader to accept the checksum even 227 // though the file has been changed. 228 func SaveJSON(meta Metadata, object interface{}, filename string) error { 229 // Verify that the filename does not have the persist temp suffix. 230 if strings.HasSuffix(filename, tempSuffix) { 231 return ErrBadFilenameSuffix 232 } 233 234 // Verify that no other thread is using this filename. 235 err := func() error { 236 activeFilesMu.Lock() 237 defer activeFilesMu.Unlock() 238 239 _, exists := activeFiles[filename] 240 if exists { 241 build.Critical(ErrFileInUse, filename) 242 return ErrFileInUse 243 } 244 activeFiles[filename] = struct{}{} 245 return nil 246 }() 247 if err != nil { 248 return err 249 } 250 // Release the lock at the end of the function. 251 defer func() { 252 activeFilesMu.Lock() 253 delete(activeFiles, filename) 254 activeFilesMu.Unlock() 255 }() 256 257 // Write the metadata to the buffer. 258 buf := new(bytes.Buffer) 259 enc := json.NewEncoder(buf) 260 if err := enc.Encode(meta.Header); err != nil { 261 return build.ExtendErr("unable to encode metadata header", err) 262 } 263 if err := enc.Encode(meta.Version); err != nil { 264 return build.ExtendErr("unable to encode metadata version", err) 265 } 266 267 // Marshal the object into json and write the checksum + result to the 268 // buffer. 269 objBytes, err := json.MarshalIndent(object, "", "\t") 270 if err != nil { 271 return build.ExtendErr("unable to marshal the provided object", err) 272 } 273 checksum := crypto.HashBytes(objBytes) 274 if err := enc.Encode(checksum); err != nil { 275 return build.ExtendErr("unable to encode checksum", err) 276 } 277 buf.Write(objBytes) 278 data := buf.Bytes() 279 280 // Write out the data to the temp file, with a sync. 281 err = func() (err error) { 282 // Verify the checksum of the real file. If the real file does not have 283 // a valid checksum, we do not want to risk overwriting the temp file, 284 // which may be the only good version of the persistence remaining. 285 // We'll skip writing the temp file to make sure it stays intact, and go 286 // straight to over-writing the real file. 287 if !verifyChecksum(filename) { 288 return nil 289 } 290 291 file, err := os.OpenFile(filename+tempSuffix, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600) 292 if err != nil { 293 return build.ExtendErr("unable to open temp file", err) 294 } 295 defer func() { 296 err = build.ComposeErrors(err, file.Close()) 297 }() 298 299 // Write and sync. 300 _, err = file.Write(data) 301 if err != nil { 302 return build.ExtendErr("unable to write temp file", err) 303 } 304 err = file.Sync() 305 if err != nil { 306 return build.ExtendErr("unable to sync temp file", err) 307 } 308 return nil 309 }() 310 if err != nil { 311 return err 312 } 313 314 // Write out the data to the real file, with a sync. 315 err = func() (err error) { 316 file, err := os.OpenFile(filename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0600) 317 if err != nil { 318 return build.ExtendErr("unable to open file", err) 319 } 320 defer func() { 321 err = build.ComposeErrors(err, file.Close()) 322 }() 323 324 // Write and sync. 325 _, err = file.Write(data) 326 if err != nil { 327 return build.ExtendErr("unable to write file", err) 328 } 329 err = file.Sync() 330 if err != nil { 331 return build.ExtendErr("unable to sync temp file", err) 332 } 333 return nil 334 }() 335 if err != nil { 336 return err 337 } 338 339 // Success 340 return nil 341 }