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  }