storj.io/uplink@v1.13.0/private/multipart/multipart.go (about)

     1  // Copyright (C) 2022 Storj Labs, Inc.
     2  // See LICENSE for copying information.
     3  
     4  package multipart
     5  
     6  import (
     7  	"context"
     8  	"crypto/rand"
     9  	"time"
    10  	_ "unsafe" // for go:linkname
    11  
    12  	"github.com/spacemonkeygo/monkit/v3"
    13  	"github.com/zeebo/errs"
    14  
    15  	"storj.io/common/base58"
    16  	"storj.io/common/encryption"
    17  	"storj.io/common/paths"
    18  	"storj.io/common/pb"
    19  	"storj.io/common/storj"
    20  	"storj.io/uplink"
    21  	"storj.io/uplink/private/metaclient"
    22  )
    23  
    24  var mon = monkit.Package()
    25  
    26  // UploadOptions contains additional options for uploading.
    27  type UploadOptions struct {
    28  	// When Expires is zero, there is no expiration.
    29  	Expires time.Time
    30  
    31  	CustomMetadata uplink.CustomMetadata
    32  }
    33  
    34  // BeginUpload begins a new multipart upload to bucket and key.
    35  //
    36  // Use project.UploadPart to upload individual parts.
    37  //
    38  // Use project.CommitUpload to finish the upload.
    39  //
    40  // Use project.AbortUpload to cancel the upload at any time.
    41  //
    42  // UploadObject is a convenient way to upload single part objects.
    43  func BeginUpload(ctx context.Context, project *uplink.Project, bucket, key string, options *UploadOptions) (info uplink.UploadInfo, err error) {
    44  	defer mon.Task()(&ctx)(&err)
    45  
    46  	switch {
    47  	case bucket == "":
    48  		return uplink.UploadInfo{}, convertKnownErrors(metaclient.ErrNoBucket.New(""), bucket, key)
    49  	case key == "":
    50  		return uplink.UploadInfo{}, convertKnownErrors(metaclient.ErrNoPath.New(""), bucket, key)
    51  	}
    52  
    53  	if options == nil {
    54  		options = &UploadOptions{}
    55  	}
    56  
    57  	encPath, err := encryptPath(project, bucket, key)
    58  	if err != nil {
    59  		return uplink.UploadInfo{}, convertKnownErrors(err, bucket, key)
    60  	}
    61  
    62  	metainfoClient, err := dialMetainfoClient(ctx, project)
    63  	if err != nil {
    64  		return uplink.UploadInfo{}, convertKnownErrors(err, bucket, key)
    65  	}
    66  	defer func() { err = errs.Combine(err, metainfoClient.Close()) }()
    67  
    68  	metadata, err := encryptMetadata(project, bucket, key, options.CustomMetadata)
    69  	if err != nil {
    70  		return uplink.UploadInfo{}, convertKnownErrors(err, bucket, key)
    71  	}
    72  
    73  	response, err := metainfoClient.BeginObject(ctx, metaclient.BeginObjectParams{
    74  		Bucket:               []byte(bucket),
    75  		EncryptedObjectKey:   []byte(encPath.Raw()),
    76  		ExpiresAt:            options.Expires,
    77  		EncryptionParameters: encryptionParameters(project),
    78  
    79  		EncryptedMetadata:             metadata.EncryptedContent,
    80  		EncryptedMetadataEncryptedKey: metadata.EncryptedKey,
    81  		EncryptedMetadataNonce:        metadata.EncryptedKeyNonce,
    82  	})
    83  	if err != nil {
    84  		return uplink.UploadInfo{}, convertKnownErrors(err, bucket, key)
    85  	}
    86  
    87  	encodedStreamID := base58.CheckEncode(response.StreamID[:], 1)
    88  	return uplink.UploadInfo{
    89  		Key:      key,
    90  		UploadID: encodedStreamID,
    91  		System: uplink.SystemMetadata{
    92  			Expires: options.Expires,
    93  		},
    94  		Custom: options.CustomMetadata,
    95  	}, nil
    96  }
    97  
    98  type encryptedMetadata struct {
    99  	EncryptedContent  []byte
   100  	EncryptedKey      []byte
   101  	EncryptedKeyNonce storj.Nonce
   102  }
   103  
   104  func encryptMetadata(project *uplink.Project, bucket, key string, metadata uplink.CustomMetadata) (encryptedMetadata, error) {
   105  	if len(metadata) == 0 {
   106  		return encryptedMetadata{}, nil
   107  	}
   108  
   109  	metadataBytes, err := pb.Marshal(&pb.SerializableMeta{
   110  		UserDefined: metadata.Clone(),
   111  	})
   112  	if err != nil {
   113  		return encryptedMetadata{}, errs.Wrap(err)
   114  	}
   115  
   116  	streamInfo, err := pb.Marshal(&pb.StreamInfo{
   117  		Metadata: metadataBytes,
   118  	})
   119  	if err != nil {
   120  		return encryptedMetadata{}, errs.Wrap(err)
   121  	}
   122  
   123  	derivedKey, err := deriveContentKey(project, bucket, key)
   124  	if err != nil {
   125  		return encryptedMetadata{}, errs.Wrap(err)
   126  	}
   127  
   128  	var metadataKey storj.Key
   129  	// generate random key for encrypting the segment's content
   130  	_, err = rand.Read(metadataKey[:])
   131  	if err != nil {
   132  		return encryptedMetadata{}, errs.Wrap(err)
   133  	}
   134  
   135  	var encryptedKeyNonce storj.Nonce
   136  	// generate random nonce for encrypting the metadata key
   137  	_, err = rand.Read(encryptedKeyNonce[:])
   138  	if err != nil {
   139  		return encryptedMetadata{}, errs.Wrap(err)
   140  	}
   141  
   142  	encryptionParameters := encryptionParameters(project)
   143  	encryptedKey, err := encryption.EncryptKey(&metadataKey, encryptionParameters.CipherSuite, derivedKey, &encryptedKeyNonce)
   144  	if err != nil {
   145  		return encryptedMetadata{}, errs.Wrap(err)
   146  	}
   147  
   148  	// encrypt metadata with the content encryption key and zero nonce.
   149  	encryptedStreamInfo, err := encryption.Encrypt(streamInfo, encryptionParameters.CipherSuite, &metadataKey, &storj.Nonce{})
   150  	if err != nil {
   151  		return encryptedMetadata{}, errs.Wrap(err)
   152  	}
   153  
   154  	// TODO should we commit StreamMeta or commit only encrypted StreamInfo
   155  	streamMetaBytes, err := pb.Marshal(&pb.StreamMeta{
   156  		EncryptedStreamInfo: encryptedStreamInfo,
   157  	})
   158  	if err != nil {
   159  		return encryptedMetadata{}, errs.Wrap(err)
   160  	}
   161  
   162  	return encryptedMetadata{
   163  		EncryptedContent:  streamMetaBytes,
   164  		EncryptedKey:      encryptedKey,
   165  		EncryptedKeyNonce: encryptedKeyNonce,
   166  	}, nil
   167  }
   168  
   169  //go:linkname convertKnownErrors storj.io/uplink.convertKnownErrors
   170  func convertKnownErrors(err error, bucket, key string) error
   171  
   172  //go:linkname dialMetainfoClient storj.io/uplink.dialMetainfoClient
   173  func dialMetainfoClient(ctx context.Context, project *uplink.Project) (_ *metaclient.Client, err error)
   174  
   175  //go:linkname encryptionParameters storj.io/uplink.encryptionParameters
   176  func encryptionParameters(project *uplink.Project) storj.EncryptionParameters
   177  
   178  //go:linkname encryptPath storj.io/uplink.encryptPath
   179  func encryptPath(project *uplink.Project, bucket, key string) (paths.Encrypted, error)
   180  
   181  //go:linkname deriveContentKey storj.io/uplink.deriveContentKey
   182  func deriveContentKey(project *uplink.Project, bucket, key string) (*storj.Key, error)