github.com/creativeprojects/go-selfupdate@v1.2.0/update/apply.go (about)

     1  package update
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"crypto/x509"
     7  	"encoding/pem"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  )
    14  
    15  var (
    16  	openFile = os.OpenFile
    17  )
    18  
    19  // Apply performs an update of the current executable (or opts.TargetFile, if set) with the contents of the given io.Reader.
    20  //
    21  // Apply performs the following actions to ensure a safe cross-platform update:
    22  //
    23  // 1. If configured, computes the checksum of the new executable and verifies it matches.
    24  //
    25  // 2. If configured, verifies the signature with a public key.
    26  //
    27  // 3. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
    28  //
    29  // 4. Renames /path/to/target to /path/to/.target.old
    30  //
    31  // 5. Renames /path/to/.target.new to /path/to/target
    32  //
    33  // 6. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
    34  // the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
    35  //
    36  // 7. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
    37  // back to /path/to/target.
    38  //
    39  // If the roll back operation fails, the file system is left in an inconsistent state (between steps 5 and 6) where
    40  // there is no new executable file and the old executable file could not be be moved to its original location. In this
    41  // case you should notify the user of the bad news and ask them to recover manually. Applications can determine whether
    42  // the rollback failed by calling RollbackError, see the documentation on that function for additional detail.
    43  func Apply(update io.Reader, opts Options) error {
    44  	// validate
    45  	verify := false
    46  	switch {
    47  	case opts.Signature != nil && opts.PublicKey != nil:
    48  		// okay
    49  		verify = true
    50  	case opts.Signature != nil:
    51  		return errors.New("no public key to verify signature with")
    52  	case opts.PublicKey != nil:
    53  		return errors.New("no signature to verify with")
    54  	}
    55  
    56  	// set defaults
    57  	if opts.Hash == 0 {
    58  		opts.Hash = crypto.SHA256
    59  	}
    60  	if opts.Verifier == nil {
    61  		opts.Verifier = NewECDSAVerifier()
    62  	}
    63  	if opts.TargetMode == 0 {
    64  		opts.TargetMode = 0755
    65  	}
    66  
    67  	// get target path
    68  	var err error
    69  	opts.TargetPath, err = opts.getPath()
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	var newBytes []byte
    75  	if newBytes, err = io.ReadAll(update); err != nil {
    76  		return err
    77  	}
    78  
    79  	// verify checksum if requested
    80  	if opts.Checksum != nil {
    81  		if err = opts.verifyChecksum(newBytes); err != nil {
    82  			return err
    83  		}
    84  	}
    85  
    86  	if verify {
    87  		if err = opts.verifySignature(newBytes); err != nil {
    88  			return err
    89  		}
    90  	}
    91  
    92  	// get the directory the executable exists in
    93  	updateDir := filepath.Dir(opts.TargetPath)
    94  	filename := filepath.Base(opts.TargetPath)
    95  
    96  	// Copy the contents of newbinary to a new executable file
    97  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
    98  	fp, err := openFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.TargetMode)
    99  	if err != nil {
   100  		return err
   101  	}
   102  	defer fp.Close()
   103  
   104  	_, err = io.Copy(fp, bytes.NewReader(newBytes))
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	// if we don't call fp.Close(), windows won't let us move the new executable
   110  	// because the file will still be "in use"
   111  	fp.Close()
   112  
   113  	// this is where we'll move the executable to so that we can swap in the updated replacement
   114  	oldPath := opts.OldSavePath
   115  	removeOld := opts.OldSavePath == ""
   116  	if removeOld {
   117  		oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
   118  	}
   119  
   120  	// delete any existing old exec file - this is necessary on Windows for two reasons:
   121  	// 1. after a successful update, Windows can't remove the .old file because the process is still running
   122  	// 2. windows rename operations fail if the destination file already exists
   123  	_ = os.Remove(oldPath)
   124  
   125  	// move the existing executable to a new file in the same directory
   126  	err = os.Rename(opts.TargetPath, oldPath)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	// move the new executable in to become the new program
   132  	err = os.Rename(newPath, opts.TargetPath)
   133  
   134  	if err != nil {
   135  		// move unsuccessful
   136  		//
   137  		// The filesystem is now in a bad state. We have successfully
   138  		// moved the existing binary to a new location, but we couldn't move the new
   139  		// binary to take its place. That means there is no file where the current executable binary
   140  		// used to be!
   141  		// Try to rollback by restoring the old binary to its original path.
   142  		rerr := os.Rename(oldPath, opts.TargetPath)
   143  		if rerr != nil {
   144  			return &rollbackErr{err, rerr}
   145  		}
   146  
   147  		return err
   148  	}
   149  
   150  	// move successful, remove the old binary if needed
   151  	if removeOld {
   152  		errRemove := os.Remove(oldPath)
   153  
   154  		// windows has trouble with removing old binaries, so hide it instead
   155  		if errRemove != nil {
   156  			_ = hideFile(oldPath)
   157  		}
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  // RollbackError takes an error value returned by Apply and returns the error, if any,
   164  // that occurred when attempting to roll back from a failed update. Applications should
   165  // always call this function on any non-nil errors returned by Apply.
   166  //
   167  // If no rollback was needed or if the rollback was successful, RollbackError returns nil,
   168  // otherwise it returns the error encountered when trying to roll back.
   169  func RollbackError(err error) error {
   170  	if err == nil {
   171  		return nil
   172  	}
   173  	if rerr, ok := err.(*rollbackErr); ok {
   174  		return rerr.rollbackErr
   175  	}
   176  	return nil
   177  }
   178  
   179  type rollbackErr struct {
   180  	error             // original error
   181  	rollbackErr error // error encountered while rolling back
   182  }
   183  
   184  // Options for Apply update
   185  type Options struct {
   186  	// TargetPath defines the path to the file to update.
   187  	// The emptry string means 'the executable file of the running program'.
   188  	TargetPath string
   189  
   190  	// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
   191  	TargetMode os.FileMode
   192  
   193  	// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
   194  	Checksum []byte
   195  
   196  	// Public key to use for signature verification. If nil, no signature verification is done.
   197  	PublicKey crypto.PublicKey
   198  
   199  	// Signature to verify the updated file. If nil, no signature verification is done.
   200  	Signature []byte
   201  
   202  	// Pluggable signature verification algorithm. If nil, ECDSA is used.
   203  	Verifier Verifier
   204  
   205  	// Use this hash function to generate the checksum. If not set, SHA256 is used.
   206  	Hash crypto.Hash
   207  
   208  	// Store the old executable file at this path after a successful update.
   209  	// The empty string means the old executable file will be removed after the update.
   210  	OldSavePath string
   211  }
   212  
   213  // SetPublicKeyPEM is a convenience method to set the PublicKey property
   214  // used for checking a completed update's signature by parsing a
   215  // Public Key formatted as PEM data.
   216  func (o *Options) SetPublicKeyPEM(pembytes []byte) error {
   217  	block, _ := pem.Decode(pembytes)
   218  	if block == nil {
   219  		return errors.New("couldn't parse PEM data")
   220  	}
   221  
   222  	pub, err := x509.ParsePKIXPublicKey(block.Bytes)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	o.PublicKey = pub
   227  	return nil
   228  }
   229  
   230  func (o *Options) getPath() (string, error) {
   231  	if o.TargetPath != "" {
   232  		return o.TargetPath, nil
   233  	}
   234  	exe, err := os.Executable()
   235  	if err != nil {
   236  		return "", err
   237  	}
   238  
   239  	exe, err = filepath.EvalSymlinks(exe)
   240  	if err != nil {
   241  		return "", err
   242  	}
   243  
   244  	return exe, nil
   245  }
   246  
   247  func (o *Options) verifyChecksum(updated []byte) error {
   248  	checksum, err := checksumFor(o.Hash, updated)
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	if !bytes.Equal(o.Checksum, checksum) {
   254  		return fmt.Errorf("updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
   255  	}
   256  	return nil
   257  }
   258  
   259  func (o *Options) verifySignature(updated []byte) error {
   260  	checksum, err := checksumFor(o.Hash, updated)
   261  	if err != nil {
   262  		return err
   263  	}
   264  	return o.Verifier.VerifySignature(checksum, o.Signature, o.Hash, o.PublicKey)
   265  }
   266  
   267  func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
   268  	if !h.Available() {
   269  		return nil, errors.New("requested hash function not available")
   270  	}
   271  	hash := h.New()
   272  	hash.Write(payload) // guaranteed not to error
   273  	return hash.Sum([]byte{}), nil
   274  }