github.com/nicocha30/gvisor-ligolo@v0.0.0-20230726075806-989fa2c0a413/pkg/state/statefile/statefile.go (about)

     1  // Copyright 2018 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package statefile defines the state file data stream.
    16  //
    17  // This package currently does not include any details regarding the state
    18  // encoding itself, only details regarding state metadata and data layout.
    19  //
    20  // The file format is defined as follows.
    21  //
    22  // /------------------------------------------------------\
    23  // |                   header (8-bytes)                   |
    24  // +------------------------------------------------------+
    25  // |              metadata length (8-bytes)               |
    26  // +------------------------------------------------------+
    27  // |                       metadata                       |
    28  // +------------------------------------------------------+
    29  // |                         data                         |
    30  // \------------------------------------------------------/
    31  //
    32  // First, it includes a 8-byte magic header which is the following
    33  // sequence of bytes [0x67, 0x56, 0x69, 0x73, 0x6f, 0x72, 0x53, 0x46]
    34  //
    35  // This header is followed by an 8-byte length N (big endian), and an
    36  // ASCII-encoded JSON map that is exactly N bytes long.
    37  //
    38  // This map includes only strings for keys and strings for values. Keys in the
    39  // map that begin with "_" are for internal use only. They may be read, but may
    40  // not be provided by the user. In the future, this metadata may contain some
    41  // information relating to the state encoding itself.
    42  //
    43  // After the map, the remainder of the file is the state data.
    44  package statefile
    45  
    46  import (
    47  	"bytes"
    48  	"compress/flate"
    49  	"crypto/hmac"
    50  	"crypto/sha256"
    51  	"encoding/binary"
    52  	"encoding/json"
    53  	"fmt"
    54  	"hash"
    55  	"io"
    56  	"strings"
    57  	"time"
    58  
    59  	"github.com/nicocha30/gvisor-ligolo/pkg/compressio"
    60  	"github.com/nicocha30/gvisor-ligolo/pkg/state/wire"
    61  )
    62  
    63  // keySize is the AES-256 key length.
    64  const keySize = 32
    65  
    66  // compressionChunkSize is the chunk size for compression.
    67  const compressionChunkSize = 1024 * 1024
    68  
    69  // maxMetadataSize is the size limit of metadata section.
    70  const maxMetadataSize = 16 * 1024 * 1024
    71  
    72  // magicHeader is the byte sequence beginning each file.
    73  var magicHeader = []byte("\x67\x56\x69\x73\x6f\x72\x53\x46")
    74  
    75  // ErrBadMagic is returned if the header does not match.
    76  var ErrBadMagic = fmt.Errorf("bad magic header")
    77  
    78  // ErrMetadataMissing is returned if the state file is missing mandatory metadata.
    79  var ErrMetadataMissing = fmt.Errorf("missing metadata")
    80  
    81  // ErrInvalidMetadataLength is returned if the metadata length is too large.
    82  var ErrInvalidMetadataLength = fmt.Errorf("metadata length invalid, maximum size is %d", maxMetadataSize)
    83  
    84  // ErrMetadataInvalid is returned if passed metadata is invalid.
    85  var ErrMetadataInvalid = fmt.Errorf("metadata invalid, can't start with _")
    86  
    87  // WriteCloser is an io.Closer and wire.Writer.
    88  type WriteCloser interface {
    89  	wire.Writer
    90  	io.Closer
    91  }
    92  
    93  func writeMetadataLen(w io.Writer, val uint64) error {
    94  	var buf [8]byte
    95  	binary.BigEndian.PutUint64(buf[:], val)
    96  	_, err := w.Write(buf[:])
    97  	return err
    98  }
    99  
   100  // NewWriter returns a state data writer for a statefile.
   101  //
   102  // Note that the returned WriteCloser must be closed.
   103  func NewWriter(w io.Writer, key []byte, metadata map[string]string) (WriteCloser, error) {
   104  	if metadata == nil {
   105  		metadata = make(map[string]string)
   106  	}
   107  	for k := range metadata {
   108  		if strings.HasPrefix(k, "_") {
   109  			return nil, ErrMetadataInvalid
   110  		}
   111  	}
   112  
   113  	// Create our HMAC function.
   114  	h := hmac.New(sha256.New, key)
   115  	mw := io.MultiWriter(w, h)
   116  
   117  	// First, write the header.
   118  	if _, err := mw.Write(magicHeader); err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	// Generate a timestamp, for convenience only.
   123  	metadata["_timestamp"] = time.Now().UTC().String()
   124  	defer delete(metadata, "_timestamp")
   125  
   126  	// Write the metadata.
   127  	b, err := json.Marshal(metadata)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if len(b) > maxMetadataSize {
   133  		return nil, ErrInvalidMetadataLength
   134  	}
   135  
   136  	// Metadata length.
   137  	if err := writeMetadataLen(mw, uint64(len(b))); err != nil {
   138  		return nil, err
   139  	}
   140  	// Metadata bytes; io.MultiWriter will return a short write error if
   141  	// any of the writers returns < n.
   142  	if _, err := mw.Write(b); err != nil {
   143  		return nil, err
   144  	}
   145  	// Write the current hash.
   146  	cur := h.Sum(nil)
   147  	for done := 0; done < len(cur); {
   148  		n, err := mw.Write(cur[done:])
   149  		done += n
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  	}
   154  
   155  	// Wrap in compression. We always use "best speed" mode here. When using
   156  	// "best compression" mode, there is usually only a little gain in file
   157  	// size reduction, which translate to even smaller gain in restore
   158  	// latency reduction, while inccuring much more CPU usage at save time.
   159  	return compressio.NewWriter(w, key, compressionChunkSize, flate.BestSpeed)
   160  }
   161  
   162  // MetadataUnsafe reads out the metadata from a state file without verifying any
   163  // HMAC. This function shouldn't be called for untrusted input files.
   164  func MetadataUnsafe(r io.Reader) (map[string]string, error) {
   165  	return metadata(r, nil)
   166  }
   167  
   168  func readMetadataLen(r io.Reader) (uint64, error) {
   169  	var buf [8]byte
   170  	if _, err := io.ReadFull(r, buf[:]); err != nil {
   171  		return 0, err
   172  	}
   173  	return binary.BigEndian.Uint64(buf[:]), nil
   174  }
   175  
   176  // metadata validates the magic header and reads out the metadata from a state
   177  // data stream.
   178  func metadata(r io.Reader, h hash.Hash) (map[string]string, error) {
   179  	if h != nil {
   180  		r = io.TeeReader(r, h)
   181  	}
   182  
   183  	// Read and validate magic header.
   184  	b := make([]byte, len(magicHeader))
   185  	if _, err := r.Read(b); err != nil {
   186  		return nil, err
   187  	}
   188  	if !bytes.Equal(b, magicHeader) {
   189  		return nil, ErrBadMagic
   190  	}
   191  
   192  	// Read and validate metadata.
   193  	b, err := func() (b []byte, err error) {
   194  		defer func() {
   195  			if r := recover(); r != nil {
   196  				b = nil
   197  				err = fmt.Errorf("%v", r)
   198  			}
   199  		}()
   200  
   201  		metadataLen, err := readMetadataLen(r)
   202  		if err != nil {
   203  			return nil, err
   204  		}
   205  		if metadataLen > maxMetadataSize {
   206  			return nil, ErrInvalidMetadataLength
   207  		}
   208  		b = make([]byte, int(metadataLen))
   209  		if _, err := io.ReadFull(r, b); err != nil {
   210  			return nil, err
   211  		}
   212  		return b, nil
   213  	}()
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	if h != nil {
   219  		// Check the hash prior to decoding.
   220  		cur := h.Sum(nil)
   221  		buf := make([]byte, len(cur))
   222  		if _, err := io.ReadFull(r, buf); err != nil {
   223  			return nil, err
   224  		}
   225  		if !hmac.Equal(cur, buf) {
   226  			return nil, compressio.ErrHashMismatch
   227  		}
   228  	}
   229  
   230  	// Decode the metadata.
   231  	metadata := make(map[string]string)
   232  	if err := json.Unmarshal(b, &metadata); err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	return metadata, nil
   237  }
   238  
   239  // NewReader returns a reader for a statefile.
   240  func NewReader(r io.Reader, key []byte) (wire.Reader, map[string]string, error) {
   241  	// Read the metadata with the hash.
   242  	h := hmac.New(sha256.New, key)
   243  	metadata, err := metadata(r, h)
   244  	if err != nil {
   245  		return nil, nil, err
   246  	}
   247  
   248  	// Wrap in compression.
   249  	cr, err := compressio.NewReader(r, key)
   250  	if err != nil {
   251  		return nil, nil, err
   252  	}
   253  	return cr, metadata, nil
   254  }