// Copyright (C) MongoDB, Inc. 2017-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package driver import ( "context" "crypto/tls" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "strings" "time" "go.mongodb.org/mongo-driver/bson/bsontype" "go.mongodb.org/mongo-driver/internal" "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt" "go.mongodb.org/mongo-driver/x/mongo/driver/mongocrypt/options" ) const ( defaultKmsPort = 443 defaultKmsTimeout = 10 * time.Second ) // CollectionInfoFn is a callback used to retrieve collection information. type CollectionInfoFn func(ctx context.Context, db string, filter bsoncore.Document) (bsoncore.Document, error) // KeyRetrieverFn is a callback used to retrieve keys from the key vault. type KeyRetrieverFn func(ctx context.Context, filter bsoncore.Document) ([]bsoncore.Document, error) // MarkCommandFn is a callback used to add encryption markings to a command. type MarkCommandFn func(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error) // CryptOptions specifies options to configure a Crypt instance. type CryptOptions struct { MongoCrypt *mongocrypt.MongoCrypt CollInfoFn CollectionInfoFn KeyFn KeyRetrieverFn MarkFn MarkCommandFn TLSConfig map[string]*tls.Config HTTPClient *http.Client BypassAutoEncryption bool BypassQueryAnalysis bool } // Crypt is an interface implemented by types that can encrypt and decrypt instances of // bsoncore.Document. // // Users should rely on the driver's crypt type (used by default) for encryption and decryption // unless they are perfectly confident in another implementation of Crypt. type Crypt interface { // Encrypt encrypts the given command. Encrypt(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error) // Decrypt decrypts the given command response. Decrypt(ctx context.Context, cmdResponse bsoncore.Document) (bsoncore.Document, error) // CreateDataKey creates a data key using the given KMS provider and options. CreateDataKey(ctx context.Context, kmsProvider string, opts *options.DataKeyOptions) (bsoncore.Document, error) // EncryptExplicit encrypts the given value with the given options. EncryptExplicit(ctx context.Context, val bsoncore.Value, opts *options.ExplicitEncryptionOptions) (byte, []byte, error) // DecryptExplicit decrypts the given encrypted value. DecryptExplicit(ctx context.Context, subtype byte, data []byte) (bsoncore.Value, error) // Close cleans up any resources associated with the Crypt instance. Close() // BypassAutoEncryption returns true if auto-encryption should be bypassed. BypassAutoEncryption() bool // RewrapDataKey attempts to rewrap the document data keys matching the filter, preparing the re-wrapped documents // to be returned as a slice of bsoncore.Document. RewrapDataKey(ctx context.Context, filter []byte, opts *options.RewrapManyDataKeyOptions) ([]bsoncore.Document, error) } // crypt consumes the libmongocrypt.MongoCrypt type to iterate the mongocrypt state machine and perform encryption // and decryption. type crypt struct { mongoCrypt *mongocrypt.MongoCrypt collInfoFn CollectionInfoFn keyFn KeyRetrieverFn markFn MarkCommandFn tlsConfig map[string]*tls.Config httpClient *http.Client bypassAutoEncryption bool } // NewCrypt creates a new Crypt instance configured with the given AutoEncryptionOptions. func NewCrypt(opts *CryptOptions) Crypt { c := &crypt{ mongoCrypt: opts.MongoCrypt, collInfoFn: opts.CollInfoFn, keyFn: opts.KeyFn, markFn: opts.MarkFn, tlsConfig: opts.TLSConfig, httpClient: opts.HTTPClient, bypassAutoEncryption: opts.BypassAutoEncryption, } if c.httpClient == nil { c.httpClient = internal.DefaultHTTPClient } return c } // Encrypt encrypts the given command. func (c *crypt) Encrypt(ctx context.Context, db string, cmd bsoncore.Document) (bsoncore.Document, error) { if c.bypassAutoEncryption { return cmd, nil } cryptCtx, err := c.mongoCrypt.CreateEncryptionContext(db, cmd) if err != nil { return nil, err } defer cryptCtx.Close() return c.executeStateMachine(ctx, cryptCtx, db) } // Decrypt decrypts the given command response. func (c *crypt) Decrypt(ctx context.Context, cmdResponse bsoncore.Document) (bsoncore.Document, error) { cryptCtx, err := c.mongoCrypt.CreateDecryptionContext(cmdResponse) if err != nil { return nil, err } defer cryptCtx.Close() return c.executeStateMachine(ctx, cryptCtx, "") } // CreateDataKey creates a data key using the given KMS provider and options. func (c *crypt) CreateDataKey(ctx context.Context, kmsProvider string, opts *options.DataKeyOptions) (bsoncore.Document, error) { cryptCtx, err := c.mongoCrypt.CreateDataKeyContext(kmsProvider, opts) if err != nil { return nil, err } defer cryptCtx.Close() return c.executeStateMachine(ctx, cryptCtx, "") } // RewrapDataKey attempts to rewrap the document data keys matching the filter, preparing the re-wrapped documents to // be returned as a slice of bsoncore.Document. func (c *crypt) RewrapDataKey(ctx context.Context, filter []byte, opts *options.RewrapManyDataKeyOptions) ([]bsoncore.Document, error) { cryptCtx, err := c.mongoCrypt.RewrapDataKeyContext(filter, opts) if err != nil { return nil, err } defer cryptCtx.Close() rewrappedBSON, err := c.executeStateMachine(ctx, cryptCtx, "") if err != nil { return nil, err } if rewrappedBSON == nil { return nil, nil } // mongocrypt_ctx_rewrap_many_datakey_init wraps the documents in a BSON of the form { "v": [(BSON document), ...] } // where each BSON document in the slice is a document containing a rewrapped datakey. rewrappedDocumentBytes, err := rewrappedBSON.LookupErr("v") if err != nil { return nil, err } // Parse the resulting BSON as individual documents. rewrappedDocsArray, ok := rewrappedDocumentBytes.ArrayOK() if !ok { return nil, fmt.Errorf("expected results from mongocrypt_ctx_rewrap_many_datakey_init to be an array") } rewrappedDocumentValues, err := rewrappedDocsArray.Values() if err != nil { return nil, err } rewrappedDocuments := []bsoncore.Document{} for _, rewrappedDocumentValue := range rewrappedDocumentValues { if rewrappedDocumentValue.Type != bsontype.EmbeddedDocument { // If a value in the document's array returned by mongocrypt is anything other than an embedded document, // then something is wrong and we should terminate the routine. return nil, fmt.Errorf("expected value of type %q, got: %q", bsontype.EmbeddedDocument.String(), rewrappedDocumentValue.Type.String()) } rewrappedDocuments = append(rewrappedDocuments, rewrappedDocumentValue.Document()) } return rewrappedDocuments, nil } // EncryptExplicit encrypts the given value with the given options. func (c *crypt) EncryptExplicit(ctx context.Context, val bsoncore.Value, opts *options.ExplicitEncryptionOptions) (byte, []byte, error) { idx, doc := bsoncore.AppendDocumentStart(nil) doc = bsoncore.AppendValueElement(doc, "v", val) doc, _ = bsoncore.AppendDocumentEnd(doc, idx) cryptCtx, err := c.mongoCrypt.CreateExplicitEncryptionContext(doc, opts) if err != nil { return 0, nil, err } defer cryptCtx.Close() res, err := c.executeStateMachine(ctx, cryptCtx, "") if err != nil { return 0, nil, err } sub, data := res.Lookup("v").Binary() return sub, data, nil } // DecryptExplicit decrypts the given encrypted value. func (c *crypt) DecryptExplicit(ctx context.Context, subtype byte, data []byte) (bsoncore.Value, error) { idx, doc := bsoncore.AppendDocumentStart(nil) doc = bsoncore.AppendBinaryElement(doc, "v", subtype, data) doc, _ = bsoncore.AppendDocumentEnd(doc, idx) cryptCtx, err := c.mongoCrypt.CreateExplicitDecryptionContext(doc) if err != nil { return bsoncore.Value{}, err } defer cryptCtx.Close() res, err := c.executeStateMachine(ctx, cryptCtx, "") if err != nil { return bsoncore.Value{}, err } return res.Lookup("v"), nil } // Close cleans up any resources associated with the Crypt instance. func (c *crypt) Close() { c.mongoCrypt.Close() if c.httpClient == internal.DefaultHTTPClient { internal.CloseIdleHTTPConnections(c.httpClient) } } func (c *crypt) BypassAutoEncryption() bool { return c.bypassAutoEncryption } func (c *crypt) executeStateMachine(ctx context.Context, cryptCtx *mongocrypt.Context, db string) (bsoncore.Document, error) { var err error for { state := cryptCtx.State() switch state { case mongocrypt.NeedMongoCollInfo: err = c.collectionInfo(ctx, cryptCtx, db) case mongocrypt.NeedMongoMarkings: err = c.markCommand(ctx, cryptCtx, db) case mongocrypt.NeedMongoKeys: err = c.retrieveKeys(ctx, cryptCtx) case mongocrypt.NeedKms: err = c.decryptKeys(cryptCtx) case mongocrypt.Ready: return cryptCtx.Finish() case mongocrypt.Done: return nil, nil case mongocrypt.NeedKmsCredentials: err = c.provideKmsProviders(ctx, cryptCtx) default: return nil, fmt.Errorf("invalid Crypt state: %v", state) } if err != nil { return nil, err } } } func (c *crypt) collectionInfo(ctx context.Context, cryptCtx *mongocrypt.Context, db string) error { op, err := cryptCtx.NextOperation() if err != nil { return err } collInfo, err := c.collInfoFn(ctx, db, op) if err != nil { return err } if collInfo != nil { if err = cryptCtx.AddOperationResult(collInfo); err != nil { return err } } return cryptCtx.CompleteOperation() } func (c *crypt) markCommand(ctx context.Context, cryptCtx *mongocrypt.Context, db string) error { op, err := cryptCtx.NextOperation() if err != nil { return err } markedCmd, err := c.markFn(ctx, db, op) if err != nil { return err } if err = cryptCtx.AddOperationResult(markedCmd); err != nil { return err } return cryptCtx.CompleteOperation() } func (c *crypt) retrieveKeys(ctx context.Context, cryptCtx *mongocrypt.Context) error { op, err := cryptCtx.NextOperation() if err != nil { return err } keys, err := c.keyFn(ctx, op) if err != nil { return err } for _, key := range keys { if err = cryptCtx.AddOperationResult(key); err != nil { return err } } return cryptCtx.CompleteOperation() } func (c *crypt) decryptKeys(cryptCtx *mongocrypt.Context) error { for { kmsCtx := cryptCtx.NextKmsContext() if kmsCtx == nil { break } if err := c.decryptKey(kmsCtx); err != nil { return err } } return cryptCtx.FinishKmsContexts() } func (c *crypt) decryptKey(kmsCtx *mongocrypt.KmsContext) error { host, err := kmsCtx.HostName() if err != nil { return err } msg, err := kmsCtx.Message() if err != nil { return err } // add a port to the address if it's not already present addr := host if idx := strings.IndexByte(host, ':'); idx == -1 { addr = fmt.Sprintf("%s:%d", host, defaultKmsPort) } kmsProvider := kmsCtx.KMSProvider() tlsCfg := c.tlsConfig[kmsProvider] if tlsCfg == nil { tlsCfg = &tls.Config{MinVersion: tls.VersionTLS12} } conn, err := tls.Dial("tcp", addr, tlsCfg) if err != nil { return err } defer func() { _ = conn.Close() }() if err = conn.SetWriteDeadline(time.Now().Add(defaultKmsTimeout)); err != nil { return err } if _, err = conn.Write(msg); err != nil { return err } for { bytesNeeded := kmsCtx.BytesNeeded() if bytesNeeded == 0 { return nil } res := make([]byte, bytesNeeded) bytesRead, err := conn.Read(res) if err != nil && err != io.EOF { return err } if err = kmsCtx.FeedResponse(res[:bytesRead]); err != nil { return err } } } // needsKmsProvider returns true if provider was initially set to an empty document. // An empty document signals the driver to fetch credentials. func needsKmsProvider(kmsProviders bsoncore.Document, provider string) bool { val, err := kmsProviders.LookupErr(provider) if err != nil { // KMS provider is not configured. return false } doc, ok := val.DocumentOK() // KMS provider is an empty document. return ok && len(doc) == 5 } func getGCPAccessToken(ctx context.Context, httpClient *http.Client) (string, error) { metadataHost := "metadata.google.internal" if envhost := os.Getenv("GCE_METADATA_HOST"); envhost != "" { metadataHost = envhost } url := fmt.Sprintf("http://%s/computeMetadata/v1/instance/service-accounts/default/token", metadataHost) req, err := http.NewRequest("GET", url, nil) if err != nil { return "", internal.WrapErrorf(err, "unable to retrieve GCP credentials") } req.Header.Set("Metadata-Flavor", "Google") resp, err := httpClient.Do(req.WithContext(ctx)) if err != nil { return "", internal.WrapErrorf(err, "unable to retrieve GCP credentials") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", internal.WrapErrorf(err, "unable to retrieve GCP credentials: error reading response body") } if resp.StatusCode != http.StatusOK { return "", internal.WrapErrorf(err, "unable to retrieve GCP credentials: expected StatusCode 200, got StatusCode: %v. Response body: %s", resp.StatusCode, body) } var tokenResponse struct { AccessToken string `json:"access_token"` } // Attempt to read body as JSON err = json.Unmarshal(body, &tokenResponse) if err != nil { return "", internal.WrapErrorf(err, "unable to retrieve GCP credentials: error reading body JSON. Response body: %s", body) } if tokenResponse.AccessToken == "" { return "", fmt.Errorf("unable to retrieve GCP credentials: got unexpected empty accessToken from GCP Metadata Server. Response body: %s", body) } return tokenResponse.AccessToken, nil } func (c *crypt) provideKmsProviders(ctx context.Context, cryptCtx *mongocrypt.Context) error { kmsProviders := c.mongoCrypt.GetKmsProviders() builder := bsoncore.NewDocumentBuilder() if needsKmsProvider(kmsProviders, "gcp") { // "gcp" KMS provider is an empty document. // Attempt to fetch from GCP Instance Metadata server. { token, err := getGCPAccessToken(ctx, c.httpClient) if err != nil { return err } builder.StartDocument("gcp"). AppendString("accessToken", token). FinishDocument() } } return cryptCtx.ProvideKmsProviders(builder.Build()) }