github.com/trustbloc/kms-go@v1.1.2/kms/webkms/crypto_box.go (about)

     1  /*
     2  Copyright SecureKey Technologies Inc. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package webkms
     8  
     9  import (
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"time"
    14  
    15  	"golang.org/x/crypto/nacl/box"
    16  
    17  	"github.com/trustbloc/kms-go/util/cryptoutil"
    18  
    19  	"github.com/trustbloc/kms-go/spi/kms"
    20  
    21  	"github.com/trustbloc/kms-go/doc/util/jwkkid"
    22  )
    23  
    24  // TODO move CryptoBox out of webkms package.
    25  //  this currently only sits inside webkms so it can execute crypto with private keys remotely. See issue #511
    26  // TODO delete this file and its corresponding test file when LegacyPacker is removed.
    27  
    28  type easyReq struct {
    29  	Payload  []byte `json:"payload"`
    30  	Nonce    []byte `json:"nonce"`
    31  	TheirPub []byte `json:"their_pub"`
    32  }
    33  
    34  type easyResp struct {
    35  	Ciphertext []byte `json:"ciphertext"`
    36  }
    37  
    38  type easyOpenReq struct {
    39  	Ciphertext []byte `json:"ciphertext"`
    40  	Nonce      []byte `json:"nonce"`
    41  	TheirPub   []byte `json:"their_pub"`
    42  	MyPub      []byte `json:"my_pub"`
    43  }
    44  
    45  type easyOpenResp struct {
    46  	Plaintext []byte `json:"plaintext"`
    47  }
    48  
    49  type sealOpenReq struct {
    50  	Ciphertext []byte `json:"ciphertext"`
    51  	MyPub      []byte `json:"my_pub"`
    52  }
    53  
    54  type sealOpenResp struct {
    55  	Plaintext []byte `json:"plaintext"`
    56  }
    57  
    58  const (
    59  	wrapURL   = "/wrap"
    60  	unwrapURL = "/unwrap"
    61  )
    62  
    63  // CryptoBox provides an elliptic-curve-based authenticated encryption scheme executed on a remote key server
    64  //
    65  // Payloads are encrypted using symmetric encryption (XChacha20Poly1305)
    66  // using a shared key derived from a shared secret created by
    67  //
    68  //	Curve25519 Elliptic Curve Diffie-Hellman key exchange.
    69  //
    70  // CryptoBox is created by a remote KMS, and remotely reads secret keys from the KMS
    71  //
    72  //	for encryption/decryption, so clients do not need to see
    73  //	the secrets themselves.
    74  type CryptoBox struct {
    75  	km *RemoteKMS
    76  }
    77  
    78  // NewCryptoBox creates a CryptoBox which provides remote crypto box encryption using the given KMS's key.
    79  func NewCryptoBox(w kms.KeyManager) (*CryptoBox, error) {
    80  	lkms, ok := w.(*RemoteKMS)
    81  	if !ok {
    82  		return nil, fmt.Errorf("cannot use parameter argument as KMS")
    83  	}
    84  
    85  	return &CryptoBox{km: lkms}, nil
    86  }
    87  
    88  // Easy remotely seals a message with a provided nonce
    89  // theirPub is used as a public key, while myPub is used to identify the private key that should be used.
    90  func (b *CryptoBox) Easy(payload, nonce, theirPub []byte, myKID string) ([]byte, error) {
    91  	easyStart := time.Now()
    92  	keyURL := b.km.buildKIDURL(myKID)
    93  
    94  	destination := keyURL + wrapURL
    95  
    96  	httpReqJSON := &easyReq{
    97  		Payload:  payload,
    98  		Nonce:    nonce,
    99  		TheirPub: theirPub,
   100  	}
   101  
   102  	marshaledReq, err := b.km.marshalFunc(httpReqJSON)
   103  	if err != nil {
   104  		return nil, fmt.Errorf("failed to marshal Easy request [%s, %w]", destination, err)
   105  	}
   106  
   107  	resp, err := b.km.postHTTPRequest(destination, marshaledReq)
   108  	if err != nil {
   109  		return nil, fmt.Errorf("posting Easy request failed [%s, %w]", destination, err)
   110  	}
   111  
   112  	// handle response
   113  	defer closeResponseBody(resp.Body, "Easy")
   114  
   115  	err = checkError(resp)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	respBody, err := ioutil.ReadAll(resp.Body)
   121  	if err != nil {
   122  		return nil, fmt.Errorf("read ciphertext response for Easy failed [%s, %w]", destination, err)
   123  	}
   124  
   125  	httpResp := &easyResp{}
   126  
   127  	err = b.km.unmarshalFunc(respBody, httpResp)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("unmarshal ciphertext for Easy failed [%s, %w]", destination, err)
   130  	}
   131  
   132  	debugLogger.Printf("overall Easy duration: %s", time.Since(easyStart))
   133  
   134  	return httpResp.Ciphertext, nil
   135  }
   136  
   137  // EasyOpen remotely unseals a message sealed with Easy, where the nonce is provided.
   138  // theirPub is the public key used to decrypt directly, while myPub is used to identify the private key to be used.
   139  func (b *CryptoBox) EasyOpen(cipherText, nonce, theirPub, myPub []byte) ([]byte, error) {
   140  	easyOpenStart := time.Now()
   141  
   142  	destination, err := b.buildUnwrapURL(myPub)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	httpReqJSON := &easyOpenReq{
   148  		Ciphertext: cipherText,
   149  		Nonce:      nonce,
   150  		TheirPub:   theirPub,
   151  		MyPub:      myPub,
   152  	}
   153  
   154  	marshaledReq, err := b.km.marshalFunc(httpReqJSON)
   155  	if err != nil {
   156  		return nil, fmt.Errorf("failed to marshal EasyOpen request [%s, %w]", destination, err)
   157  	}
   158  
   159  	resp, err := b.km.postHTTPRequest(destination, marshaledReq)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("posting EasyOpen failed [%s, %w]", destination, err)
   162  	}
   163  
   164  	// handle response
   165  	defer closeResponseBody(resp.Body, "EasyOpen")
   166  
   167  	err = checkError(resp)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	respBody, err := ioutil.ReadAll(resp.Body)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("read plaintext response for EasyOpen failed [%s, %w]", destination, err)
   175  	}
   176  
   177  	httpResp := &easyOpenResp{}
   178  
   179  	err = b.km.unmarshalFunc(respBody, httpResp)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("unmarshal plaintext for EasyOpen failed [%s, %w]", destination, err)
   182  	}
   183  
   184  	debugLogger.Printf("overall easyOpen duration: %s", time.Since(easyOpenStart))
   185  
   186  	return httpResp.Plaintext, nil
   187  }
   188  
   189  // Seal seals a payload using the equivalent of libsodium box_seal. This is an exact copy of localkms's CryptoBox.Seal()
   190  // as no private key is involved and therefore it is not necessary to call the key server.
   191  //
   192  // Generates an ephemeral keypair to use for the sender, and includes
   193  // the ephemeral sender public key in the message.
   194  func (b *CryptoBox) Seal(payload, theirEncPub []byte, randSource io.Reader) ([]byte, error) {
   195  	sealStart := time.Now()
   196  	// generate ephemeral curve25519 asymmetric keys
   197  	epk, esk, err := box.GenerateKey(randSource)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	var recPubBytes [cryptoutil.Curve25519KeySize]byte
   203  
   204  	copy(recPubBytes[:], theirEncPub)
   205  
   206  	nonce, err := cryptoutil.Nonce(epk[:], theirEncPub)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  
   211  	// now seal the msg with the ephemeral key, nonce and recPub (which is recipient's publicKey)
   212  	ret := box.Seal(epk[:], payload, nonce, &recPubBytes, esk)
   213  
   214  	debugLogger.Printf("overall Seal (non remote call) duration: %s", time.Since(sealStart))
   215  
   216  	return ret, nil
   217  }
   218  
   219  // SealOpen remotely decrypts a payload encrypted with Seal.
   220  //
   221  // Reads the ephemeral sender public key, prepended to a properly-formatted message,
   222  // and uses that along with the recipient private key corresponding to myPub to decrypt the message.
   223  func (b *CryptoBox) SealOpen(cipherText, myPub []byte) ([]byte, error) {
   224  	sealOpenStart := time.Now()
   225  
   226  	destination, err := b.buildUnwrapURL(myPub)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	httpReqJSON := &sealOpenReq{
   232  		Ciphertext: cipherText,
   233  		MyPub:      myPub,
   234  	}
   235  
   236  	marshaledReq, err := b.km.marshalFunc(httpReqJSON)
   237  	if err != nil {
   238  		return nil, fmt.Errorf("failed to marshal SealOpen request [%s, %w]", destination, err)
   239  	}
   240  
   241  	resp, err := b.km.postHTTPRequest(destination, marshaledReq)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("posting SealOpen failed [%s, %w]", destination, err)
   244  	}
   245  
   246  	// handle response
   247  	defer closeResponseBody(resp.Body, "SealOpen")
   248  
   249  	err = checkError(resp)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  
   254  	respBody, err := ioutil.ReadAll(resp.Body)
   255  	if err != nil {
   256  		return nil, fmt.Errorf("read plaintext response for SealOpen failed [%s, %w]", destination, err)
   257  	}
   258  
   259  	httpResp := &sealOpenResp{}
   260  
   261  	err = b.km.unmarshalFunc(respBody, httpResp)
   262  	if err != nil {
   263  		return nil, fmt.Errorf("unmarshal plaintext for SealOpen failed [%s, %w]", destination, err)
   264  	}
   265  
   266  	debugLogger.Printf("overall SealOpen duration: %s", time.Since(sealOpenStart))
   267  
   268  	return httpResp.Plaintext, nil
   269  }
   270  
   271  func (b *CryptoBox) buildUnwrapURL(myPub []byte) (string, error) {
   272  	// remote kms requires keyID in the keyURL for unwrapURL.
   273  	kid, err := jwkkid.CreateKID(myPub, kms.ED25519Type)
   274  	if err != nil {
   275  		return "", err
   276  	}
   277  
   278  	keyURL := b.km.buildKIDURL(kid)
   279  
   280  	return keyURL + unwrapURL, nil
   281  }