Skip to content

Commit

Permalink
GODRIVER-2400 Add FLE 2 API to explicit encryption (mongodb#960)
Browse files Browse the repository at this point in the history
* add QueryType, ContentionFactor, and new algorithm values

* add Explicit Encryption prose test

* document requirement of libmongocrypt 1.5.0
  • Loading branch information
kevinAlbs authored May 27, 2022
1 parent bc14c6e commit 62908c2
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 15 deletions.
30 changes: 30 additions & 0 deletions data/client-side-encryption-prose/encrypted-fields.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"fields": [
{
"keyId": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedIndexed",
"bsonType": "string",
"queries": {
"queryType": "equality",
"contention": {
"$numberLong": "0"
}
}
},
{
"keyId": {
"$binary": {
"base64": "q83vqxI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"path": "encryptedUnindexed",
"bsonType": "string"
}
]
}
30 changes: 30 additions & 0 deletions data/client-side-encryption-prose/key1-document.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"_id": {
"$binary": {
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
"subType": "04"
}
},
"keyMaterial": {
"$binary": {
"base64": "sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==",
"subType": "00"
}
},
"creationDate": {
"$date": {
"$numberLong": "1648914851981"
}
},
"updateDate": {
"$date": {
"$numberLong": "1648914851981"
}
},
"status": {
"$numberInt": "0"
},
"masterKey": {
"provider": "local"
}
}
12 changes: 12 additions & 0 deletions mongo/client_encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ func (ce *ClientEncryption) Encrypt(ctx context.Context, val bson.RawValue, opts
transformed.SetKeyAltName(*eo.KeyAltName)
}
transformed.SetAlgorithm(eo.Algorithm)
if eo.QueryType != nil {
switch *eo.QueryType {
case options.QueryTypeEquality:
transformed.SetQueryType(cryptOpts.QueryTypeEquality)
default:
return primitive.Binary{}, fmt.Errorf("unsupported value for QueryType: %v", *eo.QueryType)
}
}

if eo.ContentionFactor != nil {
transformed.SetContentionFactor(*eo.ContentionFactor)
}

subtype, data, err := ce.crypt.EncryptExplicit(ctx, bsoncore.Value{Type: val.Type, Data: val.Value}, transformed)
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions mongo/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,14 @@
//
// Note: Auto encryption is an enterprise-only feature.
//
// The libmongocrypt C library is required when using client-side encryption. libmongocrypt version 1.3.0 or higher is
// required when using driver version 1.8.0 or higher. To install libmongocrypt, follow the instructions for your
// The libmongocrypt C library is required when using client-side encryption. Specific versions of libmongocrypt
// are required for different versions of the Go Driver:
// - Go Driver v1.2.0 requires libmongocrypt v1.0.0 or higher
// - Go Driver v1.5.0 requires libmongocrypt v1.1.0 or higher
// - Go Driver v1.8.0 requires libmongocrypt v1.3.0 or higher
// - Go Driver v1.10.0 requires libmongocrypt v1.5.0 or higher
//
// To install libmongocrypt, follow the instructions for your
// operating system:
//
// 1. Linux: follow the instructions listed at
Expand Down
183 changes: 183 additions & 0 deletions mongo/integration/client_side_encryption_prose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,189 @@ func TestClientSideEncryptionProse(t *testing.T) {
},
}

runOpts := mtest.NewOptions().MinServerVersion("6.0").Topologies(mtest.ReplicaSet, mtest.LoadBalanced, mtest.ShardedReplicaSet)
mt.RunOpts("explicit encryption", runOpts, func(mt *mtest.T) {
// Test Setup ... begin
encryptedFields := readJSONFile(mt, "encrypted-fields.json")
key1Document := readJSONFile(mt, "key1-document.json")
var key1ID primitive.Binary
{
subtype, data := key1Document.Lookup("_id").Binary()
key1ID = primitive.Binary{Subtype: subtype, Data: data}
}

testSetup := func() (*mongo.Client, *mongo.ClientEncryption) {
mtest.DropEncryptedCollection(mt, mt.Client.Database("db").Collection("explicit_encryption"), encryptedFields)
cco := options.CreateCollection().SetEncryptedFields(encryptedFields)
err := mt.Client.Database("db").CreateCollection(context.Background(), "explicit_encryption", cco)
assert.Nil(mt, err, "error on CreateCollection: %v", err)
err = mt.Client.Database("keyvault").Collection("datakeys").Drop(context.Background())
assert.Nil(mt, err, "error on Drop: %v", err)
keyVaultClient, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(mtest.ClusterURI()))
assert.Nil(mt, err, "error on Connect: %v", err)
datakeysColl := keyVaultClient.Database("keyvault").Collection("datakeys", options.Collection().SetWriteConcern(mtest.MajorityWc))
_, err = datakeysColl.InsertOne(context.TODO(), key1Document)
assert.Nil(mt, err, "error on InsertOne: %v", err)
// Create a ClientEncryption.
ceo := options.ClientEncryption().
SetKeyVaultNamespace("keyvault.datakeys").
SetKmsProviders(fullKmsProvidersMap)
clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo)
assert.Nil(mt, err, "error on NewClientEncryption: %v", err)

// Create a MongoClient with AutoEncryptionOpts and bypassQueryAnalysis=true.
aeo := options.AutoEncryption().
SetKeyVaultNamespace("keyvault.datakeys").
SetKmsProviders(fullKmsProvidersMap).
SetBypassQueryAnalysis(true)
co := options.Client().SetAutoEncryptionOptions(aeo).ApplyURI(mtest.ClusterURI())
encryptedClient, err := mongo.Connect(context.Background(), co)
assert.Nil(mt, err, "error on Connect: %v", err)
return encryptedClient, clientEncryption
}
// Test Setup ... end

mt.Run("case 1: can insert encrypted indexed and find", func(mt *mtest.T) {
encryptedClient, clientEncryption := testSetup()
defer clientEncryption.Close(context.Background())
defer encryptedClient.Disconnect(context.Background())

// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID)
valueToEncrypt := "encrypted indexed value"
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Insert.
coll := encryptedClient.Database("db").Collection("explicit_encryption")
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", 1}, {"encryptedIndexed", insertPayload}})
assert.Nil(mt, err, "Error in InsertOne: %v", err)
// Explicit encrypt an indexed value to find.
eo = options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality)
findPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Find.
res := coll.FindOne(context.Background(), bson.D{{"encryptedIndexed", findPayload}})
assert.Nil(mt, res.Err(), "Error in FindOne: %v", res.Err())
got, err := res.DecodeBytes()
assert.Nil(mt, err, "error in DecodeBytes: %v", err)
gotValue, err := got.LookupErr("encryptedIndexed")
assert.Nil(mt, err, "error in LookupErr: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
})
mt.Run("case 2: can insert encrypted indexed and find with non-zero contention", func(mt *mtest.T) {
encryptedClient, clientEncryption := testSetup()
defer clientEncryption.Close(context.Background())
defer encryptedClient.Disconnect(context.Background())

coll := encryptedClient.Database("db").Collection("explicit_encryption")
valueToEncrypt := "encrypted indexed value"
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}

for i := 0; i < 10; i++ {
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetContentionFactor(10)
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Insert.
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", i}, {"encryptedIndexed", insertPayload}})
assert.Nil(mt, err, "Error in InsertOne: %v", err)
}

// Explicit encrypt an indexed value to find with default contentionFactor 0.
{
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality)
findPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Find with contentionFactor=0.
cursor, err := coll.Find(context.Background(), bson.D{{"encryptedIndexed", findPayload}})
assert.Nil(mt, err, "error in Find: %v", err)
var got []bson.Raw
err = cursor.All(context.Background(), &got)
assert.Nil(mt, err, "error in All: %v", err)
assert.True(mt, len(got) < 10, "expected len(got) < 10, got: %v", len(got))
for _, doc := range got {
gotValue, err := doc.LookupErr("encryptedIndexed")
assert.Nil(mt, err, "error in LookupErr: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
}
}

// Explicit encrypt an indexed value to find with contentionFactor 10.
{
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality).SetContentionFactor(10)
findPayload2, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Find with contentionFactor=10.
cursor, err := coll.Find(context.Background(), bson.D{{"encryptedIndexed", findPayload2}})
assert.Nil(mt, err, "error in Find: %v", err)
var got []bson.Raw
err = cursor.All(context.Background(), &got)
assert.Nil(mt, err, "error in All: %v", err)
assert.True(mt, len(got) == 10, "expected len(got) == 10, got: %v", len(got))
for _, doc := range got {
gotValue, err := doc.LookupErr("encryptedIndexed")
assert.Nil(mt, err, "error in LookupErr: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
}
}
})
mt.Run("case 3: can insert encrypted unindexed", func(mt *mtest.T) {
encryptedClient, clientEncryption := testSetup()
defer clientEncryption.Close(context.Background())
defer encryptedClient.Disconnect(context.Background())

// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
eo := options.Encrypt().SetAlgorithm("Unindexed").SetKeyID(key1ID)
valueToEncrypt := "encrypted unindexed value"
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
// Insert.
coll := encryptedClient.Database("db").Collection("explicit_encryption")
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", 1}, {"encryptedUnindexed", insertPayload}})
assert.Nil(mt, err, "Error in InsertOne: %v", err)
// Find.
res := coll.FindOne(context.Background(), bson.D{{"_id", 1}})
assert.Nil(mt, res.Err(), "Error in FindOne: %v", res.Err())
got, err := res.DecodeBytes()
assert.Nil(mt, err, "error in DecodeBytes: %v", err)
gotValue, err := got.LookupErr("encryptedUnindexed")
assert.Nil(mt, err, "error in LookupErr: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
})
mt.Run("case 4: can roundtrip encrypted indexed", func(mt *mtest.T) {
encryptedClient, clientEncryption := testSetup()
defer clientEncryption.Close(context.Background())
defer encryptedClient.Disconnect(context.Background())

// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID)
valueToEncrypt := "encrypted indexed value"
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
payload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
gotValue, err := clientEncryption.Decrypt(context.Background(), payload)
assert.Nil(mt, err, "error in Decrypt: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
})
mt.Run("case 5: can roundtrip encrypted unindexed", func(mt *mtest.T) {
encryptedClient, clientEncryption := testSetup()
defer clientEncryption.Close(context.Background())
defer encryptedClient.Disconnect(context.Background())

// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
eo := options.Encrypt().SetAlgorithm("Unindexed").SetKeyID(key1ID)
valueToEncrypt := "encrypted unindexed value"
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
payload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
assert.Nil(mt, err, "error in Encrypt: %v", err)
gotValue, err := clientEncryption.Decrypt(context.Background(), payload)
assert.Nil(mt, err, "error in Decrypt: %v", err)
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
})
})

mt.RunOpts("data key and double encryption", noClientOpts, func(mt *mtest.T) {
// set up options structs
schema := bson.D{
Expand Down
6 changes: 3 additions & 3 deletions mongo/integration/mtest/mongotest.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,9 @@ func (t *T) CreateCollection(coll Collection, createOnServer bool) *mongo.Collec
return coll.created
}

// dropEncryptedCollection drops a collection with EncryptedFields.
// DropEncryptedCollection drops a collection with EncryptedFields.
// The EncryptedFields option is not supported in Collection.Drop(). See GODRIVER-2413.
func dropEncryptedCollection(t *T, coll *mongo.Collection, encryptedFields interface{}) {
func DropEncryptedCollection(t *T, coll *mongo.Collection, encryptedFields interface{}) {
t.Helper()

var efBSON bsoncore.Document
Expand Down Expand Up @@ -506,7 +506,7 @@ func (t *T) ClearCollections() {
if !testContext.dataLake {
for _, coll := range t.createdColls {
if coll.CreateOpts != nil && coll.CreateOpts.EncryptedFields != nil {
dropEncryptedCollection(t, coll.created, coll.CreateOpts.EncryptedFields)
DropEncryptedCollection(t, coll.created, coll.CreateOpts.EncryptedFields)
}
_ = coll.created.Drop(context.Background())
}
Expand Down
42 changes: 37 additions & 5 deletions mongo/options/encryptoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ import (
"go.mongodb.org/mongo-driver/bson/primitive"
)

// QueryType describes the type of query the result of Encrypt is used for.
type QueryType int

// These constants specify valid values for QueryType
const (
QueryTypeEquality QueryType = 1
)

// EncryptOptions represents options to explicitly encrypt a value.
type EncryptOptions struct {
KeyID *primitive.Binary
KeyAltName *string
Algorithm string
KeyID *primitive.Binary
KeyAltName *string
Algorithm string
QueryType *QueryType
ContentionFactor *int64
}

// Encrypt creates a new EncryptOptions instance.
Expand All @@ -34,13 +44,29 @@ func (e *EncryptOptions) SetKeyAltName(keyAltName string) *EncryptOptions {
return e
}

// SetAlgorithm specifies an algorithm to use for encryption. This should be AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic
// or AEAD_AES_256_CBC_HMAC_SHA_512-Random. This is required.
// SetAlgorithm specifies an algorithm to use for encryption. This should be one of the following:
// - AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic
// - AEAD_AES_256_CBC_HMAC_SHA_512-Random
// - Indexed
// - Unindexed
// This is required.
func (e *EncryptOptions) SetAlgorithm(algorithm string) *EncryptOptions {
e.Algorithm = algorithm
return e
}

// SetQueryType specifies the intended query type. It is only valid to set if algorithm is "Indexed".
func (e *EncryptOptions) SetQueryType(queryType QueryType) *EncryptOptions {
e.QueryType = &queryType
return e
}

// SetContentionFactor specifies the contention factor. It is only valid to set if algorithm is "Indexed".
func (e *EncryptOptions) SetContentionFactor(contentionFactor int64) *EncryptOptions {
e.ContentionFactor = &contentionFactor
return e
}

// MergeEncryptOptions combines the argued EncryptOptions in a last-one wins fashion.
func MergeEncryptOptions(opts ...*EncryptOptions) *EncryptOptions {
eo := Encrypt()
Expand All @@ -58,6 +84,12 @@ func MergeEncryptOptions(opts ...*EncryptOptions) *EncryptOptions {
if opt.Algorithm != "" {
eo.Algorithm = opt.Algorithm
}
if opt.QueryType != nil {
eo.QueryType = opt.QueryType
}
if opt.ContentionFactor != nil {
eo.ContentionFactor = opt.ContentionFactor
}
}

return eo
Expand Down
Loading

0 comments on commit 62908c2

Please sign in to comment.