gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/backup.go (about) 1 package renter 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "crypto/cipher" 8 "encoding/json" 9 "io" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "gitlab.com/NebulousLabs/errors" 16 "gitlab.com/NebulousLabs/fastrand" 17 "golang.org/x/crypto/twofish" 18 19 "gitlab.com/SiaPrime/SiaPrime/build" 20 "gitlab.com/SiaPrime/SiaPrime/crypto" 21 "gitlab.com/SiaPrime/SiaPrime/modules" 22 "gitlab.com/SiaPrime/SiaPrime/modules/renter/siadir" 23 "gitlab.com/SiaPrime/SiaPrime/modules/renter/siafile" 24 ) 25 26 // backupHeader defines the structure of the backup's JSON header. 27 type backupHeader struct { 28 Version string `json:"version"` 29 Encryption string `json:"encryption"` 30 IV []byte `json:"iv"` 31 } 32 33 // The following specifiers are options for the encryption of backups. 34 var ( 35 encryptionPlaintext = "plaintext" 36 encryptionTwofish = "twofish-ctr" 37 encryptionVersion = "1.0" 38 ) 39 40 // CreateBackup creates a backup of the renter's siafiles. If a secret is not 41 // nil, the backup will be encrypted using the provided secret. 42 func (r *Renter) CreateBackup(dst string, secret []byte) error { 43 if err := r.tg.Add(); err != nil { 44 return err 45 } 46 defer r.tg.Done() 47 return r.managedCreateBackup(dst, secret) 48 } 49 50 // managedCreateBackup creates a backup of the renter's siafiles. If a secret is 51 // not nil, the backup will be encrypted using the provided secret. 52 func (r *Renter) managedCreateBackup(dst string, secret []byte) error { 53 // Create the gzip file. 54 f, err := os.Create(dst) 55 if err != nil { 56 return err 57 } 58 defer f.Close() 59 archive := io.Writer(f) 60 61 // Prepare a header for the backup and default to no encryption. This will 62 // potentially be overwritten later. 63 bh := backupHeader{ 64 Version: encryptionVersion, 65 Encryption: encryptionPlaintext, 66 } 67 68 // Wrap it for encryption if required. 69 if secret != nil { 70 bh.Encryption = encryptionTwofish 71 bh.IV = fastrand.Bytes(twofish.BlockSize) 72 c, err := twofish.NewCipher(secret) 73 if err != nil { 74 return err 75 } 76 sw := cipher.StreamWriter{ 77 S: cipher.NewCTR(c, bh.IV), 78 W: archive, 79 } 80 archive = sw 81 } 82 83 // Skip the checkum for now. 84 if _, err := f.Seek(crypto.HashSize, io.SeekStart); err != nil { 85 return err 86 } 87 // Write the header. 88 enc := json.NewEncoder(f) 89 if err := enc.Encode(bh); err != nil { 90 return err 91 } 92 // Wrap the archive in a multiwriter to hash the contents of the archive 93 // before encrypting it. 94 h := crypto.NewHash() 95 archive = io.MultiWriter(archive, h) 96 // Wrap the potentially encrypted writer into a gzip writer. 97 gzw := gzip.NewWriter(archive) 98 // Wrap the gzip writer into a tar writer. 99 tw := tar.NewWriter(gzw) 100 // Add the files to the archive. 101 if err := r.managedTarSiaFiles(tw); err != nil { 102 twErr := tw.Close() 103 gzwErr := gzw.Close() 104 return errors.Compose(err, twErr, gzwErr) 105 } 106 // Close writers to flush them before computing the hash. 107 twErr := tw.Close() 108 gzwErr := gzw.Close() 109 // Write the hash to the beginning of the file. 110 _, err = f.WriteAt(h.Sum(nil), 0) 111 return errors.Compose(err, twErr, gzwErr) 112 } 113 114 // LoadBackup loads the siafiles of a previously created backup into the 115 // renter. If the backup is encrypted, secret will be used to decrypt it. 116 // Otherwise the argument is ignored. 117 func (r *Renter) LoadBackup(src string, secret []byte) error { 118 if err := r.tg.Add(); err != nil { 119 return err 120 } 121 defer r.tg.Done() 122 123 // Only load a backup if there are no siafiles yet. 124 root, err := r.staticDirSet.Open(modules.RootSiaPath()) 125 if err != nil { 126 return err 127 } 128 defer root.Close() 129 130 // Open the gzip file. 131 f, err := os.Open(src) 132 if err != nil { 133 return err 134 } 135 defer f.Close() 136 archive := io.Reader(f) 137 138 // Read the checksum. 139 var chks crypto.Hash 140 _, err = io.ReadFull(f, chks[:]) 141 if err != nil { 142 return err 143 } 144 // Read the header. 145 dec := json.NewDecoder(archive) 146 var bh backupHeader 147 if err := dec.Decode(&bh); err != nil { 148 return err 149 } 150 // Seek back by the amount of data left in the decoder's buffer. That gives 151 // us the offset of the body. 152 var off int64 153 if buf, ok := dec.Buffered().(*bytes.Reader); ok { 154 off, err = f.Seek(int64(1-buf.Len()), io.SeekCurrent) 155 if err != nil { 156 return err 157 } 158 } else { 159 build.Critical("Buffered should return a bytes.Reader") 160 } 161 // Check the version number. 162 if bh.Version != encryptionVersion { 163 return errors.New("unknown version") 164 } 165 // Wrap the file in the correct streamcipher. 166 archive, err = wrapReaderInCipher(f, bh, secret) 167 if err != nil { 168 return err 169 } 170 // Pipe the remaining file into the hasher to verify that the hash is 171 // correct. 172 h := crypto.NewHash() 173 _, err = io.Copy(h, archive) 174 if err != nil { 175 return err 176 } 177 // Verify the hash. 178 if !bytes.Equal(h.Sum(nil), chks[:]) { 179 return errors.New("checksum doesn't match") 180 } 181 // Seek back to the beginning of the body. 182 if _, err := f.Seek(off, io.SeekStart); err != nil { 183 return err 184 } 185 // Wrap the file again. 186 archive, err = wrapReaderInCipher(f, bh, secret) 187 if err != nil { 188 return err 189 } 190 // Wrap the potentially encrypted reader in a gzip reader. 191 gzr, err := gzip.NewReader(archive) 192 if err != nil { 193 return err 194 } 195 defer gzr.Close() 196 // Wrap the gzip reader in a tar reader. 197 tr := tar.NewReader(gzr) 198 // Untar the files. 199 return r.managedUntarDir(tr) 200 } 201 202 // managedTarSiaFiles creates a tarball from the renter's siafiles and writes 203 // it to dst. 204 func (r *Renter) managedTarSiaFiles(tw *tar.Writer) error { 205 // Walk over all the siafiles and add them to the tarball. 206 return filepath.Walk(r.staticFilesDir, func(path string, info os.FileInfo, err error) error { 207 // This error is non-nil if filepath.Walk couldn't stat a file or 208 // folder. 209 if err != nil { 210 return err 211 } 212 // Nothing to do for non-folders and non-siafiles. 213 if !info.IsDir() && filepath.Ext(path) != modules.SiaFileExtension && 214 filepath.Ext(path) != modules.SiaDirExtension { 215 return nil 216 } 217 // Create the header for the file/dir. 218 header, err := tar.FileInfoHeader(info, info.Name()) 219 if err != nil { 220 return err 221 } 222 relPath := strings.TrimPrefix(path, r.staticFilesDir) 223 header.Name = relPath 224 // If the info is a dir there is nothing more to do besides writing the 225 // header. 226 if info.IsDir() { 227 return tw.WriteHeader(header) 228 } 229 // Handle siafiles and siadirs differently. 230 var file io.Reader 231 if filepath.Ext(path) == modules.SiaFileExtension { 232 // Get the siafile. 233 siaPath, err := modules.NewSiaPath(strings.TrimSuffix(relPath, modules.SiaFileExtension)) 234 if err != nil { 235 return err 236 } 237 entry, err := r.staticFileSet.Open(siaPath) 238 if err != nil { 239 return err 240 } 241 defer entry.Close() 242 // Get a reader to read from the siafile. 243 sr, err := entry.SnapshotReader() 244 if err != nil { 245 return err 246 } 247 defer sr.Close() 248 file = sr 249 // Update the size of the file within the header since it might have changed 250 // while we weren't holding the lock. 251 fi, err := sr.Stat() 252 if err != nil { 253 return err 254 } 255 header.Size = fi.Size() 256 } else if filepath.Ext(path) == modules.SiaDirExtension { 257 // Get the siadir. 258 var siaPath modules.SiaPath 259 siaPathStr := strings.TrimSuffix(relPath, modules.SiaDirExtension) 260 if siaPathStr == string(filepath.Separator) { 261 siaPath = modules.RootSiaPath() 262 } else { 263 siaPath, err = modules.NewSiaPath(siaPathStr) 264 if err != nil { 265 return err 266 } 267 } 268 entry, err := r.staticDirSet.Open(siaPath) 269 if err != nil { 270 return err 271 } 272 defer entry.Close() 273 // Get a reader to read from the siafile. 274 dr, err := entry.DirReader() 275 if err != nil { 276 return err 277 } 278 defer dr.Close() 279 file = dr 280 // Update the size of the file within the header since it might have changed 281 // while we weren't holding the lock. 282 fi, err := dr.Stat() 283 if err != nil { 284 return err 285 } 286 header.Size = fi.Size() 287 } 288 // Write the header. 289 if err := tw.WriteHeader(header); err != nil { 290 return err 291 } 292 // Add the file to the archive. 293 _, err = io.Copy(tw, file) 294 return err 295 }) 296 } 297 298 // managedUntarDir untars the archive from src and writes the contents to dstFolder 299 // while preserving the relative paths within the archive. 300 func (r *Renter) managedUntarDir(tr *tar.Reader) error { 301 // Copy the files from the tarball to the new location. 302 for { 303 header, err := tr.Next() 304 if err == io.EOF { 305 break 306 } else if err != nil { 307 return err 308 } 309 dst := filepath.Join(r.staticFilesDir, header.Name) 310 311 // Check for dir. 312 info := header.FileInfo() 313 if info.IsDir() { 314 if err = os.MkdirAll(dst, info.Mode()); err != nil { 315 return err 316 } 317 continue 318 } 319 // Load the new file in memory. 320 b, err := ioutil.ReadAll(tr) 321 if err != nil { 322 return err 323 } 324 if name := filepath.Base(info.Name()); name == modules.SiaDirExtension { 325 // Load the file as a .siadir 326 var md siadir.Metadata 327 err = json.Unmarshal(b, &md) 328 if err != nil { 329 return err 330 } 331 // Try creating a new SiaDir. 332 var siaPath modules.SiaPath 333 if err := siaPath.LoadSysPath(r.staticFilesDir, dst); err != nil { 334 return err 335 } 336 siaPath, err = siaPath.Dir() 337 if err != nil { 338 return err 339 } 340 dirEntry, err := r.staticDirSet.NewSiaDir(siaPath) 341 if err == siadir.ErrPathOverload { 342 // .siadir exists already 343 continue 344 } else if err != nil { 345 return err // unexpected error 346 } 347 // Update the metadata. 348 if err := dirEntry.UpdateMetadata(md); err != nil { 349 dirEntry.Close() 350 return err 351 } 352 if err := dirEntry.Close(); err != nil { 353 return err 354 } 355 } else if filepath.Ext(info.Name()) == modules.SiaFileExtension { 356 // Load the file as a SiaFile. 357 reader := bytes.NewReader(b) 358 sf, chunks, err := siafile.LoadSiaFileFromReaderWithChunks(reader, dst, r.wal) 359 if err != nil { 360 return err 361 } 362 // Add the file to the SiaFileSet. 363 err = r.staticFileSet.AddExistingSiaFile(sf, chunks) 364 if err != nil { 365 return err 366 } 367 } 368 } 369 return nil 370 } 371 372 // wrapReaderInCipher wraps the reader r into another reader according to the 373 // used encryption specified in the backupHeader. 374 func wrapReaderInCipher(r io.Reader, bh backupHeader, secret []byte) (io.Reader, error) { 375 // Check if encryption is required and wrap the archive into a cipher if 376 // necessary. 377 switch bh.Encryption { 378 case encryptionTwofish: 379 c, err := twofish.NewCipher(secret) 380 if err != nil { 381 return nil, err 382 } 383 return cipher.StreamReader{ 384 S: cipher.NewCTR(c, bh.IV), 385 R: r, 386 }, nil 387 case encryptionPlaintext: 388 return r, nil 389 default: 390 return nil, errors.New("unknown cipher") 391 } 392 }