Skip to content

Commit 62908c2

Browse files
authored
GODRIVER-2400 Add FLE 2 API to explicit encryption (#960)
* add QueryType, ContentionFactor, and new algorithm values * add Explicit Encryption prose test * document requirement of libmongocrypt 1.5.0
1 parent bc14c6e commit 62908c2

File tree

9 files changed

+364
-15
lines changed

9 files changed

+364
-15
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"fields": [
3+
{
4+
"keyId": {
5+
"$binary": {
6+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
7+
"subType": "04"
8+
}
9+
},
10+
"path": "encryptedIndexed",
11+
"bsonType": "string",
12+
"queries": {
13+
"queryType": "equality",
14+
"contention": {
15+
"$numberLong": "0"
16+
}
17+
}
18+
},
19+
{
20+
"keyId": {
21+
"$binary": {
22+
"base64": "q83vqxI0mHYSNBI0VniQEg==",
23+
"subType": "04"
24+
}
25+
},
26+
"path": "encryptedUnindexed",
27+
"bsonType": "string"
28+
}
29+
]
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"_id": {
3+
"$binary": {
4+
"base64": "EjRWeBI0mHYSNBI0VniQEg==",
5+
"subType": "04"
6+
}
7+
},
8+
"keyMaterial": {
9+
"$binary": {
10+
"base64": "sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==",
11+
"subType": "00"
12+
}
13+
},
14+
"creationDate": {
15+
"$date": {
16+
"$numberLong": "1648914851981"
17+
}
18+
},
19+
"updateDate": {
20+
"$date": {
21+
"$numberLong": "1648914851981"
22+
}
23+
},
24+
"status": {
25+
"$numberInt": "0"
26+
},
27+
"masterKey": {
28+
"provider": "local"
29+
}
30+
}

mongo/client_encryption.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ func (ce *ClientEncryption) Encrypt(ctx context.Context, val bson.RawValue, opts
105105
transformed.SetKeyAltName(*eo.KeyAltName)
106106
}
107107
transformed.SetAlgorithm(eo.Algorithm)
108+
if eo.QueryType != nil {
109+
switch *eo.QueryType {
110+
case options.QueryTypeEquality:
111+
transformed.SetQueryType(cryptOpts.QueryTypeEquality)
112+
default:
113+
return primitive.Binary{}, fmt.Errorf("unsupported value for QueryType: %v", *eo.QueryType)
114+
}
115+
}
116+
117+
if eo.ContentionFactor != nil {
118+
transformed.SetContentionFactor(*eo.ContentionFactor)
119+
}
108120

109121
subtype, data, err := ce.crypt.EncryptExplicit(ctx, bsoncore.Value{Type: val.Type, Data: val.Value}, transformed)
110122
if err != nil {

mongo/doc.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,14 @@
105105
//
106106
// Note: Auto encryption is an enterprise-only feature.
107107
//
108-
// The libmongocrypt C library is required when using client-side encryption. libmongocrypt version 1.3.0 or higher is
109-
// required when using driver version 1.8.0 or higher. To install libmongocrypt, follow the instructions for your
108+
// The libmongocrypt C library is required when using client-side encryption. Specific versions of libmongocrypt
109+
// are required for different versions of the Go Driver:
110+
// - Go Driver v1.2.0 requires libmongocrypt v1.0.0 or higher
111+
// - Go Driver v1.5.0 requires libmongocrypt v1.1.0 or higher
112+
// - Go Driver v1.8.0 requires libmongocrypt v1.3.0 or higher
113+
// - Go Driver v1.10.0 requires libmongocrypt v1.5.0 or higher
114+
//
115+
// To install libmongocrypt, follow the instructions for your
110116
// operating system:
111117
//
112118
// 1. Linux: follow the instructions listed at

mongo/integration/client_side_encryption_prose_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,189 @@ func TestClientSideEncryptionProse(t *testing.T) {
7676
},
7777
}
7878

79+
runOpts := mtest.NewOptions().MinServerVersion("6.0").Topologies(mtest.ReplicaSet, mtest.LoadBalanced, mtest.ShardedReplicaSet)
80+
mt.RunOpts("explicit encryption", runOpts, func(mt *mtest.T) {
81+
// Test Setup ... begin
82+
encryptedFields := readJSONFile(mt, "encrypted-fields.json")
83+
key1Document := readJSONFile(mt, "key1-document.json")
84+
var key1ID primitive.Binary
85+
{
86+
subtype, data := key1Document.Lookup("_id").Binary()
87+
key1ID = primitive.Binary{Subtype: subtype, Data: data}
88+
}
89+
90+
testSetup := func() (*mongo.Client, *mongo.ClientEncryption) {
91+
mtest.DropEncryptedCollection(mt, mt.Client.Database("db").Collection("explicit_encryption"), encryptedFields)
92+
cco := options.CreateCollection().SetEncryptedFields(encryptedFields)
93+
err := mt.Client.Database("db").CreateCollection(context.Background(), "explicit_encryption", cco)
94+
assert.Nil(mt, err, "error on CreateCollection: %v", err)
95+
err = mt.Client.Database("keyvault").Collection("datakeys").Drop(context.Background())
96+
assert.Nil(mt, err, "error on Drop: %v", err)
97+
keyVaultClient, err := mongo.Connect(context.TODO(), options.Client().ApplyURI(mtest.ClusterURI()))
98+
assert.Nil(mt, err, "error on Connect: %v", err)
99+
datakeysColl := keyVaultClient.Database("keyvault").Collection("datakeys", options.Collection().SetWriteConcern(mtest.MajorityWc))
100+
_, err = datakeysColl.InsertOne(context.TODO(), key1Document)
101+
assert.Nil(mt, err, "error on InsertOne: %v", err)
102+
// Create a ClientEncryption.
103+
ceo := options.ClientEncryption().
104+
SetKeyVaultNamespace("keyvault.datakeys").
105+
SetKmsProviders(fullKmsProvidersMap)
106+
clientEncryption, err := mongo.NewClientEncryption(keyVaultClient, ceo)
107+
assert.Nil(mt, err, "error on NewClientEncryption: %v", err)
108+
109+
// Create a MongoClient with AutoEncryptionOpts and bypassQueryAnalysis=true.
110+
aeo := options.AutoEncryption().
111+
SetKeyVaultNamespace("keyvault.datakeys").
112+
SetKmsProviders(fullKmsProvidersMap).
113+
SetBypassQueryAnalysis(true)
114+
co := options.Client().SetAutoEncryptionOptions(aeo).ApplyURI(mtest.ClusterURI())
115+
encryptedClient, err := mongo.Connect(context.Background(), co)
116+
assert.Nil(mt, err, "error on Connect: %v", err)
117+
return encryptedClient, clientEncryption
118+
}
119+
// Test Setup ... end
120+
121+
mt.Run("case 1: can insert encrypted indexed and find", func(mt *mtest.T) {
122+
encryptedClient, clientEncryption := testSetup()
123+
defer clientEncryption.Close(context.Background())
124+
defer encryptedClient.Disconnect(context.Background())
125+
126+
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
127+
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID)
128+
valueToEncrypt := "encrypted indexed value"
129+
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
130+
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
131+
assert.Nil(mt, err, "error in Encrypt: %v", err)
132+
// Insert.
133+
coll := encryptedClient.Database("db").Collection("explicit_encryption")
134+
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", 1}, {"encryptedIndexed", insertPayload}})
135+
assert.Nil(mt, err, "Error in InsertOne: %v", err)
136+
// Explicit encrypt an indexed value to find.
137+
eo = options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality)
138+
findPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
139+
assert.Nil(mt, err, "error in Encrypt: %v", err)
140+
// Find.
141+
res := coll.FindOne(context.Background(), bson.D{{"encryptedIndexed", findPayload}})
142+
assert.Nil(mt, res.Err(), "Error in FindOne: %v", res.Err())
143+
got, err := res.DecodeBytes()
144+
assert.Nil(mt, err, "error in DecodeBytes: %v", err)
145+
gotValue, err := got.LookupErr("encryptedIndexed")
146+
assert.Nil(mt, err, "error in LookupErr: %v", err)
147+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
148+
})
149+
mt.Run("case 2: can insert encrypted indexed and find with non-zero contention", func(mt *mtest.T) {
150+
encryptedClient, clientEncryption := testSetup()
151+
defer clientEncryption.Close(context.Background())
152+
defer encryptedClient.Disconnect(context.Background())
153+
154+
coll := encryptedClient.Database("db").Collection("explicit_encryption")
155+
valueToEncrypt := "encrypted indexed value"
156+
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
157+
158+
for i := 0; i < 10; i++ {
159+
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
160+
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetContentionFactor(10)
161+
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
162+
assert.Nil(mt, err, "error in Encrypt: %v", err)
163+
// Insert.
164+
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", i}, {"encryptedIndexed", insertPayload}})
165+
assert.Nil(mt, err, "Error in InsertOne: %v", err)
166+
}
167+
168+
// Explicit encrypt an indexed value to find with default contentionFactor 0.
169+
{
170+
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality)
171+
findPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
172+
assert.Nil(mt, err, "error in Encrypt: %v", err)
173+
// Find with contentionFactor=0.
174+
cursor, err := coll.Find(context.Background(), bson.D{{"encryptedIndexed", findPayload}})
175+
assert.Nil(mt, err, "error in Find: %v", err)
176+
var got []bson.Raw
177+
err = cursor.All(context.Background(), &got)
178+
assert.Nil(mt, err, "error in All: %v", err)
179+
assert.True(mt, len(got) < 10, "expected len(got) < 10, got: %v", len(got))
180+
for _, doc := range got {
181+
gotValue, err := doc.LookupErr("encryptedIndexed")
182+
assert.Nil(mt, err, "error in LookupErr: %v", err)
183+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
184+
}
185+
}
186+
187+
// Explicit encrypt an indexed value to find with contentionFactor 10.
188+
{
189+
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID).SetQueryType(options.QueryTypeEquality).SetContentionFactor(10)
190+
findPayload2, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
191+
assert.Nil(mt, err, "error in Encrypt: %v", err)
192+
// Find with contentionFactor=10.
193+
cursor, err := coll.Find(context.Background(), bson.D{{"encryptedIndexed", findPayload2}})
194+
assert.Nil(mt, err, "error in Find: %v", err)
195+
var got []bson.Raw
196+
err = cursor.All(context.Background(), &got)
197+
assert.Nil(mt, err, "error in All: %v", err)
198+
assert.True(mt, len(got) == 10, "expected len(got) == 10, got: %v", len(got))
199+
for _, doc := range got {
200+
gotValue, err := doc.LookupErr("encryptedIndexed")
201+
assert.Nil(mt, err, "error in LookupErr: %v", err)
202+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
203+
}
204+
}
205+
})
206+
mt.Run("case 3: can insert encrypted unindexed", func(mt *mtest.T) {
207+
encryptedClient, clientEncryption := testSetup()
208+
defer clientEncryption.Close(context.Background())
209+
defer encryptedClient.Disconnect(context.Background())
210+
211+
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
212+
eo := options.Encrypt().SetAlgorithm("Unindexed").SetKeyID(key1ID)
213+
valueToEncrypt := "encrypted unindexed value"
214+
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
215+
insertPayload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
216+
assert.Nil(mt, err, "error in Encrypt: %v", err)
217+
// Insert.
218+
coll := encryptedClient.Database("db").Collection("explicit_encryption")
219+
_, err = coll.InsertOne(context.Background(), bson.D{{"_id", 1}, {"encryptedUnindexed", insertPayload}})
220+
assert.Nil(mt, err, "Error in InsertOne: %v", err)
221+
// Find.
222+
res := coll.FindOne(context.Background(), bson.D{{"_id", 1}})
223+
assert.Nil(mt, res.Err(), "Error in FindOne: %v", res.Err())
224+
got, err := res.DecodeBytes()
225+
assert.Nil(mt, err, "error in DecodeBytes: %v", err)
226+
gotValue, err := got.LookupErr("encryptedUnindexed")
227+
assert.Nil(mt, err, "error in LookupErr: %v", err)
228+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
229+
})
230+
mt.Run("case 4: can roundtrip encrypted indexed", func(mt *mtest.T) {
231+
encryptedClient, clientEncryption := testSetup()
232+
defer clientEncryption.Close(context.Background())
233+
defer encryptedClient.Disconnect(context.Background())
234+
235+
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
236+
eo := options.Encrypt().SetAlgorithm("Indexed").SetKeyID(key1ID)
237+
valueToEncrypt := "encrypted indexed value"
238+
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
239+
payload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
240+
assert.Nil(mt, err, "error in Encrypt: %v", err)
241+
gotValue, err := clientEncryption.Decrypt(context.Background(), payload)
242+
assert.Nil(mt, err, "error in Decrypt: %v", err)
243+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
244+
})
245+
mt.Run("case 5: can roundtrip encrypted unindexed", func(mt *mtest.T) {
246+
encryptedClient, clientEncryption := testSetup()
247+
defer clientEncryption.Close(context.Background())
248+
defer encryptedClient.Disconnect(context.Background())
249+
250+
// Explicit encrypt the value "encrypted indexed value" with algorithm: "Indexed".
251+
eo := options.Encrypt().SetAlgorithm("Unindexed").SetKeyID(key1ID)
252+
valueToEncrypt := "encrypted unindexed value"
253+
rawVal := bson.RawValue{Type: bson.TypeString, Value: bsoncore.AppendString(nil, valueToEncrypt)}
254+
payload, err := clientEncryption.Encrypt(context.Background(), rawVal, eo)
255+
assert.Nil(mt, err, "error in Encrypt: %v", err)
256+
gotValue, err := clientEncryption.Decrypt(context.Background(), payload)
257+
assert.Nil(mt, err, "error in Decrypt: %v", err)
258+
assert.Equal(mt, gotValue.StringValue(), valueToEncrypt, "expected %q, got %q", valueToEncrypt, gotValue.StringValue())
259+
})
260+
})
261+
79262
mt.RunOpts("data key and double encryption", noClientOpts, func(mt *mtest.T) {
80263
// set up options structs
81264
schema := bson.D{

mongo/integration/mtest/mongotest.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,9 @@ func (t *T) CreateCollection(coll Collection, createOnServer bool) *mongo.Collec
467467
return coll.created
468468
}
469469

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

475475
var efBSON bsoncore.Document
@@ -506,7 +506,7 @@ func (t *T) ClearCollections() {
506506
if !testContext.dataLake {
507507
for _, coll := range t.createdColls {
508508
if coll.CreateOpts != nil && coll.CreateOpts.EncryptedFields != nil {
509-
dropEncryptedCollection(t, coll.created, coll.CreateOpts.EncryptedFields)
509+
DropEncryptedCollection(t, coll.created, coll.CreateOpts.EncryptedFields)
510510
}
511511
_ = coll.created.Drop(context.Background())
512512
}

mongo/options/encryptoptions.go

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,21 @@ import (
1010
"go.mongodb.org/mongo-driver/bson/primitive"
1111
)
1212

13+
// QueryType describes the type of query the result of Encrypt is used for.
14+
type QueryType int
15+
16+
// These constants specify valid values for QueryType
17+
const (
18+
QueryTypeEquality QueryType = 1
19+
)
20+
1321
// EncryptOptions represents options to explicitly encrypt a value.
1422
type EncryptOptions struct {
15-
KeyID *primitive.Binary
16-
KeyAltName *string
17-
Algorithm string
23+
KeyID *primitive.Binary
24+
KeyAltName *string
25+
Algorithm string
26+
QueryType *QueryType
27+
ContentionFactor *int64
1828
}
1929

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

37-
// SetAlgorithm specifies an algorithm to use for encryption. This should be AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic
38-
// or AEAD_AES_256_CBC_HMAC_SHA_512-Random. This is required.
47+
// SetAlgorithm specifies an algorithm to use for encryption. This should be one of the following:
48+
// - AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic
49+
// - AEAD_AES_256_CBC_HMAC_SHA_512-Random
50+
// - Indexed
51+
// - Unindexed
52+
// This is required.
3953
func (e *EncryptOptions) SetAlgorithm(algorithm string) *EncryptOptions {
4054
e.Algorithm = algorithm
4155
return e
4256
}
4357

58+
// SetQueryType specifies the intended query type. It is only valid to set if algorithm is "Indexed".
59+
func (e *EncryptOptions) SetQueryType(queryType QueryType) *EncryptOptions {
60+
e.QueryType = &queryType
61+
return e
62+
}
63+
64+
// SetContentionFactor specifies the contention factor. It is only valid to set if algorithm is "Indexed".
65+
func (e *EncryptOptions) SetContentionFactor(contentionFactor int64) *EncryptOptions {
66+
e.ContentionFactor = &contentionFactor
67+
return e
68+
}
69+
4470
// MergeEncryptOptions combines the argued EncryptOptions in a last-one wins fashion.
4571
func MergeEncryptOptions(opts ...*EncryptOptions) *EncryptOptions {
4672
eo := Encrypt()
@@ -58,6 +84,12 @@ func MergeEncryptOptions(opts ...*EncryptOptions) *EncryptOptions {
5884
if opt.Algorithm != "" {
5985
eo.Algorithm = opt.Algorithm
6086
}
87+
if opt.QueryType != nil {
88+
eo.QueryType = opt.QueryType
89+
}
90+
if opt.ContentionFactor != nil {
91+
eo.ContentionFactor = opt.ContentionFactor
92+
}
6193
}
6294

6395
return eo

0 commit comments

Comments
 (0)