diff --git a/build.gradle b/build.gradle index 86fe2ad12d4..543e6de19ce 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,6 @@ ext { zstdVersion = '1.5.5-3' awsSdkV2Version = '2.18.9' awsSdkV1Version = '1.12.337' - mongoCryptVersion = '1.11.0' projectReactorVersion = '2022.0.0' junitBomVersion = '5.10.2' logbackVersion = '1.3.14' diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index fedf0c72566..9ce5b944cb4 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -260,4 +260,10 @@ + + + + + + diff --git a/driver-benchmarks/build.gradle b/driver-benchmarks/build.gradle index 960674011eb..91d979cff68 100644 --- a/driver-benchmarks/build.gradle +++ b/driver-benchmarks/build.gradle @@ -31,6 +31,7 @@ sourceSets { dependencies { api project(':driver-sync') + api project(':mongodb-crypt') implementation "ch.qos.logback:logback-classic:$logbackVersion" } diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/BenchmarkSuite.java b/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/BenchmarkSuite.java index 08dce238b70..2260e0ed80a 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/BenchmarkSuite.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/benchmarks/BenchmarkSuite.java @@ -22,6 +22,8 @@ import com.mongodb.benchmark.framework.BenchmarkResultWriter; import com.mongodb.benchmark.framework.BenchmarkRunner; import com.mongodb.benchmark.framework.EvergreenBenchmarkResultWriter; +import com.mongodb.benchmark.framework.MongoCryptBenchmarkRunner; +import com.mongodb.benchmark.framework.MongocryptBecnhmarkResult; import org.bson.Document; import org.bson.codecs.Codec; @@ -56,6 +58,7 @@ public static void main(String[] args) throws Exception { private static void runBenchmarks() throws Exception { + runMongoCryptBenchMarks(); runBenchmark(new BsonEncodingBenchmark<>("Flat", "extended_bson/flat_bson.json", DOCUMENT_CODEC)); runBenchmark(new BsonEncodingBenchmark<>("Deep", "extended_bson/deep_bson.json", DOCUMENT_CODEC)); runBenchmark(new BsonEncodingBenchmark<>("Full", "extended_bson/full_bson.json", DOCUMENT_CODEC)); @@ -87,6 +90,17 @@ private static void runBenchmarks() runBenchmark(new GridFSMultiFileDownloadBenchmark()); } + private static void runMongoCryptBenchMarks() throws InterruptedException { + // This runner has been migrated from libmongocrypt as it is. + List results = new MongoCryptBenchmarkRunner().run(); + + for (BenchmarkResultWriter writer : WRITERS) { + for (MongocryptBecnhmarkResult result : results) { + writer.write(result); + } + } + } + private static void runBenchmark(final Benchmark benchmark) throws Exception { long startTime = System.currentTimeMillis(); BenchmarkResult benchmarkResult = new BenchmarkRunner(benchmark, NUM_WARMUP_ITERATIONS, NUM_ITERATIONS, MIN_TIME_SECONDS, diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/BenchmarkResultWriter.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/BenchmarkResultWriter.java index d7f4a4701ce..26828a5a75f 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/BenchmarkResultWriter.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/BenchmarkResultWriter.java @@ -21,4 +21,6 @@ public interface BenchmarkResultWriter extends Closeable { void write(BenchmarkResult benchmarkResult); + + void write(MongocryptBecnhmarkResult result); } diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/EvergreenBenchmarkResultWriter.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/EvergreenBenchmarkResultWriter.java index 719bf269163..f1e5361ffeb 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/EvergreenBenchmarkResultWriter.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/EvergreenBenchmarkResultWriter.java @@ -65,6 +65,32 @@ public void write(final BenchmarkResult benchmarkResult) { jsonWriter.writeEndDocument(); } + @Override + public void write(final MongocryptBecnhmarkResult result) { + jsonWriter.writeStartDocument(); + + jsonWriter.writeStartDocument("info"); + jsonWriter.writeString("test_name", result.getTestName()); + + jsonWriter.writeStartDocument("args"); + jsonWriter.writeInt32("threads", result.getThreadCount()); + jsonWriter.writeEndDocument(); + jsonWriter.writeEndDocument(); + + jsonWriter.writeString("created_at", result.getCreatedAt()); + jsonWriter.writeString("completed_at", result.getCompletedAt()); + jsonWriter.writeStartArray("metrics"); + + jsonWriter.writeStartDocument(); + jsonWriter.writeString("name", result.getMetricName()); + jsonWriter.writeString("type", result.getMetricType()); + jsonWriter.writeDouble("value", result.getMedianOpsPerSec()); + jsonWriter.writeEndDocument(); + + jsonWriter.writeEndArray(); + jsonWriter.writeEndDocument(); + } + @Override public void close() throws IOException { jsonWriter.writeEndArray(); diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MinimalTextBasedBenchmarkResultWriter.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MinimalTextBasedBenchmarkResultWriter.java index 73f85697f33..b5ed85f1f2e 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MinimalTextBasedBenchmarkResultWriter.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MinimalTextBasedBenchmarkResultWriter.java @@ -34,6 +34,12 @@ public void write(final BenchmarkResult benchmarkResult) { benchmarkResult.getElapsedTimeNanosAtPercentile(50) / ONE_BILLION); } + @Override + public void write(final MongocryptBecnhmarkResult result) { + printStream.printf("%s: %d%n", result.getTestName(), + result.getMedianOpsPerSec()); + } + @Override public void close() { } diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongoCryptBenchmarkRunner.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongoCryptBenchmarkRunner.java new file mode 100644 index 00000000000..33b6c0ad102 --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongoCryptBenchmarkRunner.java @@ -0,0 +1,224 @@ +package com.mongodb.benchmark.framework; + +/* + * Copyright 2023-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import com.mongodb.crypt.capi.CAPI; +import com.mongodb.crypt.capi.MongoCrypt; +import com.mongodb.crypt.capi.MongoCryptContext; +import com.mongodb.crypt.capi.MongoCryptOptions; +import com.mongodb.crypt.capi.MongoCrypts; +import com.mongodb.crypt.capi.MongoExplicitEncryptOptions; +import com.mongodb.crypt.capi.MongoLocalKmsProviderOptions; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.RawBsonDocument; + +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class MongoCryptBenchmarkRunner { + static final int NUM_FIELDS = 1500; + static final int NUM_WARMUP_SECS = 2; + static final int NUM_SECS = 10; + static final byte[] LOCAL_MASTER_KEY = new byte[]{ + -99, -108, 75, 13, -109, -48, -59, 68, -91, 114, -3, 50, 27, -108, 48, -112, 35, 53, + 115, 124, -16, -10, -62, -12, -38, 35, 86, -25, -113, 4, -52, -6, -34, 117, -76, 81, + -121, -13, -117, -105, -41, 75, 68, 59, -84, 57, -94, -58, 77, -111, 0, 62, -47, -6, 74, + 48, -63, -46, -58, 94, -5, -84, 65, -14, 72, 19, 60, -101, 80, -4, -89, 36, 122, 46, 2, + 99, -93, -58, 22, 37, 81, 80, 120, 62, 15, -40, 110, -124, -90, -20, -115, 45, 36, 71, + -27, -81 + }; + + private static String getFileAsString(final String fileName) { + try { + URL resource = BenchmarkRunner.class.getResource("/" + fileName); + if (resource == null) { + throw new RuntimeException("Could not find file " + fileName); + } + return new String(Files.readAllBytes(Paths.get(resource.toURI()))); + } catch (Throwable t) { + throw new RuntimeException("Could not parse file " + fileName, t); + } + } + + private static BsonDocument getResourceAsDocument(final String fileName) { + return BsonDocument.parse(getFileAsString(fileName)); + } + + private static MongoCrypt createMongoCrypt() { + return MongoCrypts.create(MongoCryptOptions + .builder() + .localKmsProviderOptions(MongoLocalKmsProviderOptions.builder() + .localMasterKey(ByteBuffer.wrap(LOCAL_MASTER_KEY)) + .build()) + .build()); + } + + // DecryptTask decrypts a document repeatedly for a specified number of seconds and records ops/sec. + private static class DecryptTask implements Runnable { + public DecryptTask(MongoCrypt mongoCrypt, BsonDocument toDecrypt, int numSecs, CountDownLatch doneSignal) { + this.mongoCrypt = mongoCrypt; + this.toDecrypt = toDecrypt; + this.opsPerSecs = new ArrayList(numSecs); + this.numSecs = numSecs; + this.doneSignal = doneSignal; + } + + public void run() { + for (int i = 0; i < numSecs; i++) { + long opsPerSec = 0; + long start = System.nanoTime(); + // Run for one second. + while (System.nanoTime() - start < 1_000_000_000) { + try (MongoCryptContext ctx = mongoCrypt.createDecryptionContext(toDecrypt)) { + assert ctx.getState() == MongoCryptContext.State.READY; + ctx.finish(); + opsPerSec++; + } + } + opsPerSecs.add(opsPerSec); + } + doneSignal.countDown(); + } + + public long getMedianOpsPerSecs() { + if (opsPerSecs.size() == 0) { + throw new IllegalStateException("opsPerSecs is empty. Was `run` called?"); + } + Collections.sort(opsPerSecs); + return opsPerSecs.get(numSecs / 2); + } + + private MongoCrypt mongoCrypt; + private BsonDocument toDecrypt; + private ArrayList opsPerSecs; + private int numSecs; + private CountDownLatch doneSignal; + } + + public List run() throws InterruptedException { + System.out.printf("BenchmarkRunner is using libmongocrypt version=%s, NUM_WARMUP_SECS=%d, NUM_SECS=%d%n", + CAPI.mongocrypt_version(null).toString(), NUM_WARMUP_SECS, NUM_SECS); + // `keyDocument` is a Data Encryption Key (DEK) encrypted with the Key Encryption Key (KEK) `LOCAL_MASTER_KEY`. + BsonDocument keyDocument = getResourceAsDocument("keyDocument.json"); + try (MongoCrypt mongoCrypt = createMongoCrypt()) { + // `encrypted` will contain encrypted fields. + BsonDocument encrypted = new BsonDocument(); + { + for (int i = 0; i < NUM_FIELDS; i++) { + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() + .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ=="))) + .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") + .build(); + BsonDocument toEncrypt = new BsonDocument("v", new BsonString(String.format("value %04d", i))); + try (MongoCryptContext ctx = mongoCrypt.createExplicitEncryptionContext(toEncrypt, options)) { + // If mongocrypt_t has not yet cached the DEK, supply it. + if (MongoCryptContext.State.NEED_MONGO_KEYS == ctx.getState()) { + ctx.addMongoOperationResult(keyDocument); + ctx.completeMongoOperation(); + } + assert ctx.getState() == MongoCryptContext.State.READY; + RawBsonDocument result = ctx.finish(); + BsonValue encryptedValue = result.get("v"); + String key = String.format("key%04d", i); + encrypted.append(key, encryptedValue); + } + } + } + + // Warm up benchmark and discard the result. + DecryptTask warmup = new DecryptTask(mongoCrypt, encrypted, NUM_WARMUP_SECS, new CountDownLatch(1)); + warmup.run(); + + // Decrypt `encrypted` and measure ops/sec. + // Check with varying thread counts to measure impact of a shared pool of Cipher instances. + int[] threadCounts = {1, 2, 8, 64}; + ArrayList totalMedianOpsPerSecs = new ArrayList(threadCounts.length); + ArrayList createdAts = new ArrayList(threadCounts.length); + ArrayList completedAts = new ArrayList(threadCounts.length); + + for (int threadCount : threadCounts) { + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch doneSignal = new CountDownLatch(threadCount); + ArrayList decryptTasks = new ArrayList(threadCount); + createdAts.add(ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)); + + for (int i = 0; i < threadCount; i++) { + DecryptTask decryptTask = new DecryptTask(mongoCrypt, encrypted, NUM_SECS, doneSignal); + decryptTasks.add(decryptTask); + executorService.submit(decryptTask); + } + + // Await completion of all tasks. Tasks are expected to complete shortly after NUM_SECS. Time out `await` if time exceeds 2 * NUM_SECS. + boolean ok = doneSignal.await(NUM_SECS * 2, TimeUnit.SECONDS); + assert ok; + completedAts.add(ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT)); + // Sum the median ops/secs of all tasks to get total throughput. + long totalMedianOpsPerSec = 0; + for (DecryptTask decryptTask : decryptTasks) { + totalMedianOpsPerSec += decryptTask.getMedianOpsPerSecs(); + } + System.out.printf("threadCount=%d. Decrypting 1500 fields median ops/sec : %d%n", threadCount, totalMedianOpsPerSec); + totalMedianOpsPerSecs.add(totalMedianOpsPerSec); + executorService.shutdown(); + ok = executorService.awaitTermination(NUM_SECS * 2, TimeUnit.SECONDS); + assert ok; + } + + // Print the results in JSON that can be accepted by the `perf.send` command. + // See https://docs.devprod.prod.corp.mongodb.com/evergreen/Project-Configuration/Project-Commands#perfsend for the expected `perf.send` input. + List results = new ArrayList<>(threadCounts.length); + for (int i = 0; i < threadCounts.length; i++) { + int threadCount = threadCounts[i]; + long totalMedianOpsPerSec = totalMedianOpsPerSecs.get(i); + String createdAt = createdAts.get(i); + String completedAt = completedAts.get(i); + + MongocryptBecnhmarkResult result = new MongocryptBecnhmarkResult( + "java_decrypt_1500", + threadCount, + totalMedianOpsPerSec, + createdAt, + completedAt, + "medianOpsPerSec", + "THROUGHPUT"); + + results.add(result); + } + System.out.println("Results: " + results); + return results; + } + } +} + diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongocryptBecnhmarkResult.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongocryptBecnhmarkResult.java new file mode 100644 index 00000000000..92ef999bee2 --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/MongocryptBecnhmarkResult.java @@ -0,0 +1,84 @@ +package com.mongodb.benchmark.framework; +/* + * Copyright 2016-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +public class MongocryptBecnhmarkResult { + private final String testName; + private final int threadCount; + private final long medianOpsPerSec; + private final String createdAt; + private final String completedAt; + private final String metricName; + private final String metricType; + + public MongocryptBecnhmarkResult(final String testName, + final int threadCount, + final long medianOpsPerSec, + final String createdAt, + final String completedAt, + final String metricName, + final String metricType) { + this.testName = testName; + this.threadCount = threadCount; + this.medianOpsPerSec = medianOpsPerSec; + this.createdAt = createdAt; + this.completedAt = completedAt; + this.metricName = metricName; + this.metricType = metricType; + } + + public String getTestName() { + return testName; + } + + public int getThreadCount() { + return threadCount; + } + + public long getMedianOpsPerSec() { + return medianOpsPerSec; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getCompletedAt() { + return completedAt; + } + + public String getMetricName() { + return metricName; + } + + public String getMetricType() { + return metricType; + } + + @Override + public String toString() { + return "MongocryptBecnhmarkResult{" + + "testName='" + testName + '\'' + + ", threadCount=" + threadCount + + ", medianOpsPerSec=" + medianOpsPerSec + + ", createdAt=" + createdAt + + ", completedAt=" + completedAt + + ", metricName=" + metricName + + ", metricType=" + metricType + + '}'; + } +} diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/TextBasedBenchmarkResultWriter.java b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/TextBasedBenchmarkResultWriter.java index 185e518c3a0..9a29c9bd621 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/framework/TextBasedBenchmarkResultWriter.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/framework/TextBasedBenchmarkResultWriter.java @@ -82,6 +82,20 @@ public void write(final BenchmarkResult benchmarkResult) { printStream.println(); } + @Override + public void write(final MongocryptBecnhmarkResult result) { + printStream.println(result.getTestName()); + + printStream.println("CreatedAt: " + result.getCreatedAt()); + printStream.println("CompletedAt: " + result.getCompletedAt()); + printStream.println("ThreadCount: " + result.getThreadCount()); + printStream.println("MedianOpsPerSec: " + result.getMedianOpsPerSec()); + printStream.println("MetricType: " + result.getMetricType()); + + printStream.println(); + printStream.println(); + } + @Override public void close() { } diff --git a/driver-benchmarks/src/resources/keyDocument.json b/driver-benchmarks/src/resources/keyDocument.json new file mode 100644 index 00000000000..20d631db86c --- /dev/null +++ b/driver-benchmarks/src/resources/keyDocument.json @@ -0,0 +1,24 @@ +{ + "_id": { + "$binary": { + "base64": "YWFhYWFhYWFhYWFhYWFhYQ==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "ACR7Hm33dDOAAD7l2ubZhSpSUWK8BkALUY+qW3UgBAEcTV8sBwZnaAWnzDsmrX55dgmYHWfynDlJogC/e33u6pbhyXvFTs5ow9OLCuCWBJ39T/Ivm3kMaZJybkejY0V+uc4UEdHvVVz/SbitVnzs2WXdMGmo1/HmDRrxGYZjewFslquv8wtUHF5pyB+QDlQBd/al9M444/8bJZFbMSmtIg==", + "subType": "00" + } + }, + "creationDate": { + "$date": "2023-08-21T14:28:20.875Z" + }, + "updateDate": { + "$date": "2023-08-21T14:28:20.875Z" + }, + "status": 0, + "masterKey": { + "provider": "local" + } +} \ No newline at end of file diff --git a/driver-core/build.gradle b/driver-core/build.gradle index 1f7d06f93f2..78ab607cc23 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation project(path: ':bson-record-codec', configuration: 'default') implementation project(path: ':bson-kotlin', configuration: 'default'), optional implementation project(path: ':bson-kotlinx', configuration: 'default'), optional + implementation project(path: ':mongodb-crypt', configuration: 'default'), optional implementation "com.github.jnr:jnr-unixsocket:$jnrUnixsocketVersion", optional api platform("io.netty:netty-bom:$nettyVersion") @@ -55,7 +56,6 @@ dependencies { implementation "org.xerial.snappy:snappy-java:$snappyVersion", optional implementation "com.github.luben:zstd-jni:$zstdVersion", optional - implementation "org.mongodb:mongodb-crypt:$mongoCryptVersion", optional testImplementation project(':bson').sourceSets.test.output testImplementation('org.junit.jupiter:junit-jupiter-api') diff --git a/graalvm-native-image-app/build.gradle b/graalvm-native-image-app/build.gradle index c34d8623b15..d6bc5a7b6cb 100644 --- a/graalvm-native-image-app/build.gradle +++ b/graalvm-native-image-app/build.gradle @@ -82,12 +82,13 @@ dependencies { implementation project(path:':driver-sync', configuration:'archives') implementation project(path:':driver-reactive-streams', configuration:'archives') implementation project(path:':driver-legacy', configuration:'archives') + implementation project(path: ':mongodb-crypt', configuration: 'archives') + implementation project(path: ':mongodb-crypt', configuration: 'runtimeElements') // note that as a result of these `sourceSets` dependencies, `driver-sync/src/test/resources/logback-test.xml` is used implementation project(':driver-core').sourceSets.test.output implementation project(':driver-sync').sourceSets.test.output implementation project(':driver-legacy').sourceSets.test.output implementation project(':driver-reactive-streams').sourceSets.test.output - implementation "org.mongodb:mongodb-crypt:$mongoCryptVersion" implementation 'org.slf4j:slf4j-api:2.0.12' implementation "ch.qos.logback:logback-classic:$logbackVersion" implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion") diff --git a/gradle/publish.gradle b/gradle/publish.gradle index a89b20845a3..f72773c5ad7 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -105,6 +105,9 @@ configure(javaProjects) { project -> suppressPomMetadataWarningsFor("jsonSupportApiElements") suppressPomMetadataWarningsFor("jsonSupportRuntimeElements") + + suppressPomMetadataWarningsFor("mongoCryptSupportApiElements") + suppressPomMetadataWarningsFor("mongoCryptSupportRuntimeElements") } } diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts new file mode 100644 index 00000000000..bf2fef544ff --- /dev/null +++ b/mongodb-crypt/build.gradle.kts @@ -0,0 +1,187 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import de.undercouch.gradle.tasks.download.Download + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + "classpath"(group = "net.java.dev.jna", name = "jna", version = "5.11.0") + } +} + +plugins { + // Needed to download libmongocrypt from s3. + id("de.undercouch.download") version "5.6.0" +} + +group = "org.mongodb" +base.archivesName.set("mongodb-crypt") +description = "MongoDB client-side crypto support" +ext.set("pomName", "MongoCrypt") + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + api(project(path = ":bson", configuration = "default")) + api("net.java.dev.jna:jna:5.11.0") + + // Tests + testImplementation("org.junit.jupiter:junit-jupiter") +} + +/* + * Jna copy or download resources + */ +val jnaDownloadsDir = "$buildDir/jnaLibs/downloads/" +val jnaResourcesDir = "$buildDir/jnaLibs/resources/" +val jnaLibPlatform: String = if (com.sun.jna.Platform.RESOURCE_PREFIX.startsWith("darwin")) "darwin" else com.sun.jna.Platform.RESOURCE_PREFIX +val jnaLibsPath: String = System.getProperty("jnaLibsPath", "${jnaResourcesDir}${jnaLibPlatform}") +val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) + +// Download jnaLibs that match the git tag or revision to jnaResourcesBuildDir +val downloadRevision = "9a88ac5698e8e3ffcd6580b98c247f0126f26c40" // r1.11.0 +val binariesArchiveName = "libmongocrypt-java.tar.gz" + +/** + * The name of the archive includes downloadRevision to ensure that: + * - the archive is downloaded if the revision changes. + * - the archive is not downloaded if the revision is the same and archive had already been saved in build output. + */ +val localBinariesArchiveName = "libmongocrypt-java-$downloadRevision.tar.gz" + +val downloadUrl: String = "https://mciuploads.s3.amazonaws.com/libmongocrypt/java/$downloadRevision/$binariesArchiveName" + +val jnaMapping: Map = mapOf( + "rhel-62-64-bit" to "linux-x86-64", + "rhel72-zseries-test" to "linux-s390x", + "rhel-71-ppc64el" to "linux-ppc64le", + "ubuntu1604-arm64" to "linux-aarch64", + "windows-test" to "win32-x86-64", + "macos" to "darwin" +) + +sourceSets { + main { + java { + resources { + srcDirs(jnaResourcesDir) + } + } + } +} + +tasks.register("downloadJava") { + src(downloadUrl) + dest("${jnaDownloadsDir}/$localBinariesArchiveName") + overwrite(true) + /* To make sure we don't download archive with binaries if it hasn't been changed in S3 bucket since last download.*/ + onlyIfModified(true) +} + +tasks.register("unzipJava") { + /* + Clean up the directory first if the task is not UP-TO-DATE. + This can happen if the download revision has been changed and the archive is downloaded again. + */ + doFirst { + println("Cleaning up $jnaResourcesDir") + delete(jnaResourcesDir) + } + from(tarTree(resources.gzip("${jnaDownloadsDir}/$localBinariesArchiveName"))) + include(jnaMapping.keys.flatMap { + listOf("${it}/nocrypto/**/libmongocrypt.so", "${it}/lib/**/libmongocrypt.dylib", "${it}/bin/**/mongocrypt.dll" ) + }) + eachFile { + path = "${jnaMapping[path.substringBefore("/")]}/${name}" + } + into(jnaResourcesDir) + dependsOn("downloadJava") + + doLast { + println("jna.library.path contents: \n ${fileTree(jnaResourcesDir).files.joinToString(",\n ")}") + } +} + +// The `processResources` task (defined by the `java-library` plug-in) consumes files in the main source set. +// Add a dependency on `unzipJava`. `unzipJava` adds libmongocrypt libraries to the main source set. +tasks.processResources { + mustRunAfter(tasks.named("unzipJava")) +} + +tasks.register("downloadJnaLibs") { + dependsOn("downloadJava", "unzipJava") +} + +tasks.test { + systemProperty("jna.debug_load", "true") + systemProperty("jna.library.path", jnaResources) + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + + doFirst { + println("jna.library.path contents:") + println(fileTree(jnaResources) { + this.setIncludes(listOf("*.*")) + }.files.joinToString(",\n ", " ")) + } + dependsOn("downloadJnaLibs", "downloadJava", "unzipJava") +} + +tasks.withType { + description = """$description + | System properties: + | ================= + | + | jnaLibsPath : Custom local JNA library path for inclusion into the build (rather than downloading from s3) + | gitRevision : Optional Git Revision to download the built resources for from s3. + """.trimMargin() +} + +tasks.jar { + //NOTE this enables depending on the mongocrypt from driver-core + dependsOn("downloadJnaLibs") +} + +tasks.javadoc { + if (JavaVersion.current().isJava9Compatible) { + (options as StandardJavadocDocletOptions).addBooleanOption("html5", true) + } +} + +afterEvaluate { + tasks.jar { + manifest { + attributes( + "-exportcontents" to "com.mongodb.crypt.capi.*;-noimport:=true", + "Automatic-Module-Name" to "com.mongodb.crypt.capi", + "Import-Package" to "org.slf4j.*;resolution:=optional,org.bson.*", + "Bundle-Name" to "MongoCrypt", + "Bundle-SymbolicName" to "com.mongodb.crypt.capi", + "Private-Package" to "" + ) + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/BinaryHolder.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/BinaryHolder.java new file mode 100644 index 00000000000..60570bd1180 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/BinaryHolder.java @@ -0,0 +1,45 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; + +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_destroy; + +// Wrap JNA memory and a mongocrypt_binary_t that references that memory, in order to ensure that the JNA Memory is not GC'd before the +// mongocrypt_binary_t is destroyed +class BinaryHolder implements AutoCloseable { + + private final DisposableMemory memory; + private final mongocrypt_binary_t binary; + + BinaryHolder(final DisposableMemory memory, final mongocrypt_binary_t binary) { + this.memory = memory; + this.binary = binary; + } + + mongocrypt_binary_t getBinary() { + return binary; + } + + @Override + public void close() { + mongocrypt_binary_destroy(binary); + memory.dispose(); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPI.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPI.java new file mode 100644 index 00000000000..d6567bdaf7c --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPI.java @@ -0,0 +1,1165 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.sun.jna.Callback; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.PointerType; +import com.sun.jna.ptr.PointerByReference; + +//CHECKSTYLE:OFF + +/** + * For internal use only. Not part of the public API. + */ +@SuppressWarnings("WeakerAccess") +public class CAPI { + + public static class cstring extends PointerType { + public cstring() { + super(); + } + + public cstring(String string) { + Pointer m = new Memory(string.length() + 1); + m.setString(0, string); + setPointer(m); + } + + public String toString() { + return getPointer().getString(0); + } + } + + + /** + * Indicates success or contains error information. + *

+ * Functions like @ref mongocrypt_ctx_encrypt_init follow a pattern to expose a + * status. A boolean is returned. True indicates success, and false indicates + * failure. On failure a status on the handle is set, and is accessible with a + * corresponding status function. E.g. @ref mongocrypt_ctx_status. + */ + public static class mongocrypt_status_t extends PointerType { + } + + /** + * Contains all options passed on initialization of a @ref mongocrypt_ctx_t. + */ + public static class mongocrypt_opts_t extends PointerType { + } + + /** + * A non-owning view of a byte buffer. + *

+ * Functions returning a mongocrypt_binary_t* expect it to be destroyed with + * mongocrypt_binary_destroy. + */ + public static class mongocrypt_binary_t extends PointerType { + // The `mongocrypt_binary_t` struct layout is part of libmongocrypt's ABI: + // typedef struct _mongocrypt_binary_t { + // void *data; + // uint32_t len; + // } mongocrypt_binary_t; + // To improve performance, fields are read directly using `getPointer` and `getInt`. + // This results in observed performance improvements over using of `mongocrypt_binary_data` and `mongocrypt_binary_len`. Refer: MONGOCRYPT-589. + public mongocrypt_binary_t() { + super(); + } + public Pointer data() { + return this.getPointer().getPointer(0); + } + public int len() { + int len = this.getPointer().getInt(Native.POINTER_SIZE); + // mongocrypt_binary_t represents length as an unsigned `uint32_t`. + // Representing `uint32_t` values greater than INT32_MAX is represented as a negative `int`. + // Throw an exception. mongocrypt_binary_t is not expected to use lengths greater than INT32_MAX. + if (len < 0) { + throw new AssertionError( + String.format("Expected mongocrypt_binary_t length to be non-negative, got: %d", len)); + } + return len; + + } + } + + /** + * The top-level handle to libmongocrypt. + *

+ * Create a mongocrypt_t handle to perform operations within libmongocrypt: + * encryption, decryption, registering log callbacks, etc. + *

+ * Functions on a mongocrypt_t are thread safe, though functions on derived + * handle (e.g. mongocrypt_encryptor_t) are not and must be owned by a single + * thread. See each handle's documentation for thread-safety considerations. + *

+ * Multiple mongocrypt_t handles may be created. + */ + public static class mongocrypt_t extends PointerType { + } + + /** + * Manages the state machine for encryption or decryption. + */ + public static class mongocrypt_ctx_t extends PointerType { + } + + /** + * Manages a single KMS HTTP request/response. + */ + public static class mongocrypt_kms_ctx_t extends PointerType { + } + + /** + * Returns the version string x.y.z for libmongocrypt. + * + * @param len an optional length of the returned string. May be NULL. + * @return the version string x.y.z for libmongocrypt. + */ + public static native cstring + mongocrypt_version(Pointer len); + + + /** + * Create a new non-owning view of a buffer (data + length). + *

+ * Use this to create a mongocrypt_binary_t used for output parameters. + * + * @return A new mongocrypt_binary_t. + */ + public static native mongocrypt_binary_t + mongocrypt_binary_new(); + + + /** + * Create a new non-owning view of a buffer (data + length). + * + * @param data A pointer to an array of bytes. This is not copied. data must outlive the binary object. + * @param len The length of the @p data byte array. + * @return A new mongocrypt_binary_t. + */ + public static native mongocrypt_binary_t + mongocrypt_binary_new_from_data(Pointer data, int len); + + + /** + * Get a pointer to the referenced data. + * + * @param binary The @ref mongocrypt_binary_t. + * @return A pointer to the referenced data. + */ + public static native Pointer + mongocrypt_binary_data(mongocrypt_binary_t binary); + + + /** + * Get the length of the referenced data. + * + * @param binary The @ref mongocrypt_binary_t. + * @return The length of the referenced data. + */ + public static native int + mongocrypt_binary_len(mongocrypt_binary_t binary); + + + /** + * Free the @ref mongocrypt_binary_t. + *

+ * This does not free the referenced data. Refer to individual function + * documentation to determine the lifetime guarantees of the underlying + * data. + * + * @param binary The mongocrypt_binary_t destroy. + */ + public static native void + mongocrypt_binary_destroy(mongocrypt_binary_t binary); + + + public static final int MONGOCRYPT_STATUS_OK = 0; + public static final int MONGOCRYPT_STATUS_ERROR_CLIENT = 1; + public static final int MONGOCRYPT_STATUS_ERROR_KMS = 2; + + /** + * Create a new status object. + *

+ * Use a new status object to retrieve the status from a handle by passing + * this as an out-parameter to functions like @ref mongocrypt_ctx_status. + * When done, destroy it with @ref mongocrypt_status_destroy. + * + * @return A new status object. + */ + public static native mongocrypt_status_t + mongocrypt_status_new(); + + /** + * Set a status object with message, type, and code. + *

+ * Use this to set the mongocrypt_status_t given in the crypto hooks. + * + * @param status The status. + * @param type The status type. + * @param code The status code. + * @param message The message. + * @param message_len The length of @p message. Pass -1 to determine the * string length with strlen (must * be NULL terminated). + */ + public static native void + mongocrypt_status_set(mongocrypt_status_t status, + int type, + int code, + cstring message, + int message_len); + + /** + * Indicates success or the type of error. + * + * @param status The status object. + * @return A @ref mongocrypt_status_type_t. + */ + + public static native int + mongocrypt_status_type(mongocrypt_status_t status); + + + /** + * Get an error code or 0. + * + * @param status The status object. + * @return An error code. + */ + public static native int + mongocrypt_status_code(mongocrypt_status_t status); + + + /** + * Get the error message associated with a status, or an empty string. + * + * @param status The status object. + * @param len an optional length of the returned string. May be NULL. + * @return An error message or an empty string. + */ + public static native cstring + mongocrypt_status_message(mongocrypt_status_t status, Pointer len); + + + /** + * Returns true if the status indicates success. + * + * @param status The status to check. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_status_ok(mongocrypt_status_t status); + + + /** + * Free the memory for a status object. + * + * @param status The status to destroy. + */ + public static native void + mongocrypt_status_destroy(mongocrypt_status_t status); + + + public static final int MONGOCRYPT_LOG_LEVEL_FATAL = 0; + public static final int MONGOCRYPT_LOG_LEVEL_ERROR = 1; + public static final int MONGOCRYPT_LOG_LEVEL_WARNING = 2; + public static final int MONGOCRYPT_LOG_LEVEL_INFO = 3; + public static final int MONGOCRYPT_LOG_LEVEL_TRACE = 4; + + + /** + * A log callback function. Set a custom log callback with mongocrypt_setopt_log_handler. + */ + public interface mongocrypt_log_fn_t extends Callback { + void log(int level, cstring message, int message_len, Pointer ctx); + } + + public interface mongocrypt_crypto_fn extends Callback { + boolean crypt(Pointer ctx, mongocrypt_binary_t key, mongocrypt_binary_t iv, mongocrypt_binary_t in, + mongocrypt_binary_t out, Pointer bytesWritten, mongocrypt_status_t status); + } + + public interface mongocrypt_hmac_fn extends Callback { + boolean hmac(Pointer ctx, mongocrypt_binary_t key, mongocrypt_binary_t in, mongocrypt_binary_t out, + mongocrypt_status_t status); + } + + public interface mongocrypt_hash_fn extends Callback { + boolean hash(Pointer ctx, mongocrypt_binary_t in, mongocrypt_binary_t out, mongocrypt_status_t status); + } + + public interface mongocrypt_random_fn extends Callback { + boolean random(Pointer ctx, mongocrypt_binary_t out, int count, mongocrypt_status_t status); + } + + /** + * Allocate a new @ref mongocrypt_t object. + *

+ * Initialize with @ref mongocrypt_init. When done, free with @ref + * mongocrypt_destroy. + * + * @return A new @ref mongocrypt_t object. + */ + public static native mongocrypt_t + mongocrypt_new(); + + /** + * Set a handler to get called on every log message. + * + * @param crypt The @ref mongocrypt_t object. + * @param log_fn The log callback. + * @param log_ctx A context passed as an argument to the log callback every + * invokation. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_setopt_log_handler(mongocrypt_t crypt, + mongocrypt_log_fn_t log_fn, + Pointer log_ctx); + + + public static native boolean + mongocrypt_setopt_crypto_hooks(mongocrypt_t crypt, + mongocrypt_crypto_fn aes_256_cbc_encrypt, + mongocrypt_crypto_fn aes_256_cbc_decrypt, + mongocrypt_random_fn random, + mongocrypt_hmac_fn hmac_sha_512, + mongocrypt_hmac_fn hmac_sha_256, + mongocrypt_hash_fn sha_256, + Pointer ctx); + + /** + * Set a crypto hook for the AES256-CTR operations. + * + * @param crypt The @ref mongocrypt_t object. + * @param aes_256_ctr_encrypt The crypto callback function for encrypt + * operation. + * @param aes_256_ctr_decrypt The crypto callback function for decrypt + * operation. + * @param ctx A context passed as an argument to the crypto callback + * every invocation. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_status + * + */ + public static native boolean + mongocrypt_setopt_aes_256_ctr (mongocrypt_t crypt, + mongocrypt_crypto_fn aes_256_ctr_encrypt, + mongocrypt_crypto_fn aes_256_ctr_decrypt, + Pointer ctx); + + /** + * Set a crypto hook for the RSASSA-PKCS1-v1_5 algorithm with a SHA-256 hash. + * + *

See: https://tools.ietf.org/html/rfc3447#section-8.2

+ * + *

Note: this function has the wrong name. It should be: + * mongocrypt_setopt_crypto_hook_sign_rsassa_pkcs1_v1_5

+ * + * @param crypt The @ref mongocrypt_t object. + * @param sign_rsaes_pkcs1_v1_5 The crypto callback function. + * @param sign_ctx A context passed as an argument to the crypto callback + * every invocation. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_status + */ + public static native boolean + mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5( + mongocrypt_t crypt, + mongocrypt_hmac_fn sign_rsaes_pkcs1_v1_5, + Pointer sign_ctx); + + /** + * Set a handler to get called on every log message. + * + * @param crypt The @ref mongocrypt_t object. + * @param aws_access_key_id The AWS access key ID used to generate KMS + * messages. + * @param aws_access_key_id_len The string length (in bytes) of @p + * * aws_access_key_id. Pass -1 to determine the string length with strlen (must + * * be NULL terminated). + * @param aws_secret_access_key The AWS secret access key used to generate + * KMS messages. + * @param aws_secret_access_key_len The string length (in bytes) of @p + * aws_secret_access_key. Pass -1 to determine the string length with strlen + * (must be NULL terminated). + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_setopt_kms_provider_aws(mongocrypt_t crypt, + cstring aws_access_key_id, + int aws_access_key_id_len, + cstring aws_secret_access_key, + int aws_secret_access_key_len); + + /** + * Configure a local KMS provider on the @ref mongocrypt_t object. + * + * @param crypt The @ref mongocrypt_t object. + * @param key A 64 byte master key used to encrypt and decrypt key vault keys. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_setopt_kms_provider_local(mongocrypt_t crypt, + mongocrypt_binary_t key); + + /** + * Configure KMS providers with a BSON document. + * + * @param crypt The @ref mongocrypt_t object. + * @param kms_providers A BSON document mapping the KMS provider names to credentials. + * @return A boolean indicating success. If false, an error status is set. + * @since 1.1 + */ + public static native boolean + mongocrypt_setopt_kms_providers(mongocrypt_t crypt, + mongocrypt_binary_t kms_providers); + + /** + * Set a local schema map for encryption. + * + * @param crypt The @ref mongocrypt_t object. + * @param schema_map A BSON document representing the schema map supplied by + * the user. The keys are collection namespaces and values are JSON schemas. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_status + */ + public static native boolean + mongocrypt_setopt_schema_map (mongocrypt_t crypt, mongocrypt_binary_t schema_map); + + /** + * Opt-into setting KMS providers before each KMS request. + * + * If set, before entering the MONGOCRYPT_CTX_NEED_KMS state, + * contexts will enter the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state + * and then wait for credentials to be supplied through @ref mongocrypt_ctx_provide_kms_providers. + * + * @param crypt The @ref mongocrypt_t object to update + */ + public static native void + mongocrypt_setopt_use_need_kms_credentials_state (mongocrypt_t crypt); + + + /** + * Set a local EncryptedFieldConfigMap for encryption. + * + * @param crypt The @ref mongocrypt_t object. + * @param encryptedFieldConfigMap A BSON document representing the EncryptedFieldConfigMap + * supplied by the user. The keys are collection namespaces and values are + * EncryptedFieldConfigMap documents. The viewed data copied. It is valid to + * destroy @p efc_map with @ref mongocrypt_binary_destroy immediately after. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_status + */ + public static native boolean + mongocrypt_setopt_encrypted_field_config_map (mongocrypt_t crypt, mongocrypt_binary_t encryptedFieldConfigMap); + + /** + * Opt-into skipping query analysis. + * + *

If opted in: + *

    + *
  • The crypt_shared shared library will not attempt to be loaded.
  • + *
  • A mongocrypt_ctx_t will never enter the MONGOCRYPT_CTX_NEED_MARKINGS state.
  • + *
+ * + * @param crypt The @ref mongocrypt_t object to update + * @since 1.5 + */ + public static native void + mongocrypt_setopt_bypass_query_analysis (mongocrypt_t crypt); + + /** + * Set the contention factor used for explicit encryption. + * The contention factor is only used for indexed Queryable Encryption. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param contention_factor the contention factor + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status. + * @since 1.5 + */ + public static native boolean + mongocrypt_ctx_setopt_contention_factor (mongocrypt_ctx_t ctx, long contention_factor); + + /** + * Set the index key id to use for Queryable Encryption explicit encryption. + * + * If the index key id not set, the key id from @ref mongocrypt_ctx_setopt_key_id is used. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param key_id The binary corresponding to the _id (a UUID) of the data key to use from + * the key vault collection. Note, the UUID must be encoded with RFC-4122 byte order. + * The viewed data is copied. It is valid to destroy key_id with @ref mongocrypt_binary_destroy immediately after. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + * @since 1.5 + */ + public static native boolean + mongocrypt_ctx_setopt_index_key_id (mongocrypt_ctx_t ctx, mongocrypt_binary_t key_id); + + /** + * Append an additional search directory to the search path for loading + * the crypt_shared dynamic library. + * + * @param crypt The @ref mongocrypt_t object to update + * @param path A null-terminated sequence of bytes for the search path. On + * some filesystems, this may be arbitrary bytes. On other filesystems, this may + * be required to be a valid UTF-8 code unit sequence. If the leading element of + * the path is the literal string "$ORIGIN", that substring will be replaced + * with the directory path containing the executable libmongocrypt module. If + * the path string is literal "$SYSTEM", then libmongocrypt will defer to the + * system's library resolution mechanism to find the crypt_shared library. + * + *

If no crypt_shared dynamic library is found in any of the directories + * specified by the search paths loaded here, @ref mongocrypt_init() will still + * succeed and continue to operate without crypt_shared.

+ * + *

The search paths are searched in the order that they are appended. This + * allows one to provide a precedence in how the library will be discovered. For + * example, appending known directories before appending "$SYSTEM" will allow + * one to supersede the system's installed library, but still fall-back to it if + * the library wasn't found otherwise. If one does not ever append "$SYSTEM", + * then the system's library-search mechanism will never be consulted.

+ * + *

If an absolute path to the library is specified using @ref mongocrypt_setopt_set_crypt_shared_lib_path_override, + * then paths appended here will have no effect.

+ * @since 1.5 + */ + public static native void + mongocrypt_setopt_append_crypt_shared_lib_search_path (mongocrypt_t crypt, cstring path); + + /** + * Set a single override path for loading the crypt_shared dynamic library. + * @param crypt The @ref mongocrypt_t object to update + * @param path A null-terminated sequence of bytes for a path to the crypt_shared + * dynamic library. On some filesystems, this may be arbitrary bytes. On other + * filesystems, this may be required to be a valid UTF-8 code unit sequence. If + * the leading element of the path is the literal string `$ORIGIN`, that + * substring will be replaced with the directory path containing the executable + * libmongocrypt module. + * + *

This function will do no IO nor path validation. All validation will + * occur during the call to @ref mongocrypt_init.

+ *

If a crypt_shared library path override is specified here, then no paths given + * to @ref mongocrypt_setopt_append_crypt_shared_lib_search_path will be consulted when + * opening the crypt_shared library.

+ *

If a path is provided via this API and @ref mongocrypt_init fails to + * initialize a valid crypt_shared library instance for the path specified, then + * the initialization of mongocrypt_t will fail with an error.

+ * @since 1.5 + */ + public static native void + mongocrypt_setopt_set_crypt_shared_lib_path_override(mongocrypt_t crypt, cstring path); + + /** + * Set the query type to use for Queryable Encryption explicit encryption. + * The query type is only used for indexed Queryable Encryption. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param query_type the query type + * @param len the length + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + */ + public static native boolean + mongocrypt_ctx_setopt_query_type (mongocrypt_ctx_t ctx, cstring query_type, int len); + + /** + * Set options for explicit encryption with the "range" algorithm. + * NOTE: "range" is currently unstable API and subject to backwards breaking changes. + * + * opts is a BSON document of the form: + * { + * "min": Optional<BSON value>, + * "max": Optional<BSON value>, + * "sparsity": Int64, + * "precision": Optional<Int32> + * "trimFactor": Optional<Int32> + * } + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param opts BSON. + * @return A boolean indicating success. If false, an error status is set. + * @since 1.7 + */ + public static native boolean + mongocrypt_ctx_setopt_algorithm_range (mongocrypt_ctx_t ctx, mongocrypt_binary_t opts); + + /** + * Initialize new @ref mongocrypt_t object. + * + * @param crypt The @ref mongocrypt_t object. + * @return A boolean indicating success. Failure may occur if previously set options are invalid. + */ + public static native boolean + mongocrypt_init(mongocrypt_t crypt); + + + /** + * Get the status associated with a @ref mongocrypt_t object. + * + * @param crypt The @ref mongocrypt_t object. + * @param status Receives the status. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_status(mongocrypt_t crypt, mongocrypt_status_t status); + + /** + * Returns true if libmongocrypt was built with native crypto support. + * + *

+ * If libmongocrypt was not built with native crypto support, setting crypto hooks is required. + *

+ * + * @return true if libmongocrypt was built with native crypto support + */ + public static native boolean + mongocrypt_is_crypto_available(); + + /** + * Destroy the @ref mongocrypt_t object. + * + * @param crypt The @ref mongocrypt_t object to destroy. + */ + public static native void + mongocrypt_destroy(mongocrypt_t crypt); + + /** + * Obtain a nul-terminated version string of the loaded crypt_shared dynamic library, + * if available. + * + * If no crypt_shared was successfully loaded, this function returns NULL. + * + * @param crypt The mongocrypt_t object after a successful call to mongocrypt_init. + * @param len an optional length of the returned string. May be NULL. + * + * @return A nul-terminated string of the dynamically loaded crypt_shared library. + * @since 1.5 + */ + public static native cstring + mongocrypt_crypt_shared_lib_version_string (mongocrypt_t crypt, Pointer len); + + /** + * Call in response to the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS state + * to set per-context KMS provider settings. These follow the same format + * as @ref mongocrypt_setopt_kms_providers. If no keys are present in the + * BSON input, the KMS provider settings configured for the @ref mongocrypt_t + * at initialization are used. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param kms_providers A BSON document mapping the KMS provider names + * to credentials. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status. + */ + public static native boolean + mongocrypt_ctx_provide_kms_providers (mongocrypt_ctx_t ctx, + mongocrypt_binary_t kms_providers); + + /** + * Set the key id to use for explicit encryption. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param key_id The key_id to use. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_setopt_key_id (mongocrypt_ctx_t ctx, + mongocrypt_binary_t key_id); + + /** + * Set the keyAltName to use for explicit encryption. + * keyAltName should be a binary encoding a bson document + * with the following format: { "keyAltName" : >BSON UTF8 value< } + * + *

It is an error to set both this and the key id.

+ * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param key_alt_name The name to use. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + */ + public static native boolean + mongocrypt_ctx_setopt_key_alt_name (mongocrypt_ctx_t ctx, + mongocrypt_binary_t key_alt_name); + + /** + * Set the keyMaterial to use for encrypting data. + * + *

+ * Pass the binary encoding of a BSON document like the following: + * { "keyMaterial" : (BSON BINARY value) } + *

+ * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param key_material The data encryption key to use. The viewed data is + * copied. It is valid to destroy @p key_material with @ref + * mongocrypt_binary_destroy immediately after. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + */ + public static native boolean + mongocrypt_ctx_setopt_key_material (mongocrypt_ctx_t ctx, mongocrypt_binary_t key_material); + + /** + * Set the algorithm used for encryption to either + * deterministic or random encryption. This value + * should only be set when using explicit encryption. + * + * If -1 is passed in for "len", then "algorithm" is + * assumed to be a null-terminated string. + * + * Valid values for algorithm are: + * "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + * "AEAD_AES_256_CBC_HMAC_SHA_512-Randomized" + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param algorithm A string specifying the algorithm to + * use for encryption. + * @param len The length of the algorithm string. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_setopt_algorithm (mongocrypt_ctx_t ctx, + cstring algorithm, + int len); + + + /** + * Create a new uninitialized @ref mongocrypt_ctx_t. + *

+ * Initialize the context with functions like @ref mongocrypt_ctx_encrypt_init. + * When done, destroy it with @ref mongocrypt_ctx_destroy. + * + * @param crypt The @ref mongocrypt_t object. + * @return A new context. + */ + public static native mongocrypt_ctx_t + mongocrypt_ctx_new(mongocrypt_t crypt); + + + /** + * Get the status associated with a @ref mongocrypt_ctx_t object. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param status Receives the status. + * @return A boolean indicating success. + */ + + public static native boolean + mongocrypt_ctx_status(mongocrypt_ctx_t ctx, mongocrypt_status_t status); + + + /** + * Identify the AWS KMS master key to use for creating a data key. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param region The AWS region. + * @param region_len The string length of @p region. Pass -1 to determine + * the string length with strlen (must be NULL terminated). + * @param cmk The Amazon Resource Name (ARN) of the customer master key + * (CMK). + * @param cmk_len The string length of @p cmk_len. Pass -1 to determine the + * string length with strlen (must be NULL terminated). + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_setopt_masterkey_aws (mongocrypt_ctx_t ctx, + cstring region, + int region_len, + cstring cmk, + int cmk_len); + + /** + * Identify a custom AWS endpoint when creating a data key. + * This is used internally to construct the correct HTTP request + * (with the Host header set to this endpoint). This endpoint + * is persisted in the new data key, and will be returned via + * mongocrypt_kms_ctx_endpoint. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param endpoint The endpoint. + * @param endpoint_len The string length of @p endpoint. Pass -1 to + * determine the string length with strlen (must be NULL terminated). + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + */ + public static native boolean + mongocrypt_ctx_setopt_masterkey_aws_endpoint (mongocrypt_ctx_t ctx, + cstring endpoint, + int endpoint_len); + + + /** + * Set the master key to "local" for creating a data key. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_setopt_masterkey_local (mongocrypt_ctx_t ctx); + + /** + * Set key encryption key document for creating a data key. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param keyDocument BSON representing the key encryption key document. + * @return A boolean indicating success. If false, and error status is set. + * @since 1.1 + */ + public static native boolean + mongocrypt_ctx_setopt_key_encryption_key(mongocrypt_ctx_t ctx, + mongocrypt_binary_t keyDocument); + + /** + * Initialize a context to create a data key. + * + * Set options before using @ref mongocrypt_ctx_setopt_masterkey_aws and + * mongocrypt_ctx_setopt_masterkey_local. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @return A boolean indicating success. + * + * Assumes a master key option has been set, and an associated KMS provider + * has been set on the parent @ref mongocrypt_t. + */ + public static native boolean + mongocrypt_ctx_datakey_init (mongocrypt_ctx_t ctx); + + /** + * Initialize a context for encryption. + * + * Associated options: + * - @ref mongocrypt_ctx_setopt_cache_noblock + * - @ref mongocrypt_ctx_setopt_schema + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param db The database name. + * @param db_len The byte length of @p db. Pass -1 to determine the string length with strlen (must be NULL terminated). + * @param cmd The BSON command to be encrypted. + * @return A boolean indicating success. If false, an error status is set. + * Retrieve it with @ref mongocrypt_ctx_status + */ + public static native boolean + mongocrypt_ctx_encrypt_init(mongocrypt_ctx_t ctx, + cstring db, + int db_len, + mongocrypt_binary_t cmd); + + /** + * Explicit helper method to encrypt a single BSON object. Contexts + * created for explicit encryption will not go through mongocryptd. + * + * To specify a key_id, algorithm, or iv to use, please use the + * corresponding mongocrypt_setopt methods before calling this. + * + * This method expects the passed-in BSON to be of the form: + * { "v" : BSON value to encrypt } + * + * @param ctx A @ref mongocrypt_ctx_t. + * @param msg A @ref mongocrypt_binary_t the plaintext BSON value. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_explicit_encrypt_init (mongocrypt_ctx_t ctx, + mongocrypt_binary_t msg); + + /** + * Explicit helper method to encrypt a Match Expression or Aggregate Expression. + * Contexts created for explicit encryption will not go through mongocryptd. + * Requires query_type to be "range". + * NOTE: "range" is currently unstable API and subject to backwards breaking changes. + * + * This method expects the passed-in BSON to be of the form: + * { "v" : FLE2RangeFindDriverSpec } + * + * FLE2RangeFindDriverSpec is a BSON document with one of these forms: + * + * 1. A Match Expression of this form: + * {$and: [{<field>: {<op>: <value1>, {<field>: {<op>: <value2> }}]} + * 2. An Aggregate Expression of this form: + * {$and: [{<op>: [<fieldpath>, <value1>]}, {<op>: [<fieldpath>, <value2>]}] + * + * may be $lt, $lte, $gt, or $gte. + * + * The value of "v" is expected to be the BSON value passed to a driver + * ClientEncryption.encryptExpression helper. + * + * Associated options for FLE 1: + * - @ref mongocrypt_ctx_setopt_key_id + * - @ref mongocrypt_ctx_setopt_key_alt_name + * - @ref mongocrypt_ctx_setopt_algorithm + * + * Associated options for Queryable Encryption: + * - @ref mongocrypt_ctx_setopt_key_id + * - @ref mongocrypt_ctx_setopt_index_key_id + * - @ref mongocrypt_ctx_setopt_contention_factor + * - @ref mongocrypt_ctx_setopt_query_type + * - @ref mongocrypt_ctx_setopt_algorithm_range + * + * An error is returned if FLE 1 and Queryable Encryption incompatible options + * are set. + * + * @param ctx A @ref mongocrypt_ctx_t. + * @param msg A @ref mongocrypt_binary_t the plaintext BSON value. + * @return A boolean indicating success. + * @since 1.7 + */ + public static native boolean + mongocrypt_ctx_explicit_encrypt_expression_init (mongocrypt_ctx_t ctx, + mongocrypt_binary_t msg); + + /** + * Initialize a context for decryption. + * + * @param ctx The mongocrypt_ctx_t object. + * @param doc The document to be decrypted. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_decrypt_init(mongocrypt_ctx_t ctx, mongocrypt_binary_t doc); + + + /** + * Explicit helper method to decrypt a single BSON object. + * + * @param ctx A @ref mongocrypt_ctx_t. + * @param msg A @ref mongocrypt_binary_t the encrypted BSON. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_explicit_decrypt_init (mongocrypt_ctx_t ctx, + mongocrypt_binary_t msg); + + /** + * Initialize a context to rewrap datakeys. + * + * Associated options {@link #mongocrypt_ctx_setopt_key_encryption_key(mongocrypt_ctx_t, mongocrypt_binary_t)} + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param filter The filter to use for the find command on the key vault collection to retrieve datakeys to rewrap. + * @return A boolean indicating success. If false, and error status is set. + * @since 1.5 + */ + public static native boolean + mongocrypt_ctx_rewrap_many_datakey_init (mongocrypt_ctx_t ctx, mongocrypt_binary_t filter); + + + public static final int MONGOCRYPT_CTX_ERROR = 0; + public static final int MONGOCRYPT_CTX_NEED_MONGO_COLLINFO = 1; /* run on main MongoClient */ + public static final int MONGOCRYPT_CTX_NEED_MONGO_MARKINGS = 2; /* run on mongocryptd. */ + public static final int MONGOCRYPT_CTX_NEED_MONGO_KEYS = 3; /* run on key vault */ + public static final int MONGOCRYPT_CTX_NEED_KMS = 4; + public static final int MONGOCRYPT_CTX_READY = 5; /* ready for encryption/decryption */ + public static final int MONGOCRYPT_CTX_DONE = 6; + public static final int MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS = 7; /* fetch/renew KMS credentials */ + + public static final int MONGOCRYPT_INDEX_TYPE_NONE = 1; + public static final int MONGOCRYPT_INDEX_TYPE_EQUALITY = 2; + public static final int MONGOCRYPT_QUERY_TYPE_EQUALITY = 1; + + /** + * Get the current state of a context. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @return A @ref mongocrypt_ctx_state_t. + */ + public static native int + mongocrypt_ctx_state(mongocrypt_ctx_t ctx); + + + /** + * Get BSON necessary to run the mongo operation when mongocrypt_ctx_t + * is in MONGOCRYPT_CTX_NEED_MONGO_* states. + * + *

+ * op_bson is a BSON document to be used for the operation. + * - For MONGOCRYPT_CTX_NEED_MONGO_COLLINFO it is a listCollections filter. + * - For MONGOCRYPT_CTX_NEED_MONGO_KEYS it is a find filter. + * - For MONGOCRYPT_CTX_NEED_MONGO_MARKINGS it is a JSON schema to append. + *

+ * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param op_bson A BSON document for the MongoDB operation. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_mongo_op(mongocrypt_ctx_t ctx, mongocrypt_binary_t op_bson); + + + /** + * Feed a BSON reply or result when when mongocrypt_ctx_t is in + * MONGOCRYPT_CTX_NEED_MONGO_* states. This may be called multiple times + * depending on the operation. + *

+ * op_bson is a BSON document to be used for the operation. + * - For MONGOCRYPT_CTX_NEED_MONGO_COLLINFO it is a doc from a listCollections + * cursor. + * - For MONGOCRYPT_CTX_NEED_MONGO_KEYS it is a doc from a find cursor. + * - For MONGOCRYPT_CTX_NEED_MONGO_MARKINGS it is a reply from mongocryptd. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @param reply A BSON document for the MongoDB operation. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_mongo_feed(mongocrypt_ctx_t ctx, mongocrypt_binary_t reply); + + + /** + * Call when done feeding the reply (or replies) back to the context. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @return A boolean indicating success. + */ + + public static native boolean + mongocrypt_ctx_mongo_done(mongocrypt_ctx_t ctx); + + /** + * Get the next KMS handle. + *

+ * Multiple KMS handles may be retrieved at once. Drivers may do this to fan + * out multiple concurrent KMS HTTP requests. Feeding multiple KMS requests + * is thread-safe. + *

+ * Is KMS handles are being handled synchronously, the driver can reuse the same + * TLS socket to send HTTP requests and receive responses. + * + * @param ctx A @ref mongocrypt_ctx_t. + * @return a new @ref mongocrypt_kms_ctx_t or NULL. + */ + public static native mongocrypt_kms_ctx_t + mongocrypt_ctx_next_kms_ctx(mongocrypt_ctx_t ctx); + + /** + * Get the KMS provider identifier associated with this KMS request. + * + * This is used to conditionally configure TLS connections based on the KMS + * request. It is useful for KMIP, which authenticates with a client + * certificate. + * + * @param kms The mongocrypt_kms_ctx_t object. + * @param len Receives the length of the returned string. + * + * @return The name of the KMS provider + */ + public static native cstring + mongocrypt_kms_ctx_get_kms_provider(mongocrypt_kms_ctx_t kms, + Pointer len); + + /** + * Get the HTTP request message for a KMS handle. + * + * @param kms A @ref mongocrypt_kms_ctx_t. + * @param msg The HTTP request to send to KMS. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_kms_ctx_message(mongocrypt_kms_ctx_t kms, + mongocrypt_binary_t msg); + + /** + * Get the hostname from which to connect over TLS. + *

+ * The storage for @p endpoint is not owned by the caller, but + * is valid until calling @ref mongocrypt_ctx_kms_done on the + * parent @ref mongocrypt_ctx_t. + * + * @param kms A @ref mongocrypt_kms_ctx_t. + * @param endpoint The output hostname. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_kms_ctx_endpoint(mongocrypt_kms_ctx_t kms, PointerByReference endpoint); + + /** + * Indicates how many bytes to feed into @ref mongocrypt_kms_ctx_feed. + * + * @param kms The @ref mongocrypt_kms_ctx_t. + * @return The number of requested bytes. + */ + public static native int + mongocrypt_kms_ctx_bytes_needed(mongocrypt_kms_ctx_t kms); + + + /** + * Feed bytes from the HTTP response. + *

+ * Feeding more bytes than what has been returned in @ref + * mongocrypt_kms_ctx_bytes_needed is an error. + * + * @param kms The @ref mongocrypt_kms_ctx_t. + * @param bytes The bytes to feed. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_kms_ctx_feed(mongocrypt_kms_ctx_t kms, mongocrypt_binary_t bytes); + + + /** + * Get the status associated with a @ref mongocrypt_kms_ctx_t object. + * + * @param kms The @ref mongocrypt_kms_ctx_t object. + * @param status Receives the status. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_kms_ctx_status(mongocrypt_kms_ctx_t kms, + mongocrypt_status_t status); + + + /** + * Call when done handling all KMS contexts. + * + * @param ctx The @ref mongocrypt_ctx_t object. + * @return A boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_kms_done(mongocrypt_ctx_t ctx); + + + /** + * Perform the final encryption or decryption. + * + * @param ctx A @ref mongocrypt_ctx_t. + * @param out The final BSON to send to the server. + * @return a boolean indicating success. + */ + public static native boolean + mongocrypt_ctx_finalize(mongocrypt_ctx_t ctx, mongocrypt_binary_t out); + + + /** + * Destroy and free all memory associated with a @ref mongocrypt_ctx_t. + * + * @param ctx A @ref mongocrypt_ctx_t. + */ + public static native void + mongocrypt_ctx_destroy(mongocrypt_ctx_t ctx); + + static final String NATIVE_LIBRARY_NAME = "mongocrypt"; + + static { + Native.register(CAPI.class, NATIVE_LIBRARY_NAME); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPIHelper.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPIHelper.java new file mode 100644 index 00000000000..c1de63e8c8c --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CAPIHelper.java @@ -0,0 +1,94 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.sun.jna.Pointer; +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.RawBsonDocument; +import org.bson.codecs.BsonValueCodecProvider; +import org.bson.codecs.Codec; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; +import org.bson.io.BasicOutputBuffer; + +import java.nio.ByteBuffer; + +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_new_from_data; +import static java.lang.String.format; + +final class CAPIHelper { + + private static final CodecRegistry CODEC_REGISTRY = CodecRegistries.fromProviders(new BsonValueCodecProvider()); + + @SuppressWarnings("unchecked") + static BinaryHolder toBinary(final BsonDocument document) { + BasicOutputBuffer buffer = new BasicOutputBuffer(); + BsonBinaryWriter writer = new BsonBinaryWriter(buffer); + ((Codec) CODEC_REGISTRY.get(document.getClass())).encode(writer, document, EncoderContext.builder().build()); + + DisposableMemory memory = new DisposableMemory(buffer.size()); + memory.write(0, buffer.getInternalBuffer(), 0, buffer.size()); + + return new BinaryHolder(memory, mongocrypt_binary_new_from_data(memory, buffer.getSize())); + } + + static RawBsonDocument toDocument(final mongocrypt_binary_t binary) { + ByteBuffer byteBuffer = toByteBuffer(binary); + byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(bytes); + return new RawBsonDocument(bytes); + } + + static BinaryHolder toBinary(final ByteBuffer buffer) { + byte[] message = new byte[buffer.remaining()]; + buffer.get(message, 0, buffer.remaining()); + + DisposableMemory memory = new DisposableMemory(message.length); + memory.write(0, message, 0, message.length); + + return new BinaryHolder(memory, mongocrypt_binary_new_from_data(memory, message.length)); + } + + static ByteBuffer toByteBuffer(final mongocrypt_binary_t binary) { + Pointer pointer = binary.data(); + int length = binary.len(); + return pointer.getByteBuffer(0, length); + } + + static byte[] toByteArray(final mongocrypt_binary_t binary) { + ByteBuffer byteBuffer = toByteBuffer(binary); + byte[] byteArray = new byte[byteBuffer.remaining()]; + byteBuffer.get(byteArray); + return byteArray; + } + + static void writeByteArrayToBinary(final mongocrypt_binary_t binary, final byte[] bytes) { + if (binary.len() < bytes.length) { + throw new IllegalArgumentException(format("mongocrypt binary of length %d is not large enough to hold %d bytes", + binary.len(), bytes.length)); + } + Pointer outPointer = binary.data(); + outPointer.write(0, bytes, 0, bytes.length); + } + + private CAPIHelper() { + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/CipherCallback.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CipherCallback.java new file mode 100644 index 00000000000..b10c0f21c67 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/CipherCallback.java @@ -0,0 +1,92 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_crypto_fn; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.ConcurrentLinkedDeque; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_STATUS_ERROR_CLIENT; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_set; +import static com.mongodb.crypt.capi.CAPIHelper.toByteArray; +import static com.mongodb.crypt.capi.CAPIHelper.writeByteArrayToBinary; + +class CipherCallback implements mongocrypt_crypto_fn { + private final String algorithm; + private final String transformation; + private final int mode; + private final CipherPool cipherPool; + + CipherCallback(final String algorithm, final String transformation, final int mode) { + this.algorithm = algorithm; + this.transformation = transformation; + this.mode = mode; + this.cipherPool = new CipherPool(); + } + + @Override + public boolean crypt(final Pointer ctx, final mongocrypt_binary_t key, final mongocrypt_binary_t iv, + final mongocrypt_binary_t in, final mongocrypt_binary_t out, + final Pointer bytesWritten, final mongocrypt_status_t status) { + Cipher cipher = null; + try { + IvParameterSpec ivParameterSpec = new IvParameterSpec(toByteArray(iv)); + SecretKeySpec secretKeySpec = new SecretKeySpec(toByteArray(key), algorithm); + cipher = cipherPool.get(); + cipher.init(mode, secretKeySpec, ivParameterSpec); + + byte[] result = cipher.doFinal(toByteArray(in)); + writeByteArrayToBinary(out, result); + bytesWritten.setInt(0, result.length); + + return true; + } catch (Exception e) { + mongocrypt_status_set(status, MONGOCRYPT_STATUS_ERROR_CLIENT, 0, new cstring(e.toString()), -1); + return false; + } finally { + if (cipher != null) { + cipherPool.release(cipher); + } + } + } + + private class CipherPool { + private final ConcurrentLinkedDeque available = new ConcurrentLinkedDeque<>(); + + Cipher get() throws NoSuchAlgorithmException, NoSuchPaddingException { + Cipher cipher = available.pollLast(); + if (cipher != null) { + return cipher; + } + return Cipher.getInstance(transformation); + } + + void release(final Cipher cipher) { + available.addLast(cipher); + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/DisposableMemory.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/DisposableMemory.java new file mode 100644 index 00000000000..fdcfb268fea --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/DisposableMemory.java @@ -0,0 +1,31 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.sun.jna.Memory; + +// Subclass of JNA's Memory class so that we can call its protected dispose method +class DisposableMemory extends Memory { + DisposableMemory(final int size) { + super(size); + } + + public void dispose() { + super.dispose(); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/JULLogger.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/JULLogger.java new file mode 100644 index 00000000000..9a53e850d15 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/JULLogger.java @@ -0,0 +1,130 @@ + +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import java.util.logging.Level; + +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.FINER; +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.SEVERE; +import static java.util.logging.Level.WARNING; + +class JULLogger implements Logger { + + private final java.util.logging.Logger delegate; + + JULLogger(final String name) { + this.delegate = java.util.logging.Logger.getLogger(name); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public boolean isTraceEnabled() { + return isEnabled(FINER); + } + + @Override + public void trace(final String msg) { + log(FINER, msg); + } + + @Override + public void trace(final String msg, final Throwable t) { + log(FINER, msg, t); + } + + @Override + public boolean isDebugEnabled() { + return isEnabled(FINE); + } + + @Override + public void debug(final String msg) { + log(FINE, msg); + } + + @Override + public void debug(final String msg, final Throwable t) { + log(FINE, msg, t); + } + + @Override + public boolean isInfoEnabled() { + return delegate.isLoggable(INFO); + } + + @Override + public void info(final String msg) { + log(INFO, msg); + } + + @Override + public void info(final String msg, final Throwable t) { + log(INFO, msg, t); + } + + @Override + public boolean isWarnEnabled() { + return delegate.isLoggable(WARNING); + } + + @Override + public void warn(final String msg) { + log(WARNING, msg); + } + + @Override + public void warn(final String msg, final Throwable t) { + log(WARNING, msg, t); + } + + + @Override + public boolean isErrorEnabled() { + return delegate.isLoggable(SEVERE); + } + + @Override + public void error(final String msg) { + log(SEVERE, msg); + } + + @Override + public void error(final String msg, final Throwable t) { + log(SEVERE, msg, t); + } + + + private boolean isEnabled(final Level level) { + return delegate.isLoggable(level); + } + + private void log(final Level level, final String msg) { + delegate.log(level, msg); + } + + public void log(final Level level, final String msg, final Throwable t) { + delegate.log(level, msg, t); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/Logger.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/Logger.java new file mode 100644 index 00000000000..38e82c235b8 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/Logger.java @@ -0,0 +1,144 @@ + +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +/** + * Not part of the public API + */ +public interface Logger { + /** + * Return the name of this Logger instance. + * + * @return name of this logger instance + */ + String getName(); + + /** + * Is the logger instance enabled for the TRACE level? + * + * @return True if this Logger is enabled for the TRACE level, false otherwise. + */ + boolean isTraceEnabled(); + + /** + * Log a message at the TRACE level. + * + * @param msg the message string to be logged + */ + void trace(String msg); + + /** + * Log an exception (throwable) at the TRACE level with an accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + void trace(String msg, Throwable t); + + /** + * Is the logger instance enabled for the DEBUG level? + * + * @return True if this Logger is enabled for the DEBUG level, false otherwise. + */ + boolean isDebugEnabled(); + + + /** + * Log a message at the DEBUG level. + * + * @param msg the message string to be logged + */ + void debug(String msg); + + + /** + * Log an exception (throwable) at the DEBUG level with an accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + void debug(String msg, Throwable t); + + /** + * Is the logger instance enabled for the INFO level? + * + * @return True if this Logger is enabled for the INFO level, false otherwise. + */ + boolean isInfoEnabled(); + + + /** + * Log a message at the INFO level. + * + * @param msg the message string to be logged + */ + void info(String msg); + + /** + * Log an exception (throwable) at the INFO level with an accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + void info(String msg, Throwable t); + + /** + * Is the logger instance enabled for the WARN level? + * + * @return True if this Logger is enabled for the WARN level, false otherwise. + */ + boolean isWarnEnabled(); + + /** + * Log a message at the WARN level. + * + * @param msg the message string to be logged + */ + void warn(String msg); + + /** + * Log an exception (throwable) at the WARN level with an accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + void warn(String msg, Throwable t); + + /** + * Is the logger instance enabled for the ERROR level? + * + * @return True if this Logger is enabled for the ERROR level, false otherwise. + */ + boolean isErrorEnabled(); + + /** + * Log a message at the ERROR level. + * + * @param msg the message string to be logged + */ + void error(String msg); + + /** + * Log an exception (throwable) at the ERROR level with an accompanying message. + * + * @param msg the message accompanying the exception + * @param t the exception (throwable) to log + */ + void error(String msg, Throwable t); +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/Loggers.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/Loggers.java new file mode 100644 index 00000000000..c57cd3994e4 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/Loggers.java @@ -0,0 +1,50 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +/** + * This class is not part of the public API. + */ +public final class Loggers { + private static final String NAME = "org.mongodb.driver.crypt"; + + private static final boolean USE_SLF4J = shouldUseSLF4J(); + + /** + * @return the logger + */ + public static Logger getLogger() { + if (USE_SLF4J) { + return new SLF4JLogger(NAME); + } else { + return new JULLogger(NAME); + } + } + + private Loggers() { + } + + private static boolean shouldUseSLF4J() { + try { + Class.forName("org.slf4j.Logger"); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MacCallback.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MacCallback.java new file mode 100644 index 00000000000..2ea09550bb4 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MacCallback.java @@ -0,0 +1,60 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_hmac_fn; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_STATUS_ERROR_CLIENT; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_set; +import static com.mongodb.crypt.capi.CAPIHelper.toByteArray; +import static com.mongodb.crypt.capi.CAPIHelper.writeByteArrayToBinary; + +class MacCallback implements mongocrypt_hmac_fn { + private final String algorithm; + + MacCallback(final String algorithm) { + this.algorithm = algorithm; + } + + @Override + public boolean hmac(final Pointer ctx, final mongocrypt_binary_t key, final mongocrypt_binary_t in, + final mongocrypt_binary_t out, final mongocrypt_status_t status) { + try { + Mac mac = Mac.getInstance(algorithm); + SecretKeySpec keySpec = new SecretKeySpec(toByteArray(key), algorithm); + mac.init(keySpec); + + mac.update(toByteArray(in)); + + byte[] result = mac.doFinal(); + writeByteArrayToBinary(out, result); + + return true; + } catch (Exception e) { + mongocrypt_status_set(status, MONGOCRYPT_STATUS_ERROR_CLIENT, 0, new cstring(e.toString()), -1); + return false; + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MessageDigestCallback.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MessageDigestCallback.java new file mode 100644 index 00000000000..861290d0a8f --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MessageDigestCallback.java @@ -0,0 +1,55 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_hash_fn; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; + +import java.security.MessageDigest; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_STATUS_ERROR_CLIENT; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_set; +import static com.mongodb.crypt.capi.CAPIHelper.toByteArray; +import static com.mongodb.crypt.capi.CAPIHelper.writeByteArrayToBinary; + +class MessageDigestCallback implements mongocrypt_hash_fn { + + private final String algorithm; + + MessageDigestCallback(final String algorithm) { + this.algorithm = algorithm; + } + + @Override + public boolean hash(final Pointer ctx, final mongocrypt_binary_t in, final mongocrypt_binary_t out, + final mongocrypt_status_t status) { + try { + MessageDigest messageDigest = MessageDigest.getInstance(algorithm); + messageDigest.update(toByteArray(in)); + byte[] digest = messageDigest.digest(); + writeByteArrayToBinary(out, digest); + return true; + } catch (Exception e) { + mongocrypt_status_set(status, MONGOCRYPT_STATUS_ERROR_CLIENT, 0, new cstring(e.toString()), -1); + return false; + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoAwsKmsProviderOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoAwsKmsProviderOptions.java new file mode 100644 index 00000000000..4824197510d --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoAwsKmsProviderOptions.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import static org.bson.assertions.Assertions.notNull; + +/** + * The options for configuring the AWS KMS provider. + */ +public final class MongoAwsKmsProviderOptions { + + private final String accessKeyId; + private final String secretAccessKey; + + /** + * Construct a builder for the options + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the access key id + * + * @return the access key id, which may not be null + */ + public String getAccessKeyId() { + return accessKeyId; + } + + /** + * Gets the secret access key + * + * @return the secret access key, which may not be null + */ + public String getSecretAccessKey() { + return secretAccessKey; + } + + + /** + * The builder for the options + */ + public static final class Builder { + private String accessKeyId; + private String secretAccessKey; + + private Builder() { + } + + /** + * Sets the access key id. + * + * @param accessKeyId the access key id + * @return this + */ + public Builder accessKeyId(final String accessKeyId) { + this.accessKeyId = accessKeyId; + return this; + } + + /** + * Sets the secret access key. + * + * @param secretAccessKey the secret access key + * @return this + */ + public Builder secretAccessKey(final String secretAccessKey) { + this.secretAccessKey = secretAccessKey; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoAwsKmsProviderOptions build() { + return new MongoAwsKmsProviderOptions(this); + } + } + + private MongoAwsKmsProviderOptions(final Builder builder) { + this.accessKeyId = notNull("AWS KMS provider accessKeyId", builder.accessKeyId); + this.secretAccessKey = notNull("AWS KMS provider secretAccessKey", builder.secretAccessKey); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypt.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypt.java new file mode 100644 index 00000000000..74816dbe42c --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypt.java @@ -0,0 +1,100 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + + +package com.mongodb.crypt.capi; + +import org.bson.BsonDocument; + +import java.io.Closeable; + +/** + * A context for encryption/decryption operations. + */ +public interface MongoCrypt extends Closeable { + + /** + * Create a context to use for encryption + * + * @param database the namespace + * @param command the document representing the command to encrypt + * @return the context + */ + MongoCryptContext createEncryptionContext(String database, BsonDocument command); + + /** + * Create a context to use for decryption + * + * @param document the document to decrypt + * @return the context + */ + MongoCryptContext createDecryptionContext(BsonDocument document); + + /** + * Create a context to use for creating a data key + * @param kmsProvider the KMS provider + * @param options the data key options + * @return the context + */ + MongoCryptContext createDataKeyContext(String kmsProvider, MongoDataKeyOptions options); + + /** + * Create a context to use for encryption + * + * @param document the document to encrypt, which must be in the form { "v" : BSON value to encrypt } + * @param options the explicit encryption options + * @return the context + */ + MongoCryptContext createExplicitEncryptionContext(BsonDocument document, MongoExplicitEncryptOptions options); + + /** + * Create a context to use for encryption + * + * @param document the document to encrypt, which must be in the form { "v" : BSON value to encrypt } + * @param options the expression encryption options + * @return the context + * @since 1.7 + */ + MongoCryptContext createEncryptExpressionContext(BsonDocument document, MongoExplicitEncryptOptions options); + + /** + * Create a context to use for encryption + * + * @param document the document to decrypt,which must be in the form { "v" : encrypted BSON value } + * @return the context + */ + MongoCryptContext createExplicitDecryptionContext(BsonDocument document); + + /** + * Create a context to use for encryption + * + * @param filter The filter to use for the find command on the key vault collection to retrieve datakeys to rewrap. + * @param options the rewrap many data key options + * @return the context + * @since 1.5 + */ + MongoCryptContext createRewrapManyDatakeyContext(BsonDocument filter, MongoRewrapManyDataKeyOptions options); + + /** + * @return the version string of the loaded crypt shared dynamic library if available or null + * @since 1.5 + */ + String getCryptSharedLibVersionString(); + + @Override + void close(); +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContext.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContext.java new file mode 100644 index 00000000000..2c3aa250b87 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContext.java @@ -0,0 +1,137 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import org.bson.BsonDocument; +import org.bson.RawBsonDocument; + +import java.io.Closeable; + +/** + * An interface representing the lifecycle of an encryption or decryption request. It's modelled as a state machine. + */ +public interface MongoCryptContext extends Closeable { + + /** + * The possible states. + */ + enum State { + /** + * Needs collection information from the cluster encrypting to + */ + NEED_MONGO_COLLINFO(CAPI.MONGOCRYPT_CTX_NEED_MONGO_COLLINFO), + + /** + * Need to mark command with encryption markers + */ + NEED_MONGO_MARKINGS(CAPI.MONGOCRYPT_CTX_NEED_MONGO_MARKINGS), + + /** + * Need keys from the key vault + */ + NEED_MONGO_KEYS(CAPI.MONGOCRYPT_CTX_NEED_MONGO_KEYS), + + /** + * Need the key management service + */ + NEED_KMS(CAPI.MONGOCRYPT_CTX_NEED_KMS), + + /** + * Need to fetch/renew KMS credentials + * @since 1.4 + */ + NEED_KMS_CREDENTIALS(CAPI.MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS), + + /** + * Ready for encryption/decryption + */ + READY(CAPI.MONGOCRYPT_CTX_READY), + + /** + * Done + */ + DONE(CAPI.MONGOCRYPT_CTX_DONE); + + private final int index; + + State(final int index) { + this.index = index; + } + + static State fromIndex(final int index) { + for (State state : State.values()) { + if (state.index == index) { + return state; + } + } + throw new MongoCryptException("Unknown context state " + index); + } + } + + /** + * Gets the current state. + * + * @return the current state + */ + State getState(); + + /** + * + * @return the operation to execute + */ + RawBsonDocument getMongoOperation(); + + /** + * + * @param document a result of the operation + */ + void addMongoOperationResult(BsonDocument document); + + /** + * Signal completion of the operation + */ + void completeMongoOperation(); + + /** + * Provide KMS credentials on demand, in response to NEED_KMS_CREDENTIALS state + * + * @param credentialsDocument document containing all credentials + * @since 1.4 + */ + void provideKmsProviderCredentials(BsonDocument credentialsDocument); + + /** + * + * @return the next key decryptor, or null if there are no more + */ + MongoKeyDecryptor nextKeyDecryptor(); + + /** + * Indicate that all key decryptors have been completed + */ + void completeKeyDecryptors(); + + /** + * + * @return the encrypted or decrypted document + */ + RawBsonDocument finish(); + + @Override + void close(); +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContextImpl.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContextImpl.java new file mode 100644 index 00000000000..34aaafe7344 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptContextImpl.java @@ -0,0 +1,164 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_t; +import org.bson.BsonDocument; +import org.bson.RawBsonDocument; + +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_new; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_finalize; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_kms_done; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_mongo_done; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_mongo_feed; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_mongo_op; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_next_kms_ctx; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_provide_kms_providers; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_state; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_status; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_new; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import static com.mongodb.crypt.capi.CAPIHelper.toBinary; +import static com.mongodb.crypt.capi.CAPIHelper.toDocument; +import static org.bson.assertions.Assertions.isTrue; +import static org.bson.assertions.Assertions.notNull; + +class MongoCryptContextImpl implements MongoCryptContext { + private final mongocrypt_ctx_t wrapped; + private volatile boolean closed; + + MongoCryptContextImpl(final mongocrypt_ctx_t wrapped) { + notNull("wrapped", wrapped); + this.wrapped = wrapped; + } + + @Override + public State getState() { + isTrue("open", !closed); + return State.fromIndex(mongocrypt_ctx_state(wrapped)); + } + + @Override + public RawBsonDocument getMongoOperation() { + isTrue("open", !closed); + mongocrypt_binary_t binary = mongocrypt_binary_new(); + + try { + boolean success = mongocrypt_ctx_mongo_op(wrapped, binary); + if (!success) { + throwExceptionFromStatus(); + } + return toDocument(binary); + } finally { + mongocrypt_binary_destroy(binary); + } + } + + @Override + public void addMongoOperationResult(final BsonDocument document) { + isTrue("open", !closed); + + try (BinaryHolder binaryHolder = toBinary(document)) { + boolean success = mongocrypt_ctx_mongo_feed(wrapped, binaryHolder.getBinary()); + if (!success) { + throwExceptionFromStatus(); + } + } + } + + @Override + public void completeMongoOperation() { + isTrue("open", !closed); + boolean success = mongocrypt_ctx_mongo_done(wrapped); + if (!success) { + throwExceptionFromStatus(); + } + } + + @Override + public void provideKmsProviderCredentials(final BsonDocument credentialsDocument) { + try (BinaryHolder binaryHolder = toBinary(credentialsDocument)) { + boolean success = mongocrypt_ctx_provide_kms_providers(wrapped, binaryHolder.getBinary()); + if (!success) { + throwExceptionFromStatus(); + } + } + } + + @Override + public MongoKeyDecryptor nextKeyDecryptor() { + isTrue("open", !closed); + + mongocrypt_kms_ctx_t kmsContext = mongocrypt_ctx_next_kms_ctx(wrapped); + if (kmsContext == null) { + return null; + } + return new MongoKeyDecryptorImpl(kmsContext); + } + + @Override + public void completeKeyDecryptors() { + isTrue("open", !closed); + + boolean success = mongocrypt_ctx_kms_done(wrapped); + if (!success) { + throwExceptionFromStatus(); + } + + } + + @Override + public RawBsonDocument finish() { + isTrue("open", !closed); + + mongocrypt_binary_t binary = mongocrypt_binary_new(); + + try { + boolean success = mongocrypt_ctx_finalize(wrapped, binary); + if (!success) { + throwExceptionFromStatus(); + } + return toDocument(binary); + } finally { + mongocrypt_binary_destroy(binary); + } + } + + @Override + public void close() { + mongocrypt_ctx_destroy(wrapped); + closed = true; + } + + static void throwExceptionFromStatus(final mongocrypt_ctx_t wrapped) { + mongocrypt_status_t status = mongocrypt_status_new(); + mongocrypt_ctx_status(wrapped, status); + MongoCryptException e = new MongoCryptException(status); + mongocrypt_status_destroy(status); + throw e; + } + + private void throwExceptionFromStatus() { + throwExceptionFromStatus(wrapped); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptException.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptException.java new file mode 100644 index 00000000000..63074e20bc9 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptException.java @@ -0,0 +1,67 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + + +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; + +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_code; +import static org.bson.assertions.Assertions.isTrue; + +/** + * Top level Exception for all Mongo Crypt CAPI exceptions + */ +public class MongoCryptException extends RuntimeException { + private static final long serialVersionUID = -5524416583514807953L; + private final int code; + + /** + * @param msg the message + */ + public MongoCryptException(final String msg) { + super(msg); + this.code = -1; + } + + /** + * @param msg the message + * @param cause the cause + */ + public MongoCryptException(final String msg, final Throwable cause) { + super(msg, cause); + this.code = -1; + } + + /** + * Construct an instance from a {@code mongocrypt_status_t}. + * + * @param status the status + */ + MongoCryptException(final mongocrypt_status_t status) { + super(CAPI.mongocrypt_status_message(status, null).toString()); + isTrue("status not ok", !CAPI.mongocrypt_status_ok(status)); + code = mongocrypt_status_code(status); + } + + /** + * @return the error code for the exception. + */ + public int getCode() { + return code; + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptImpl.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptImpl.java new file mode 100644 index 00000000000..2949e2a11e4 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptImpl.java @@ -0,0 +1,423 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_log_fn_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_t; +import com.sun.jna.Pointer; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonString; + +import javax.crypto.Cipher; +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_ERROR; +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_FATAL; +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_INFO; +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_TRACE; +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_LOG_LEVEL_WARNING; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_crypt_shared_lib_version_string; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_datakey_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_decrypt_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_encrypt_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_explicit_decrypt_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_explicit_encrypt_expression_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_explicit_encrypt_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_new; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_rewrap_many_datakey_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_algorithm; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_algorithm_range; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_contention_factor; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_alt_name; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_encryption_key; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_id; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_key_material; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_ctx_setopt_query_type; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_init; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_is_crypto_available; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_new; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_aes_256_ctr; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_append_crypt_shared_lib_search_path; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_bypass_query_analysis; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_crypto_hooks; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_encrypted_field_config_map; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_kms_provider_aws; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_kms_provider_local; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_kms_providers; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_log_handler; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_schema_map; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_set_crypt_shared_lib_path_override; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_setopt_use_need_kms_credentials_state; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_new; +import static com.mongodb.crypt.capi.CAPIHelper.toBinary; +import static org.bson.assertions.Assertions.isTrue; +import static org.bson.assertions.Assertions.notNull; + +class MongoCryptImpl implements MongoCrypt { + private static final Logger LOGGER = Loggers.getLogger(); + private final mongocrypt_t wrapped; + + // Keep a strong reference to all the callbacks so that they don't get garbage collected + @SuppressWarnings("FieldCanBeLocal") + private final LogCallback logCallback; + + @SuppressWarnings("FieldCanBeLocal") + private final CipherCallback aesCBC256EncryptCallback; + @SuppressWarnings("FieldCanBeLocal") + private final CipherCallback aesCBC256DecryptCallback; + @SuppressWarnings("FieldCanBeLocal") + private final CipherCallback aesCTR256EncryptCallback; + @SuppressWarnings("FieldCanBeLocal") + private final CipherCallback aesCTR256DecryptCallback; + @SuppressWarnings("FieldCanBeLocal") + private final MacCallback hmacSha512Callback; + @SuppressWarnings("FieldCanBeLocal") + private final MacCallback hmacSha256Callback; + @SuppressWarnings("FieldCanBeLocal") + private final MessageDigestCallback sha256Callback; + @SuppressWarnings("FieldCanBeLocal") + private final SecureRandomCallback secureRandomCallback; + @SuppressWarnings("FieldCanBeLocal") + private final SigningRSAESPKCSCallback signingRSAESPKCSCallback; + + private final AtomicBoolean closed; + + MongoCryptImpl(final MongoCryptOptions options) { + closed = new AtomicBoolean(); + wrapped = mongocrypt_new(); + if (wrapped == null) { + throw new MongoCryptException("Unable to create new mongocrypt object"); + } + + logCallback = new LogCallback(); + + configure(() -> mongocrypt_setopt_log_handler(wrapped, logCallback, null)); + + if (mongocrypt_is_crypto_available()) { + LOGGER.debug("libmongocrypt is compiled with cryptography support, so not registering Java callbacks"); + aesCBC256EncryptCallback = null; + aesCBC256DecryptCallback = null; + aesCTR256EncryptCallback = null; + aesCTR256DecryptCallback = null; + hmacSha512Callback = null; + hmacSha256Callback = null; + sha256Callback = null; + secureRandomCallback = null; + signingRSAESPKCSCallback = null; + } else { + LOGGER.debug("libmongocrypt is compiled without cryptography support, so registering Java callbacks"); + // We specify NoPadding here because the underlying C library is responsible for padding prior + // to executing the callback + aesCBC256EncryptCallback = new CipherCallback("AES", "AES/CBC/NoPadding", Cipher.ENCRYPT_MODE); + aesCBC256DecryptCallback = new CipherCallback("AES", "AES/CBC/NoPadding", Cipher.DECRYPT_MODE); + aesCTR256EncryptCallback = new CipherCallback("AES", "AES/CTR/NoPadding", Cipher.ENCRYPT_MODE); + aesCTR256DecryptCallback = new CipherCallback("AES", "AES/CTR/NoPadding", Cipher.DECRYPT_MODE); + + hmacSha512Callback = new MacCallback("HmacSHA512"); + hmacSha256Callback = new MacCallback("HmacSHA256"); + sha256Callback = new MessageDigestCallback("SHA-256"); + secureRandomCallback = new SecureRandomCallback(new SecureRandom()); + + configure(() -> mongocrypt_setopt_crypto_hooks(wrapped, aesCBC256EncryptCallback, aesCBC256DecryptCallback, + secureRandomCallback, hmacSha512Callback, hmacSha256Callback, + sha256Callback, null)); + + signingRSAESPKCSCallback = new SigningRSAESPKCSCallback(); + configure(() -> mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5(wrapped, signingRSAESPKCSCallback, null)); + configure(() -> mongocrypt_setopt_aes_256_ctr(wrapped, aesCTR256EncryptCallback, aesCTR256DecryptCallback, null)); + } + + if (options.getLocalKmsProviderOptions() != null) { + try (BinaryHolder localMasterKeyBinaryHolder = toBinary(options.getLocalKmsProviderOptions().getLocalMasterKey())) { + configure(() -> mongocrypt_setopt_kms_provider_local(wrapped, localMasterKeyBinaryHolder.getBinary())); + } + } + + if (options.getAwsKmsProviderOptions() != null) { + configure(() -> mongocrypt_setopt_kms_provider_aws(wrapped, + new cstring(options.getAwsKmsProviderOptions().getAccessKeyId()), -1, + new cstring(options.getAwsKmsProviderOptions().getSecretAccessKey()), -1)); + } + + if (options.isNeedsKmsCredentialsStateEnabled()) { + mongocrypt_setopt_use_need_kms_credentials_state(wrapped); + } + + if (options.getKmsProviderOptions() != null) { + try (BinaryHolder binaryHolder = toBinary(options.getKmsProviderOptions())) { + configure(() -> mongocrypt_setopt_kms_providers(wrapped, binaryHolder.getBinary())); + } + } + + if (options.getLocalSchemaMap() != null) { + BsonDocument localSchemaMapDocument = new BsonDocument(); + localSchemaMapDocument.putAll(options.getLocalSchemaMap()); + + try (BinaryHolder localSchemaMapBinaryHolder = toBinary(localSchemaMapDocument)) { + configure(() -> mongocrypt_setopt_schema_map(wrapped, localSchemaMapBinaryHolder.getBinary())); + } + } + + if (options.isBypassQueryAnalysis()) { + mongocrypt_setopt_bypass_query_analysis(wrapped); + } + + if (options.getEncryptedFieldsMap() != null) { + BsonDocument localEncryptedFieldsMap = new BsonDocument(); + localEncryptedFieldsMap.putAll(options.getEncryptedFieldsMap()); + + try (BinaryHolder localEncryptedFieldsMapHolder = toBinary(localEncryptedFieldsMap)) { + configure(() -> mongocrypt_setopt_encrypted_field_config_map(wrapped, localEncryptedFieldsMapHolder.getBinary())); + } + } + + options.getSearchPaths().forEach(p -> mongocrypt_setopt_append_crypt_shared_lib_search_path(wrapped, new cstring(p))); + if (options.getExtraOptions().containsKey("cryptSharedLibPath")) { + mongocrypt_setopt_set_crypt_shared_lib_path_override(wrapped, new cstring(options.getExtraOptions().getString("cryptSharedLibPath").getValue())); + } + + configure(() -> mongocrypt_init(wrapped)); + } + + @Override + public MongoCryptContext createEncryptionContext(final String database, final BsonDocument commandDocument) { + isTrue("open", !closed.get()); + notNull("database", database); + notNull("commandDocument", commandDocument); + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + + try (BinaryHolder commandDocumentBinaryHolder = toBinary(commandDocument)) { + configure(() -> mongocrypt_ctx_encrypt_init(context, new cstring(database), -1, + commandDocumentBinaryHolder.getBinary()), context); + return new MongoCryptContextImpl(context); + } + } + + @Override + public MongoCryptContext createDecryptionContext(final BsonDocument document) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + try (BinaryHolder documentBinaryHolder = toBinary(document)){ + configure(() -> mongocrypt_ctx_decrypt_init(context, documentBinaryHolder.getBinary()), context); + } + return new MongoCryptContextImpl(context); + } + + @Override + public MongoCryptContext createDataKeyContext(final String kmsProvider, final MongoDataKeyOptions options) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + + BsonDocument keyDocument = new BsonDocument("provider", new BsonString(kmsProvider)); + BsonDocument masterKey = options.getMasterKey(); + if (masterKey != null) { + masterKey.forEach(keyDocument::append); + } + try (BinaryHolder masterKeyHolder = toBinary(keyDocument)) { + configure(() -> mongocrypt_ctx_setopt_key_encryption_key(context, masterKeyHolder.getBinary()), context); + } + + if (options.getKeyAltNames() != null) { + for (String cur : options.getKeyAltNames()) { + try (BinaryHolder keyAltNameBinaryHolder = toBinary(new BsonDocument("keyAltName", new BsonString(cur)))) { + configure(() -> mongocrypt_ctx_setopt_key_alt_name(context, keyAltNameBinaryHolder.getBinary()), context); + } + } + } + + if (options.getKeyMaterial() != null) { + try (BinaryHolder keyMaterialBinaryHolder = toBinary(new BsonDocument("keyMaterial", new BsonBinary(options.getKeyMaterial())))) { + configure(() -> mongocrypt_ctx_setopt_key_material(context, keyMaterialBinaryHolder.getBinary()), context); + } + } + + if (!mongocrypt_ctx_datakey_init(context)) { + MongoCryptContextImpl.throwExceptionFromStatus(context); + } + return new MongoCryptContextImpl(context); + } + + @Override + public MongoCryptContext createExplicitEncryptionContext(final BsonDocument document, final MongoExplicitEncryptOptions options) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = configureExplicitEncryption(options); + + try (BinaryHolder documentBinaryHolder = toBinary(document)) { + configure(() -> mongocrypt_ctx_explicit_encrypt_init(context, documentBinaryHolder.getBinary()), context); + } + + return new MongoCryptContextImpl(context); + } + + @Override + public MongoCryptContext createEncryptExpressionContext(final BsonDocument document, final MongoExplicitEncryptOptions options) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = configureExplicitEncryption(options); + + try (BinaryHolder documentBinaryHolder = toBinary(document)) { + configure(() -> mongocrypt_ctx_explicit_encrypt_expression_init(context, documentBinaryHolder.getBinary()), context); + } + return new MongoCryptContextImpl(context); + } + + @Override + public MongoCryptContext createExplicitDecryptionContext(final BsonDocument document) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + try (BinaryHolder binaryHolder = toBinary(document)) { + configure(() -> mongocrypt_ctx_explicit_decrypt_init(context, binaryHolder.getBinary()), context); + } + return new MongoCryptContextImpl(context); + } + + @Override + public MongoCryptContext createRewrapManyDatakeyContext(final BsonDocument filter, final MongoRewrapManyDataKeyOptions options) { + isTrue("open", !closed.get()); + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + + if (options != null && options.getProvider() != null) { + BsonDocument keyDocument = new BsonDocument("provider", new BsonString(options.getProvider())); + BsonDocument masterKey = options.getMasterKey(); + if (masterKey != null) { + masterKey.forEach(keyDocument::append); + } + try (BinaryHolder binaryHolder = toBinary(keyDocument)) { + configure(() -> mongocrypt_ctx_setopt_key_encryption_key(context, binaryHolder.getBinary()), context); + } + } + + try (BinaryHolder binaryHolder = toBinary(filter)) { + configure(() -> mongocrypt_ctx_rewrap_many_datakey_init(context, binaryHolder.getBinary()), context); + } + return new MongoCryptContextImpl(context); + } + + @Override + public String getCryptSharedLibVersionString() { + cstring versionString = mongocrypt_crypt_shared_lib_version_string(wrapped, null); + return versionString == null ? null : versionString.toString(); + } + + @Override + public void close() { + if (!closed.getAndSet(true)) { + mongocrypt_destroy(wrapped); + } + } + + private mongocrypt_ctx_t configureExplicitEncryption(final MongoExplicitEncryptOptions options) { + mongocrypt_ctx_t context = mongocrypt_ctx_new(wrapped); + if (context == null) { + throwExceptionFromStatus(); + } + + if (options.getKeyId() != null) { + try (BinaryHolder keyIdBinaryHolder = toBinary(ByteBuffer.wrap(options.getKeyId().getData()))) { + configure(() -> mongocrypt_ctx_setopt_key_id(context, keyIdBinaryHolder.getBinary()), context); + } + } else if (options.getKeyAltName() != null) { + try (BinaryHolder keyAltNameBinaryHolder = toBinary(new BsonDocument("keyAltName", new BsonString(options.getKeyAltName())))) { + configure(() -> mongocrypt_ctx_setopt_key_alt_name(context, keyAltNameBinaryHolder.getBinary()), context); + } + } + + if (options.getAlgorithm() != null) { + configure(() -> mongocrypt_ctx_setopt_algorithm(context, new cstring(options.getAlgorithm()), -1), context); + } + if (options.getQueryType() != null) { + configure(() -> mongocrypt_ctx_setopt_query_type(context, new cstring(options.getQueryType()), -1), context); + } + if (options.getContentionFactor() != null) { + configure(() -> mongocrypt_ctx_setopt_contention_factor(context, options.getContentionFactor()), context); + } + if (options.getRangeOptions() != null) { + try (BinaryHolder rangeOptionsHolder = toBinary(options.getRangeOptions())) { + configure(() -> mongocrypt_ctx_setopt_algorithm_range(context, rangeOptionsHolder.getBinary()), context); + } + } + return context; + } + + + private void configure(final Supplier successSupplier) { + if (!successSupplier.get()) { + throwExceptionFromStatus(); + } + } + + private void configure(final Supplier successSupplier, final mongocrypt_ctx_t context) { + if (!successSupplier.get()) { + MongoCryptContextImpl.throwExceptionFromStatus(context); + } + } + + private void throwExceptionFromStatus() { + mongocrypt_status_t status = mongocrypt_status_new(); + mongocrypt_status(wrapped, status); + MongoCryptException e = new MongoCryptException(status); + mongocrypt_status_destroy(status); + throw e; + } + + static class LogCallback implements mongocrypt_log_fn_t { + @Override + public void log(final int level, final cstring message, final int messageLength, final Pointer ctx) { + if (level == MONGOCRYPT_LOG_LEVEL_FATAL) { + LOGGER.error(message.toString()); + } + if (level == MONGOCRYPT_LOG_LEVEL_ERROR) { + LOGGER.error(message.toString()); + } + if (level == MONGOCRYPT_LOG_LEVEL_WARNING) { + LOGGER.warn(message.toString()); + } + if (level == MONGOCRYPT_LOG_LEVEL_INFO) { + LOGGER.info(message.toString()); + } + if (level == MONGOCRYPT_LOG_LEVEL_TRACE) { + LOGGER.trace(message.toString()); + } + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptOptions.java new file mode 100644 index 00000000000..dc65bbdd9ae --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCryptOptions.java @@ -0,0 +1,284 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import org.bson.BsonDocument; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyList; +import static org.bson.assertions.Assertions.isTrue; + +/** + * The options for configuring MongoCrypt. + */ +public final class MongoCryptOptions { + + private final MongoAwsKmsProviderOptions awsKmsProviderOptions; + private final MongoLocalKmsProviderOptions localKmsProviderOptions; + private final BsonDocument kmsProviderOptions; + private final Map localSchemaMap; + private final boolean needsKmsCredentialsStateEnabled; + private final Map encryptedFieldsMap; + private final BsonDocument extraOptions; + private final boolean bypassQueryAnalysis; + private final List searchPaths; + + + /** + * Construct a builder for the options + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the AWS KMS provider options. + * + * @return the AWS KMS provider options, which may be null + */ + public MongoAwsKmsProviderOptions getAwsKmsProviderOptions() { + return awsKmsProviderOptions; + } + + /** + * Gets the local KMS provider options. + * + * @return the local KMS provider options, which may be null + */ + public MongoLocalKmsProviderOptions getLocalKmsProviderOptions() { + return localKmsProviderOptions; + } + + /** + * Returns the KMS provider options. + * + * @return the KMS provider options, which may be null + * @since 1.1 + */ + public BsonDocument getKmsProviderOptions() { + return kmsProviderOptions; + } + + /** + * Gets the local schema map. + * + * @return the local schema map + */ + public Map getLocalSchemaMap() { + return localSchemaMap; + } + + /** + * Gets whether the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS is enabled. Defaults to false + * + * @return whether the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS is enabled + * @since 1.4 + */ + public boolean isNeedsKmsCredentialsStateEnabled() { + return needsKmsCredentialsStateEnabled; + } + + /** + * Gets the encrypted fields map. + * + * @since 1.5 + * @return the encrypted fields map + */ + public Map getEncryptedFieldsMap() { + return encryptedFieldsMap; + } + + /** + * Gets whether automatic analysis of outgoing commands should be disabled. + * + * @since 1.5 + * @return true if bypassing query analysis + */ + public boolean isBypassQueryAnalysis() { + return bypassQueryAnalysis; + } + + /** + * The extraOptions that relate to the mongocryptd process or shared library. + * @return the extra options + * @since 1.5 + */ + public BsonDocument getExtraOptions() { + return extraOptions; + } + + /** + * Gets the search paths + * @return this + * @since 1.5 + */ + public List getSearchPaths() { + return searchPaths; + } + + /** + * The builder for the options + */ + public static final class Builder { + private MongoAwsKmsProviderOptions awsKmsProviderOptions; + private MongoLocalKmsProviderOptions localKmsProviderOptions; + private BsonDocument kmsProviderOptions = null; + private Map localSchemaMap = null; + private boolean needsKmsCredentialsStateEnabled; + private Map encryptedFieldsMap = null; + private boolean bypassQueryAnalysis; + private BsonDocument extraOptions = new BsonDocument(); + private List searchPaths = emptyList(); + + private Builder() { + } + + /** + * Sets the AWS KMS provider options. + * + * @param awsKmsProviderOptions the AWS KMS provider options + * @return this + */ + public Builder awsKmsProviderOptions(final MongoAwsKmsProviderOptions awsKmsProviderOptions) { + this.awsKmsProviderOptions = awsKmsProviderOptions; + return this; + } + + /** + * Sets the local KMS provider options. + * + * @param localKmsProviderOptions the local KMS provider options + * @return this + */ + public Builder localKmsProviderOptions(final MongoLocalKmsProviderOptions localKmsProviderOptions) { + this.localKmsProviderOptions = localKmsProviderOptions; + return this; + } + + /** + * Sets the KMS provider options. + * + * @param kmsProviderOptions the KMS provider options document + * @return this + * @since 1.1 + */ + public Builder kmsProviderOptions(final BsonDocument kmsProviderOptions) { + this.kmsProviderOptions = kmsProviderOptions; + return this; + } + + /** + * Sets the local schema map. + * + * @param localSchemaMap local schema map + * @return this + */ + public Builder localSchemaMap(final Map localSchemaMap) { + this.localSchemaMap = localSchemaMap; + return this; + } + + /** + * Sets whether the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS is enabled. Defaults to false + * + * @param needsKmsCredentialsStateEnabled whether the MONGOCRYPT_CTX_NEED_KMS_CREDENTIALS is enabled + * @return this + * @since 1.4 + */ + public Builder needsKmsCredentialsStateEnabled(final boolean needsKmsCredentialsStateEnabled) { + this.needsKmsCredentialsStateEnabled = needsKmsCredentialsStateEnabled; + return this; + } + + /** + * Sets the encrypted fields map. + * + * @param encryptedFieldsMap the encrypted fields map + * @since 1.5 + * @return this + */ + public Builder encryptedFieldsMap(final Map encryptedFieldsMap) { + this.encryptedFieldsMap = encryptedFieldsMap; + return this; + } + + /** + * Sets whether automatic analysis of outgoing commands should be disabled. + * + *

Set bypassQueryAnalysis to true to use explicit encryption on indexed fields + * without the MongoDB Enterprise Advanced licensed crypt shared library.

+ * + * @param bypassQueryAnalysis whether the analysis of outgoing commands should be disabled. + * @since 1.5 + * @return this + */ + public Builder bypassQueryAnalysis(final boolean bypassQueryAnalysis) { + this.bypassQueryAnalysis = bypassQueryAnalysis; + return this; + } + + /** + * The extraOptions that relate to the mongocryptd process or shared library. + * @param extraOptions the extraOptions + * @return this + * @since 1.5 + */ + public Builder extraOptions(final BsonDocument extraOptions) { + this.extraOptions = extraOptions; + return this; + } + + /** + * Sets search paths + * @param searchPaths sets search path + * @return this + * @since 1.5 + */ + public Builder searchPaths(final List searchPaths) { + this.searchPaths = searchPaths; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoCryptOptions build() { + return new MongoCryptOptions(this); + } + } + + private MongoCryptOptions(final Builder builder) { + isTrue("at least one KMS provider is configured", + builder.awsKmsProviderOptions != null || builder.localKmsProviderOptions != null + || builder.kmsProviderOptions != null); + this.awsKmsProviderOptions = builder.awsKmsProviderOptions; + this.localKmsProviderOptions = builder.localKmsProviderOptions; + this.kmsProviderOptions = builder.kmsProviderOptions; + this.localSchemaMap = builder.localSchemaMap; + this.needsKmsCredentialsStateEnabled = builder.needsKmsCredentialsStateEnabled; + this.encryptedFieldsMap = builder.encryptedFieldsMap; + this.bypassQueryAnalysis = builder.bypassQueryAnalysis; + this.extraOptions = builder.extraOptions; + this.searchPaths = builder.searchPaths; + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypts.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypts.java new file mode 100644 index 00000000000..683dcdf90f1 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoCrypts.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +/** + * The entry point to the MongoCrypt library. + */ +public final class MongoCrypts { + + private MongoCrypts() { + //NOP + } + + /** + * Create a {@code MongoCrypt} instance. + * + *

+ * Make sure that JNA is able to find the shared library, most likely by setting the jna.library.path system property + *

+ * + * @param options the options + * @return the instance + */ + public static MongoCrypt create(final MongoCryptOptions options) { + return new MongoCryptImpl(options); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoDataKeyOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoDataKeyOptions.java new file mode 100644 index 00000000000..27f62514aeb --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoDataKeyOptions.java @@ -0,0 +1,125 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import org.bson.BsonDocument; + +import java.util.List; + +/** + * The options for creation of a data key + */ +public final class MongoDataKeyOptions { + private final List keyAltNames; + private final BsonDocument masterKey; + private final byte[] keyMaterial; + + /** + * Options builder + */ + public static final class Builder { + private List keyAltNames; + private BsonDocument masterKey; + private byte[] keyMaterial; + + /** + * Add alternate key names + * @param keyAltNames the alternate key names + * @return this + */ + public Builder keyAltNames(final List keyAltNames) { + this.keyAltNames = keyAltNames; + return this; + } + + /** + * Add the master key. + * + * @param masterKey the master key + * @return this + */ + public Builder masterKey(final BsonDocument masterKey) { + this.masterKey = masterKey; + return this; + } + + /** + * Add the key material + * + * @param keyMaterial the optional custom key material for the data key + * @return this + * @since 1.5 + */ + public Builder keyMaterial(final byte[] keyMaterial) { + this.keyMaterial = keyMaterial; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoDataKeyOptions build() { + return new MongoDataKeyOptions(this); + } + } + + /** + * Create a builder for the options. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the alternate key names for the data key. + * + * @return the alternate key names + */ + public List getKeyAltNames() { + return keyAltNames; + } + + /** + * Gets the master key for the data key. + * + * @return the master key + */ + public BsonDocument getMasterKey() { + return masterKey; + } + + /** + * Gets the custom key material if set. + * + * @return the custom key material for the data key or null + * @since 1.5 + */ + public byte[] getKeyMaterial() { + return keyMaterial; + } + + private MongoDataKeyOptions(final Builder builder) { + keyAltNames = builder.keyAltNames; + masterKey = builder.masterKey; + keyMaterial = builder.keyMaterial; + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoExplicitEncryptOptions.java new file mode 100644 index 00000000000..2dad2182e7d --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoExplicitEncryptOptions.java @@ -0,0 +1,227 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import org.bson.BsonBinary; +import org.bson.BsonDocument; + +import java.util.Objects; + +/** + * Options for explicit encryption. + */ +public final class MongoExplicitEncryptOptions { + private final BsonBinary keyId; + private final String keyAltName; + private final String algorithm; + private final Long contentionFactor; + private final String queryType; + private final BsonDocument rangeOptions; + + /** + * The builder for the options + */ + public static final class Builder { + private BsonBinary keyId; + private String keyAltName; + private String algorithm; + private Long contentionFactor; + private String queryType; + private BsonDocument rangeOptions; + + private Builder() { + } + + /** + * Add the key identifier. + * + * @param keyId the key idenfifier + * @return this + */ + public Builder keyId(final BsonBinary keyId) { + this.keyId = keyId; + return this; + } + + /** + * Add the key alternative name. + * + * @param keyAltName the key alternative name + * @return this + */ + public Builder keyAltName(final String keyAltName) { + this.keyAltName = keyAltName; + return this; + } + + /** + * Add the encryption algorithm. + * + *

To insert or query with an "Indexed" encrypted payload, use a MongoClient configured with {@code AutoEncryptionSettings}. + * {@code AutoEncryptionSettings.bypassQueryAnalysis} may be true. + * {@code AutoEncryptionSettings.bypassAutoEncryption must be false}.

+ * + * @param algorithm the encryption algorithm + * @return this + */ + public Builder algorithm(final String algorithm) { + this.algorithm = algorithm; + return this; + } + + /** + * The contention factor. + * + *

It is an error to set contentionFactor when algorithm is not "Indexed". + * @param contentionFactor the contention factor + * @return this + * @since 1.5 + */ + public Builder contentionFactor(final Long contentionFactor) { + this.contentionFactor = contentionFactor; + return this; + } + + /** + * The QueryType. + * + *

It is an error to set queryType when algorithm is not "Indexed".

+ * + * @param queryType the query type + * @return this + * @since 1.5 + */ + public Builder queryType(final String queryType) { + this.queryType = queryType; + return this; + } + + /** + * The Range Options. + * + *

It is an error to set rangeOptions when the algorithm is not "range".

+ * + * @param rangeOptions the range options + * @return this + * @since 1.7 + */ + public Builder rangeOptions(final BsonDocument rangeOptions) { + this.rangeOptions = rangeOptions; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoExplicitEncryptOptions build() { + return new MongoExplicitEncryptOptions(this); + } + } + + /** + * Create a builder for the options. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the key identifier + * @return the key identifier + */ + public BsonBinary getKeyId() { + return keyId; + } + + /** + * Gets the key alternative name + * @return the key alternative name + */ + public String getKeyAltName() { + return keyAltName; + } + + /** + * Gets the encryption algorithm + * @return the encryption algorithm + */ + public String getAlgorithm() { + return algorithm; + } + + /** + * Gets the contention factor + * @return the contention factor + * @since 1.5 + */ + public Long getContentionFactor() { + return contentionFactor; + } + + /** + * Gets the query type + * @return the query type + * @since 1.5 + */ + public String getQueryType() { + return queryType; + } + + /** + * Gets the range options + * @return the range options + * @since 1.7 + */ + public BsonDocument getRangeOptions() { + return rangeOptions; + } + + private MongoExplicitEncryptOptions(final Builder builder) { + this.keyId = builder.keyId; + this.keyAltName = builder.keyAltName; + this.algorithm = builder.algorithm; + this.contentionFactor = builder.contentionFactor; + this.queryType = builder.queryType; + this.rangeOptions = builder.rangeOptions; + if (!(Objects.equals(algorithm, "Indexed") || Objects.equals(algorithm, "Range"))) { + if (contentionFactor != null) { + throw new IllegalStateException( + "Invalid configuration, contentionFactor can only be set if algorithm is 'Indexed' or 'Range'"); + } else if (queryType != null) { + throw new IllegalStateException( + "Invalid configuration, queryType can only be set if algorithm is 'Indexed' or 'Range'"); + } + } + } + + @Override + public String toString() { + return "MongoExplicitEncryptOptions{" + + "keyId=" + keyId + + ", keyAltName='" + keyAltName + '\'' + + ", algorithm='" + algorithm + '\'' + + ", contentionFactor=" + contentionFactor + + ", queryType='" + queryType + '\'' + + ", rangeOptions=" + rangeOptions + + '}'; + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptor.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptor.java new file mode 100644 index 00000000000..43a724348d6 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import java.nio.ByteBuffer; + +/** + * An interface representing a key decryption operation using a key management service. + */ +public interface MongoKeyDecryptor { + + /** + * Gets the name of the KMS provider, e.g. "aws" or "kmip" + * + * @return the KMS provider name + */ + String getKmsProvider(); + + /** + * Gets the host name of the key management service. + * + * @return the host name + */ + String getHostName(); + + /** + * Gets the message to send to the key management service. + * + *

+ * Clients should call this method first, and send the message on a TLS connection to a configured KMS server. + *

+ * + * @return the message to send + */ + ByteBuffer getMessage(); + + /** + * Gets the number of bytes that should be received from the KMS server. + * + *

+ * After sending the message to the KMS server, clients should call this method in a loop, receiving {@code bytesNeeded} from + * the KMS server and feeding those bytes to this decryptor, until {@code bytesNeeded} is 0. + *

+ * + * @return the actual number of bytes that clients should be prepared receive + */ + int bytesNeeded(); + + /** + * Feed the received bytes to the decryptor. + * + *

+ * After sending the message to the KMS server, clients should call this method in a loop, receiving the number of bytes indicated by + * a call to {@link #bytesNeeded()} from the KMS server and feeding those bytes to this decryptor, until {@link #bytesNeeded()} + * returns 0. + *

+ * + * @param bytes the received bytes + */ + void feed(ByteBuffer bytes); +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptorImpl.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptorImpl.java new file mode 100644 index 00000000000..cef14bf855f --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoKeyDecryptorImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.PointerByReference; + +import java.nio.ByteBuffer; + +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_binary_new; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_bytes_needed; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_endpoint; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_feed; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_get_kms_provider; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_message; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_kms_ctx_status; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_destroy; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_new; +import static com.mongodb.crypt.capi.CAPIHelper.toBinary; +import static com.mongodb.crypt.capi.CAPIHelper.toByteBuffer; +import static org.bson.assertions.Assertions.notNull; + +class MongoKeyDecryptorImpl implements MongoKeyDecryptor { + private final mongocrypt_kms_ctx_t wrapped; + + MongoKeyDecryptorImpl(final mongocrypt_kms_ctx_t wrapped) { + notNull("wrapped", wrapped); + this.wrapped = wrapped; + } + + @Override + public String getKmsProvider() { + return mongocrypt_kms_ctx_get_kms_provider(wrapped, null).toString(); + } + + @Override + public String getHostName() { + PointerByReference hostNamePointerByReference = new PointerByReference(); + boolean success = mongocrypt_kms_ctx_endpoint(wrapped, hostNamePointerByReference); + if (!success) { + throwExceptionFromStatus(); + } + Pointer hostNamePointer = hostNamePointerByReference.getValue(); + return hostNamePointer.getString(0); + } + + @Override + public ByteBuffer getMessage() { + mongocrypt_binary_t binary = mongocrypt_binary_new(); + + try { + boolean success = mongocrypt_kms_ctx_message(wrapped, binary); + if (!success) { + throwExceptionFromStatus(); + } + return toByteBuffer(binary); + } finally { + mongocrypt_binary_destroy(binary); + } + } + + @Override + public int bytesNeeded() { + return mongocrypt_kms_ctx_bytes_needed(wrapped); + } + + @Override + public void feed(final ByteBuffer bytes) { + try (BinaryHolder binaryHolder = toBinary(bytes)) { + boolean success = mongocrypt_kms_ctx_feed(wrapped, binaryHolder.getBinary()); + if (!success) { + throwExceptionFromStatus(); + } + } + } + + private void throwExceptionFromStatus() { + mongocrypt_status_t status = mongocrypt_status_new(); + mongocrypt_kms_ctx_status(wrapped, status); + MongoCryptException e = new MongoCryptException(status); + mongocrypt_status_destroy(status); + throw e; + } + +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoLocalKmsProviderOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoLocalKmsProviderOptions.java new file mode 100644 index 00000000000..be8eef09573 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoLocalKmsProviderOptions.java @@ -0,0 +1,83 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import java.nio.ByteBuffer; + +import static org.bson.assertions.Assertions.notNull; + +/** + * The options for configuring a local KMS provider. + */ +public final class MongoLocalKmsProviderOptions { + + private final ByteBuffer localMasterKey; + + /** + * Construct a builder for the options + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Gets the local master key + * + * @return the local master key + */ + public ByteBuffer getLocalMasterKey() { + return localMasterKey; + } + + /** + * The builder for the options + */ + public static final class Builder { + private ByteBuffer localMasterKey; + + private Builder() { + } + + /** + * Sets the local master key. + * + * @param localMasterKey the local master key + * @return this + */ + public Builder localMasterKey(final ByteBuffer localMasterKey) { + this.localMasterKey = localMasterKey; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoLocalKmsProviderOptions build() { + return new MongoLocalKmsProviderOptions(this); + } + } + + private MongoLocalKmsProviderOptions(final Builder builder) { + this.localMasterKey = notNull("Local KMS provider localMasterKey", builder.localMasterKey); + + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoRewrapManyDataKeyOptions.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoRewrapManyDataKeyOptions.java new file mode 100644 index 00000000000..0bfc6defa63 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/MongoRewrapManyDataKeyOptions.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.crypt.capi; + +import org.bson.BsonDocument; + +/** + * The rewrap many data key options + * + *

+ * The masterKey document MUST have the fields corresponding to the given provider as specified in masterKey. + *

+ * + * @since 1.5 + */ +public final class MongoRewrapManyDataKeyOptions { + + private final String provider; + private final BsonDocument masterKey; + + /** + * Options builder + */ + public static final class Builder { + private String provider; + private BsonDocument masterKey; + + /** + * The provider + * + * @param provider the provider + * @return this + */ + public Builder provider(final String provider) { + this.provider = provider; + return this; + } + + /** + * Add the master key. + * + * @param masterKey the master key + * @return this + */ + public Builder masterKey(final BsonDocument masterKey) { + this.masterKey = masterKey; + return this; + } + + /** + * Build the options. + * + * @return the options + */ + public MongoRewrapManyDataKeyOptions build() { + return new MongoRewrapManyDataKeyOptions(this); + } + } + + /** + * Create a builder for the options. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * @return the provider name + */ + public String getProvider() { + return provider; + } + + /** + * Gets the master key for the data key. + * + * @return the master key + */ + public BsonDocument getMasterKey() { + return masterKey; + } + + private MongoRewrapManyDataKeyOptions(final Builder builder) { + provider = builder.provider; + masterKey = builder.masterKey; + } +} + diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/SLF4JLogger.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SLF4JLogger.java new file mode 100644 index 00000000000..23064f8bf85 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SLF4JLogger.java @@ -0,0 +1,110 @@ + +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import org.slf4j.LoggerFactory; + +class SLF4JLogger implements Logger { + + private final org.slf4j.Logger delegate; + + SLF4JLogger(final String name) { + this.delegate = LoggerFactory.getLogger(name); + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public boolean isTraceEnabled() { + return delegate.isTraceEnabled(); + } + + @Override + public void trace(final String msg) { + delegate.trace(msg); + } + + @Override + public void trace(final String msg, final Throwable t) { + delegate.trace(msg, t); + } + + @Override + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public void debug(final String msg) { + delegate.debug(msg); + } + + @Override + public void debug(final String msg, final Throwable t) { + delegate.debug(msg, t); + } + + @Override + public boolean isInfoEnabled() { + return delegate.isInfoEnabled(); + } + + @Override + public void info(final String msg) { + delegate.info(msg); + } + + @Override + public void info(final String msg, final Throwable t) { + delegate.info(msg, t); + } + + @Override + public boolean isWarnEnabled() { + return delegate.isWarnEnabled(); + } + + @Override + public void warn(final String msg) { + delegate.warn(msg); + } + + @Override + public void warn(final String msg, final Throwable t) { + delegate.warn(msg, t); + } + + @Override + public boolean isErrorEnabled() { + return delegate.isErrorEnabled(); + } + + @Override + public void error(final String msg) { + delegate.error(msg); + } + + @Override + public void error(final String msg, final Throwable t) { + delegate.error(msg, t); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/SecureRandomCallback.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SecureRandomCallback.java new file mode 100644 index 00000000000..0a2a83c02f7 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SecureRandomCallback.java @@ -0,0 +1,51 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_random_fn; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; + +import java.security.SecureRandom; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_STATUS_ERROR_CLIENT; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_set; +import static com.mongodb.crypt.capi.CAPIHelper.writeByteArrayToBinary; + +class SecureRandomCallback implements mongocrypt_random_fn { + private final SecureRandom secureRandom; + + SecureRandomCallback(final SecureRandom secureRandom) { + this.secureRandom = secureRandom; + } + + @Override + public boolean random(final Pointer ctx, final mongocrypt_binary_t out, final int count, final mongocrypt_status_t status) { + try { + byte[] randomBytes = new byte[count]; + secureRandom.nextBytes(randomBytes); + writeByteArrayToBinary(out, randomBytes); + return true; + } catch (Exception e) { + mongocrypt_status_set(status, MONGOCRYPT_STATUS_ERROR_CLIENT, 0, new cstring(e.toString()), -1); + return false; + } + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/SigningRSAESPKCSCallback.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SigningRSAESPKCSCallback.java new file mode 100644 index 00000000000..a5b7ac9f050 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/SigningRSAESPKCSCallback.java @@ -0,0 +1,73 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.CAPI.cstring; +import com.mongodb.crypt.capi.CAPI.mongocrypt_binary_t; +import com.mongodb.crypt.capi.CAPI.mongocrypt_hmac_fn; +import com.mongodb.crypt.capi.CAPI.mongocrypt_status_t; +import com.sun.jna.Pointer; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; + +import static com.mongodb.crypt.capi.CAPI.MONGOCRYPT_STATUS_ERROR_CLIENT; +import static com.mongodb.crypt.capi.CAPI.mongocrypt_status_set; +import static com.mongodb.crypt.capi.CAPIHelper.toByteArray; +import static com.mongodb.crypt.capi.CAPIHelper.writeByteArrayToBinary; + +class SigningRSAESPKCSCallback implements mongocrypt_hmac_fn { + + private static final String KEY_ALGORITHM = "RSA"; + private static final String SIGN_ALGORITHM = "SHA256withRSA"; + + SigningRSAESPKCSCallback() { + } + + @Override + public boolean hmac(final Pointer ctx, final mongocrypt_binary_t key, final mongocrypt_binary_t in, + final mongocrypt_binary_t out, final mongocrypt_status_t status) { + try { + byte[] result = getSignature(toByteArray(key), toByteArray(in)); + writeByteArrayToBinary(out, result); + return true; + } catch (Exception e) { + mongocrypt_status_set(status, MONGOCRYPT_STATUS_ERROR_CLIENT, 0, new cstring(e.toString()), -1); + return false; + } + } + + static byte[] getSignature(final byte[] privateKeyBytes, final byte[] dataToSign) throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { + KeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey privateKey = keyFactory.generatePrivate(keySpec); + + Signature privateSignature = Signature.getInstance(SIGN_ALGORITHM); + privateSignature.initSign(privateKey); + privateSignature.update(dataToSign); + + return privateSignature.sign(); + } +} diff --git a/mongodb-crypt/src/main/com/mongodb/crypt/capi/package-info.java b/mongodb-crypt/src/main/com/mongodb/crypt/capi/package-info.java new file mode 100644 index 00000000000..c1c9060de33 --- /dev/null +++ b/mongodb-crypt/src/main/com/mongodb/crypt/capi/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * The mongocrypt API package + */ +package com.mongodb.crypt.capi; diff --git a/mongodb-crypt/src/main/resources/META-INF/native-image/jni-config.json b/mongodb-crypt/src/main/resources/META-INF/native-image/jni-config.json new file mode 100644 index 00000000000..44e398cb556 --- /dev/null +++ b/mongodb-crypt/src/main/resources/META-INF/native-image/jni-config.json @@ -0,0 +1,180 @@ +[ +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_crypto_fn", + "methods":[{"name":"crypt","parameterTypes":["com.sun.jna.Pointer","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.sun.jna.Pointer","com.mongodb.crypt.capi.CAPI$mongocrypt_status_t"] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_hash_fn", + "methods":[{"name":"hash","parameterTypes":["com.sun.jna.Pointer","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_status_t"] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_hmac_fn", + "methods":[{"name":"hmac","parameterTypes":["com.sun.jna.Pointer","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","com.mongodb.crypt.capi.CAPI$mongocrypt_status_t"] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_log_fn_t", + "methods":[{"name":"log","parameterTypes":["int","com.mongodb.crypt.capi.CAPI$cstring","int","com.sun.jna.Pointer"] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_random_fn", + "methods":[{"name":"random","parameterTypes":["com.sun.jna.Pointer","com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t","int","com.mongodb.crypt.capi.CAPI$mongocrypt_status_t"] }] +}, +{ + "name":"com.sun.jna.Callback" +}, +{ + "name":"com.sun.jna.CallbackReference", + "methods":[{"name":"getCallback","parameterTypes":["java.lang.Class","com.sun.jna.Pointer","boolean"] }, {"name":"getFunctionPointer","parameterTypes":["com.sun.jna.Callback","boolean"] }, {"name":"getNativeString","parameterTypes":["java.lang.Object","boolean"] }, {"name":"initializeThread","parameterTypes":["com.sun.jna.Callback","com.sun.jna.CallbackReference$AttachOptions"] }] +}, +{ + "name":"com.sun.jna.CallbackReference$AttachOptions" +}, +{ + "name":"com.sun.jna.FromNativeConverter", + "methods":[{"name":"nativeType","parameterTypes":[] }] +}, +{ + "name":"com.sun.jna.IntegerType", + "fields":[{"name":"value"}] +}, +{ + "name":"com.sun.jna.JNIEnv" +}, +{ + "name":"com.sun.jna.Native", + "methods":[{"name":"dispose","parameterTypes":[] }, {"name":"fromNative","parameterTypes":["com.sun.jna.FromNativeConverter","java.lang.Object","java.lang.reflect.Method"] }, {"name":"fromNative","parameterTypes":["java.lang.Class","java.lang.Object"] }, {"name":"fromNative","parameterTypes":["java.lang.reflect.Method","java.lang.Object"] }, {"name":"nativeType","parameterTypes":["java.lang.Class"] }, {"name":"toNative","parameterTypes":["com.sun.jna.ToNativeConverter","java.lang.Object"] }] +}, +{ + "name":"com.sun.jna.Native$ffi_callback", + "methods":[{"name":"invoke","parameterTypes":["long","long","long"] }] +}, +{ + "name":"com.sun.jna.NativeMapped", + "methods":[{"name":"toNative","parameterTypes":[] }] +}, +{ + "name":"com.sun.jna.Pointer", + "fields":[{"name":"peer"}], + "methods":[{"name":"","parameterTypes":["long"] }] +}, +{ + "name":"com.sun.jna.PointerType", + "fields":[{"name":"pointer"}] +}, +{ + "name":"com.sun.jna.Structure", + "fields":[{"name":"memory"}, {"name":"typeInfo"}], + "methods":[{"name":"autoRead","parameterTypes":[] }, {"name":"autoWrite","parameterTypes":[] }, {"name":"getTypeInfo","parameterTypes":[] }, {"name":"newInstance","parameterTypes":["java.lang.Class","long"] }] +}, +{ + "name":"com.sun.jna.Structure$ByValue" +}, +{ + "name":"com.sun.jna.Structure$FFIType$FFITypes", + "fields":[{"name":"ffi_type_double"}, {"name":"ffi_type_float"}, {"name":"ffi_type_longdouble"}, {"name":"ffi_type_pointer"}, {"name":"ffi_type_sint16"}, {"name":"ffi_type_sint32"}, {"name":"ffi_type_sint64"}, {"name":"ffi_type_sint8"}, {"name":"ffi_type_uint16"}, {"name":"ffi_type_uint32"}, {"name":"ffi_type_uint64"}, {"name":"ffi_type_uint8"}, {"name":"ffi_type_void"}] +}, +{ + "name":"com.sun.jna.WString", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Boolean", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["boolean"] }, {"name":"getBoolean","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Byte", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["byte"] }] +}, +{ + "name":"java.lang.Character", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["char"] }] +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"getComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.Double", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["double"] }] +}, +{ + "name":"java.lang.Float", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["float"] }] +}, +{ + "name":"java.lang.Integer", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["int"] }] +}, +{ + "name":"java.lang.Long", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["long"] }] +}, +{ + "name":"java.lang.Object", + "methods":[{"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.Short", + "fields":[{"name":"TYPE"}, {"name":"value"}], + "methods":[{"name":"","parameterTypes":["short"] }] +}, +{ + "name":"java.lang.String", + "methods":[{"name":"","parameterTypes":["byte[]"] }, {"name":"","parameterTypes":["byte[]","java.lang.String"] }, {"name":"getBytes","parameterTypes":[] }, {"name":"getBytes","parameterTypes":["java.lang.String"] }, {"name":"lastIndexOf","parameterTypes":["int"] }, {"name":"substring","parameterTypes":["int"] }, {"name":"toCharArray","parameterTypes":[] }] +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getProperty","parameterTypes":["java.lang.String"] }, {"name":"setProperty","parameterTypes":["java.lang.String","java.lang.String"] }] +}, +{ + "name":"java.lang.UnsatisfiedLinkError", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Void", + "fields":[{"name":"TYPE"}] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getParameterTypes","parameterTypes":[] }, {"name":"getReturnType","parameterTypes":[] }] +}, +{ + "name":"java.nio.Buffer", + "methods":[{"name":"position","parameterTypes":[] }] +}, +{ + "name":"java.nio.ByteBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.CharBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.DoubleBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.FloatBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.IntBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.LongBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +}, +{ + "name":"java.nio.ShortBuffer", + "methods":[{"name":"array","parameterTypes":[] }, {"name":"arrayOffset","parameterTypes":[] }] +} +] diff --git a/mongodb-crypt/src/main/resources/META-INF/native-image/reflect-config.json b/mongodb-crypt/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 00000000000..4187c0e8eab --- /dev/null +++ b/mongodb-crypt/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,134 @@ +[ +{ + "name":"com.mongodb.crypt.capi.CAPI", + "allPublicFields":true, + "queryAllDeclaredMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$cstring", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_binary_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_crypto_fn", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_ctx_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_hash_fn", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_hmac_fn", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_kms_ctx_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_log_fn_t", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_random_fn", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_status_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.mongodb.crypt.capi.CAPI$mongocrypt_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.jna.CallbackProxy", + "methods":[{"name":"callback","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"com.sun.jna.Pointer", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}] +}, +{ + "name":"com.sun.jna.Structure$FFIType", + "allDeclaredFields":true, + "queryAllPublicConstructors":true, + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.jna.Structure$FFIType$size_t", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.jna.ptr.PointerByReference", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}], + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"boolean", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}] +}, +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.HmacCore$HmacSHA512", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"int", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}] +}, +{ + "name":"java.lang.Throwable", + "methods":[{"name":"addSuppressed","parameterTypes":["java.lang.Throwable"] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"isVarArgs","parameterTypes":[] }] +}, +{ + "name":"java.nio.Buffer" +}, +{ + "name":"long", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA2$SHA256", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.SHA5$SHA512", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"void", + "fields":[{"name":"OPTIONS"}, {"name":"STRING_ENCODING"}, {"name":"STRUCTURE_ALIGNMENT"}, {"name":"TYPE_MAPPER"}] +}, +{ + "name":"org.slf4j.Logger" +} +] diff --git a/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java b/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java new file mode 100644 index 00000000000..87fbab2e82f --- /dev/null +++ b/mongodb-crypt/src/test/java/com/mongodb/crypt/capi/MongoCryptTest.java @@ -0,0 +1,388 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.crypt.capi; + +import com.mongodb.crypt.capi.MongoCryptContext.State; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.RawBsonDocument; +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +@SuppressWarnings("SameParameterValue") +public class MongoCryptTest { + @Test + public void testEncrypt() throws URISyntaxException, IOException { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + MongoCryptContext encryptor = mongoCrypt.createEncryptionContext("test", getResourceAsDocument("command.json")); + + assertEquals(State.NEED_MONGO_COLLINFO, encryptor.getState()); + + BsonDocument listCollectionsFilter = encryptor.getMongoOperation(); + assertEquals(getResourceAsDocument("list-collections-filter.json"), listCollectionsFilter); + + encryptor.addMongoOperationResult(getResourceAsDocument("collection-info.json")); + encryptor.completeMongoOperation(); + assertEquals(State.NEED_MONGO_MARKINGS, encryptor.getState()); + + BsonDocument jsonSchema = encryptor.getMongoOperation(); + assertEquals(getResourceAsDocument("mongocryptd-command.json"), jsonSchema); + + encryptor.addMongoOperationResult(getResourceAsDocument("mongocryptd-reply.json")); + encryptor.completeMongoOperation(); + assertEquals(State.NEED_MONGO_KEYS, encryptor.getState()); + + testKeyDecryptor(encryptor); + + assertEquals(State.READY, encryptor.getState()); + + RawBsonDocument encryptedDocument = encryptor.finish(); + assertEquals(State.DONE, encryptor.getState()); + assertEquals(getResourceAsDocument("encrypted-command.json"), encryptedDocument); + + encryptor.close(); + + mongoCrypt.close(); + } + + + @Test + public void testDecrypt() throws IOException, URISyntaxException { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + MongoCryptContext decryptor = mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json")); + + assertEquals(State.NEED_MONGO_KEYS, decryptor.getState()); + + testKeyDecryptor(decryptor); + + assertEquals(State.READY, decryptor.getState()); + + RawBsonDocument decryptedDocument = decryptor.finish(); + assertEquals(State.DONE, decryptor.getState()); + assertEquals(getResourceAsDocument("command-reply.json"), decryptedDocument); + + decryptor.close(); + + mongoCrypt.close(); + } + + @Test + public void testEmptyAwsCredentials() throws URISyntaxException, IOException { + MongoCrypt mongoCrypt = MongoCrypts.create(MongoCryptOptions + .builder() + .kmsProviderOptions(new BsonDocument("aws", new BsonDocument())) + .needsKmsCredentialsStateEnabled(true) + .build()); + + MongoCryptContext decryptor = mongoCrypt.createDecryptionContext(getResourceAsDocument("encrypted-command-reply.json")); + + assertEquals(State.NEED_KMS_CREDENTIALS, decryptor.getState()); + + BsonDocument awsCredentials = new BsonDocument(); + awsCredentials.put("accessKeyId", new BsonString("example")); + awsCredentials.put("secretAccessKey", new BsonString("example")); + + decryptor.provideKmsProviderCredentials(new BsonDocument("aws", awsCredentials)); + + assertEquals(State.NEED_MONGO_KEYS, decryptor.getState()); + + mongoCrypt.close(); + } + + @Test + public void testMultipleCloseCalls() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + mongoCrypt.close(); + mongoCrypt.close(); + } + + @Test + public void testDataKeyCreation() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + List keyAltNames = Arrays.asList("first", "second"); + MongoCryptContext dataKeyContext = mongoCrypt.createDataKeyContext("local", + MongoDataKeyOptions.builder().masterKey(new BsonDocument()) + .keyAltNames(keyAltNames) + .build()); + assertEquals(State.READY, dataKeyContext.getState()); + + RawBsonDocument dataKeyDocument = dataKeyContext.finish(); + assertEquals(State.DONE, dataKeyContext.getState()); + assertNotNull(dataKeyDocument); + + List actualKeyAltNames = dataKeyDocument.getArray("keyAltNames").stream() + .map(bsonValue -> bsonValue.asString().getValue()) + .sorted() + .collect(Collectors.toList()); + assertIterableEquals(keyAltNames, actualKeyAltNames); + dataKeyContext.close(); + mongoCrypt.close(); + } + + @Test + public void testExplicitEncryptionDecryption() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello")); + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() + .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("YWFhYWFhYWFhYWFhYWFhYQ=="))) + .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") + .build(); + MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options); + assertEquals(State.NEED_MONGO_KEYS, encryptor.getState()); + + testKeyDecryptor(encryptor); + + assertEquals(State.READY, encryptor.getState()); + + RawBsonDocument encryptedDocument = encryptor.finish(); + assertEquals(State.DONE, encryptor.getState()); + assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument); + + MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument); + + assertEquals(State.READY, decryptor.getState()); + + RawBsonDocument decryptedDocument = decryptor.finish(); + assertEquals(State.DONE, decryptor.getState()); + assertEquals(documentToEncrypt, decryptedDocument); + + encryptor.close(); + + mongoCrypt.close(); + } + + + @Test + public void testExplicitExpressionEncryption() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json"); + BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json"); + BsonDocument expectedEncryptedPayload = getResourceAsDocument("fle2-find-range-explicit-v2/int32/encrypted-payload.json"); + + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() + .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg=="))) + .algorithm("Range") + .queryType("range") + .contentionFactor(4L) + .rangeOptions(rangeOptions) + .build(); + MongoCryptContext encryptor = mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options); + assertEquals(State.NEED_MONGO_KEYS, encryptor.getState()); + + testKeyDecryptor(encryptor, "fle2-find-range-explicit-v2/int32/key-filter.json", "keys/ABCDEFAB123498761234123456789012-local-document.json"); + + assertEquals(State.READY, encryptor.getState()); + + RawBsonDocument actualEncryptedPayload = encryptor.finish(); + assertEquals(State.DONE, encryptor.getState()); + assertEquals(expectedEncryptedPayload, actualEncryptedPayload); + + encryptor.close(); + mongoCrypt.close(); + } + + @Test + public void testRangePreviewQueryTypeIsNotSupported() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + BsonDocument valueToEncrypt = getResourceAsDocument("fle2-find-range-explicit-v2/int32/value-to-encrypt.json"); + BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json"); + + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() + .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg=="))) + .algorithm("Range") + .queryType("rangePreview") + .contentionFactor(4L) + .rangeOptions(rangeOptions) + .build(); + + MongoCryptException exp = assertThrows(MongoCryptException.class, () -> mongoCrypt.createEncryptExpressionContext(valueToEncrypt, options)); + assertEquals("Query type 'rangePreview' is deprecated, please use 'range'", exp.getMessage()); + mongoCrypt.close(); + } + + @Test + public void testRangePreviewAlgorithmIsNotSupported() { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + BsonDocument rangeOptions = getResourceAsDocument("fle2-find-range-explicit-v2/int32/rangeopts.json"); + + IllegalStateException illegalStateException = assertThrows(IllegalStateException.class, () -> MongoExplicitEncryptOptions.builder() + .keyId(new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("q83vqxI0mHYSNBI0VniQEg=="))) + .algorithm("RangePreview") + .queryType("range") + .contentionFactor(4L) + .rangeOptions(rangeOptions) + .build()); + + assertEquals("Invalid configuration, contentionFactor can only be set if algorithm is 'Indexed' or 'Range'", + illegalStateException.getMessage()); + mongoCrypt.close(); + } + + @Test + public void testExplicitEncryptionDecryptionKeyAltName() throws IOException, URISyntaxException { + MongoCrypt mongoCrypt = createMongoCrypt(); + assertNotNull(mongoCrypt); + + BsonDocument documentToEncrypt = new BsonDocument("v", new BsonString("hello")); + MongoExplicitEncryptOptions options = MongoExplicitEncryptOptions.builder() + .keyAltName("altKeyName") + .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") + .build(); + MongoCryptContext encryptor = mongoCrypt.createExplicitEncryptionContext(documentToEncrypt, options); + + assertEquals(State.NEED_MONGO_KEYS, encryptor.getState()); + testKeyDecryptor(encryptor, "key-filter-keyAltName.json", "key-document.json"); + + assertEquals(State.READY, encryptor.getState()); + + RawBsonDocument encryptedDocument = encryptor.finish(); + assertEquals(State.DONE, encryptor.getState()); + assertEquals(getResourceAsDocument("encrypted-value.json"), encryptedDocument); + + MongoCryptContext decryptor = mongoCrypt.createExplicitDecryptionContext(encryptedDocument); + + assertEquals(State.READY, decryptor.getState()); + + RawBsonDocument decryptedDocument = decryptor.finish(); + assertEquals(State.DONE, decryptor.getState()); + assertEquals(documentToEncrypt, decryptedDocument); + + encryptor.close(); + + mongoCrypt.close(); + } + + private void testKeyDecryptor(final MongoCryptContext context) { + testKeyDecryptor(context, "key-filter.json", "key-document.json"); + } + + private void testKeyDecryptor(final MongoCryptContext context, final String keyFilterPath, final String keyDocumentPath) { + BsonDocument keyFilter = context.getMongoOperation(); + assertEquals(getResourceAsDocument(keyFilterPath), keyFilter); + context.addMongoOperationResult(getResourceAsDocument(keyDocumentPath)); + context.completeMongoOperation(); + if (context.getState() == State.READY) { + return; + } + + assertEquals(State.NEED_KMS, context.getState()); + + MongoKeyDecryptor keyDecryptor = context.nextKeyDecryptor(); + assertEquals("aws", keyDecryptor.getKmsProvider()); + assertEquals("kms.us-east-1.amazonaws.com:443", keyDecryptor.getHostName()); + + ByteBuffer keyDecryptorMessage = keyDecryptor.getMessage(); + assertEquals(790, keyDecryptorMessage.remaining()); + + int bytesNeeded = keyDecryptor.bytesNeeded(); + assertEquals(1024, bytesNeeded); + + keyDecryptor.feed(getHttpResourceAsByteBuffer("kms-reply.txt")); + bytesNeeded = keyDecryptor.bytesNeeded(); + assertEquals(0, bytesNeeded); + + assertNull(context.nextKeyDecryptor()); + + context.completeKeyDecryptors(); + } + + private MongoCrypt createMongoCrypt() { + return MongoCrypts.create(MongoCryptOptions + .builder() + .awsKmsProviderOptions(MongoAwsKmsProviderOptions.builder() + .accessKeyId("example") + .secretAccessKey("example") + .build()) + .localKmsProviderOptions(MongoLocalKmsProviderOptions.builder() + .localMasterKey(ByteBuffer.wrap(new byte[96])) + .build()) + .build()); + } + + private static BsonDocument getResourceAsDocument(final String fileName) { + return BsonDocument.parse(getFileAsString(fileName, System.getProperty("line.separator"))); + } + + private static ByteBuffer getHttpResourceAsByteBuffer(final String fileName) { + return ByteBuffer.wrap(getFileAsString(fileName, "\r\n").getBytes(StandardCharsets.UTF_8)); + } + + private static String getFileAsString(final String fileName, final String lineSeparator) { + try { + URL resource = MongoCryptTest.class.getResource("/" + fileName); + if (resource == null) { + throw new RuntimeException("Could not find file " + fileName); + } + File file = new File(resource.toURI()); + StringBuilder stringBuilder = new StringBuilder(); + String line; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(Files.newInputStream(file.toPath()), StandardCharsets.UTF_8))) { + boolean first = true; + while ((line = reader.readLine()) != null) { + if (!first) { + stringBuilder.append(lineSeparator); + } + first = false; + stringBuilder.append(line); + } + } + return stringBuilder.toString(); + } catch (Throwable t) { + throw new RuntimeException("Could not parse file " + fileName, t); + } + } +} diff --git a/mongodb-crypt/src/test/resources/collection-info.json b/mongodb-crypt/src/test/resources/collection-info.json new file mode 100644 index 00000000000..3b9660938a3 --- /dev/null +++ b/mongodb-crypt/src/test/resources/collection-info.json @@ -0,0 +1,37 @@ +{ + "type": "collection", + "name": "test", + "idIndex": { + "ns": "test.test", + "name": "_id_", + "key": { + "_id": { + "$numberInt": "1" + } + }, + "v": { + "$numberInt": "2" + } + }, + "options": { + "validator": { + "$jsonSchema": { + "properties": { + "ssn": { + "encrypt": { + "keyId": { + "$binary": { + "base64": "YWFhYWFhYWFhYWFhYWFhYQ==", + "subType": "04" + } + }, + "type": "string", + "algorithm": "AEAD_AES_CBC_HMAC_SHA512-Deterministic" + } + } + }, + "bsonType": "object" + } + } + } +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/command-reply.json b/mongodb-crypt/src/test/resources/command-reply.json new file mode 100644 index 00000000000..c110f737f45 --- /dev/null +++ b/mongodb-crypt/src/test/resources/command-reply.json @@ -0,0 +1,13 @@ +{ + "cursor": { + "firstBatch": [ + { + "_id": 1, + "ssn": "457-55-5462" + } + ], + "id": 0, + "ns": "test.test" + }, + "ok": 1 +} diff --git a/mongodb-crypt/src/test/resources/command.json b/mongodb-crypt/src/test/resources/command.json new file mode 100644 index 00000000000..d04bf7799ad --- /dev/null +++ b/mongodb-crypt/src/test/resources/command.json @@ -0,0 +1,6 @@ +{ + "find": "test", + "filter": { + "ssn": "457-55-5462" + } +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/encrypted-command-reply.json b/mongodb-crypt/src/test/resources/encrypted-command-reply.json new file mode 100644 index 00000000000..73d4d3427ee --- /dev/null +++ b/mongodb-crypt/src/test/resources/encrypted-command-reply.json @@ -0,0 +1,16 @@ +{ + "cursor" : { + "firstBatch" : [ + { + "_id": 1, + "ssn": { + "$binary": "AWFhYWFhYWFhYWFhYWFhYWECRTOW9yZzNDn5dGwuqsrJQNLtgMEKaujhs9aRWRp+7Yo3JK8N8jC8P0Xjll6C1CwLsE/iP5wjOMhVv1KMMyOCSCrHorXRsb2IKPtzl2lKTqQ=", + "$type": "06" + } + } + ], + "id" : 0, + "ns" : "test.test" + }, + "ok" : 1 +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/encrypted-command.json b/mongodb-crypt/src/test/resources/encrypted-command.json new file mode 100644 index 00000000000..8b8cfaa27ee --- /dev/null +++ b/mongodb-crypt/src/test/resources/encrypted-command.json @@ -0,0 +1,11 @@ +{ + "filter": { + "ssn": { + "$binary": { + "base64": "AWFhYWFhYWFhYWFhYWFhYWECRTOW9yZzNDn5dGwuqsrJQNLtgMEKaujhs9aRWRp+7Yo3JK8N8jC8P0Xjll6C1CwLsE/iP5wjOMhVv1KMMyOCSCrHorXRsb2IKPtzl2lKTqQ=", + "subType": "06" + } + } + }, + "find": "test" +} diff --git a/mongodb-crypt/src/test/resources/encrypted-value.json b/mongodb-crypt/src/test/resources/encrypted-value.json new file mode 100644 index 00000000000..e1a832b5ecb --- /dev/null +++ b/mongodb-crypt/src/test/resources/encrypted-value.json @@ -0,0 +1,6 @@ +{ + "v": { + "$binary": "AWFhYWFhYWFhYWFhYWFhYWECW+zDjR/69eS6VtuMD5+O2lZw6JyiWOw3avI7mnUkdpKzPfvy8F/nlZrgZa2cGmQsb0TmLZuk5trldosnGKD91w==", + "$type": "06" + } +} diff --git a/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/encrypted-payload.json b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/encrypted-payload.json new file mode 100644 index 00000000000..7db5540ca1b --- /dev/null +++ b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/encrypted-payload.json @@ -0,0 +1,26 @@ +{ + "v": { + "$and": [ + { + "age": { + "$gte": { + "$binary": { + "base64": "DQECAAADcGF5bG9hZACZAQAABGcAhQEAAAMwAH0AAAAFZAAgAAAAAInd0noBhIiJMv8QTjcfgRqnnVhxRJRRACLfvgT+CTR/BXMAIAAAAADm0EjqF/T4EmR6Dw6NaPLrL0OuzS4AFvm90czFluAAygVsACAAAAAA5MXcYWjYlzhPFUDebBEa17B5z2bupmaW9uCdtLjc7RkAAzEAfQAAAAVkACAAAAAA7lkNtT6RLw91aJ07K/blwlFs5wi9pQjqUXDcaCTxe98FcwAgAAAAAPwySffuLQihmF70Ot93KtaUMNU8KpmA+niyPRcvarNMBWwAIAAAAACDv6fJXXwRqwZH3O2kO+hdeLZ36U6bMZSui8kv0PsPtAADMgB9AAAABWQAIAAAAACcMWVTbZC4ox5VdjWeYKLgf4oBjpPlbTTAkucm9JPK0wVzACAAAAAA3tIww4ZTytkxFsUKyJbc3zwQ2w7DhkOqaNvX9g8pi3gFbAAgAAAAAGs9XR3Q1JpxV+HPW8P2GvCuCBF5bGZ8Kl1zHqzZcd5/AAASY20ABAAAAAAAAAAAEHBheWxvYWRJZAAAAAAAEGZpcnN0T3BlcmF0b3IAAgAAABBzZWNvbmRPcGVyYXRvcgAEAAAAEnNwAAEAAAAAAAAAEHRmAAEAAAAQbW4AAAAAABBteADIAAAAAA==", + "subType": "06" + } + } + } + }, + { + "age": { + "$lte": { + "$binary": { + "base64": "DTsAAAAQcGF5bG9hZElkAAAAAAAQZmlyc3RPcGVyYXRvcgACAAAAEHNlY29uZE9wZXJhdG9yAAQAAAAA", + "subType": "06" + } + } + } + } + ] + } +} diff --git a/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/key-filter.json b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/key-filter.json new file mode 100644 index 00000000000..897364761c7 --- /dev/null +++ b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/key-filter.json @@ -0,0 +1,19 @@ +{ + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": "q83vqxI0mHYSNBI0VniQEg==", + "$type": "04" + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/rangeopts.json b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/rangeopts.json new file mode 100644 index 00000000000..2e1407fe4e6 --- /dev/null +++ b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/rangeopts.json @@ -0,0 +1,14 @@ +{ + "min": { + "$numberInt": "0" + }, + "max": { + "$numberInt": "200" + }, + "sparsity": { + "$numberLong": "1" + }, + "trimFactor": { + "$numberInt": "1" + } +} diff --git a/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/value-to-encrypt.json b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/value-to-encrypt.json new file mode 100644 index 00000000000..4c294e887e6 --- /dev/null +++ b/mongodb-crypt/src/test/resources/fle2-find-range-explicit-v2/int32/value-to-encrypt.json @@ -0,0 +1,20 @@ +{ + "v": { + "$and": [ + { + "age": { + "$gte": { + "$numberInt": "23" + } + } + }, + { + "age": { + "$lte": { + "$numberInt": "35" + } + } + } + ] + } +} diff --git a/mongodb-crypt/src/test/resources/json-schema.json b/mongodb-crypt/src/test/resources/json-schema.json new file mode 100644 index 00000000000..059373d9ca1 --- /dev/null +++ b/mongodb-crypt/src/test/resources/json-schema.json @@ -0,0 +1,15 @@ +{ + "properties": { + "ssn": { + "encrypt": { + "keyId": { + "$binary": "YWFhYWFhYWFhYWFhYWFhYQ==", + "$type": "04" + }, + "type": "string", + "algorithm": "AEAD_AES_CBC_HMAC_SHA512-Deterministic" + } + } + }, + "bsonType": "object" +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/key-document.json b/mongodb-crypt/src/test/resources/key-document.json new file mode 100644 index 00000000000..5414072596d --- /dev/null +++ b/mongodb-crypt/src/test/resources/key-document.json @@ -0,0 +1,36 @@ +{ + "status": { + "$numberInt": "1" + }, + "_id": { + "$binary": { + "base64": "YWFhYWFhYWFhYWFhYWFhYQ==", + "subType": "04" + } + }, + "masterKey": { + "region": "us-east-1", + "key": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0", + "provider": "aws" + }, + "updateDate": { + "$date": { + "$numberLong": "1557827033449" + } + }, + "keyMaterial": { + "$binary": { + "base64": "AQICAHhQNmWG2CzOm1dq3kWLM+iDUZhEqnhJwH9wZVpuZ94A8gEqnsxXlR51T5EbEVezUqqKAAAAwjCBvwYJKoZIhvcNAQcGoIGxMIGuAgEAMIGoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDHa4jo6yp0Z18KgbUgIBEIB74sKxWtV8/YHje5lv5THTl0HIbhSwM6EqRlmBiFFatmEWaeMk4tO4xBX65eq670I5TWPSLMzpp8ncGHMmvHqRajNBnmFtbYxN3E3/WjxmdbOOe+OXpnGJPcGsftc7cB2shRfA4lICPnE26+oVNXT6p0Lo20nY5XC7jyCO", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1557827033449" + } + }, + "keyAltNames": [ + "altKeyName", + "another_altname" + ] +} diff --git a/mongodb-crypt/src/test/resources/key-filter-keyAltName.json b/mongodb-crypt/src/test/resources/key-filter-keyAltName.json new file mode 100644 index 00000000000..eb53a142a14 --- /dev/null +++ b/mongodb-crypt/src/test/resources/key-filter-keyAltName.json @@ -0,0 +1,14 @@ +{ + "$or": [ + { + "_id": { + "$in": [] + } + }, + { + "keyAltNames": { + "$in": ["altKeyName"] + } + } + ] +} diff --git a/mongodb-crypt/src/test/resources/key-filter.json b/mongodb-crypt/src/test/resources/key-filter.json new file mode 100644 index 00000000000..9ad7c70e5a7 --- /dev/null +++ b/mongodb-crypt/src/test/resources/key-filter.json @@ -0,0 +1,19 @@ +{ + "$or": [ + { + "_id": { + "$in": [ + { + "$binary": "YWFhYWFhYWFhYWFhYWFhYQ==", + "$type": "04" + } + ] + } + }, + { + "keyAltNames": { + "$in": [] + } + } + ] +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/keys/ABCDEFAB123498761234123456789012-local-document.json b/mongodb-crypt/src/test/resources/keys/ABCDEFAB123498761234123456789012-local-document.json new file mode 100644 index 00000000000..e5d1a3f7661 --- /dev/null +++ b/mongodb-crypt/src/test/resources/keys/ABCDEFAB123498761234123456789012-local-document.json @@ -0,0 +1,30 @@ +{ + "_id": { + "$binary": { + "base64": "q83vqxI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "27OBvUqHAuYFy60nwCdvq2xmZ4kFzVySphXzBGq+HEot13comCoydEfnltBzLTuXLbV9cnREFJIO5f0jMqrlkxIuvAV8yO84p5VJTEa8j/xSNe7iA594rx7UeKT0fOt4VqM47fht8h+8PZYc5JVezvEMvwk115IBCwENxDjLtT0g+y8Hf+aTUEGtxrYToH8zf1/Y7S16mHiIc4jK3/vxHw==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648915408923" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648915408923" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } +} diff --git a/mongodb-crypt/src/test/resources/kms-reply.txt b/mongodb-crypt/src/test/resources/kms-reply.txt new file mode 100644 index 00000000000..c2c52e38413 --- /dev/null +++ b/mongodb-crypt/src/test/resources/kms-reply.txt @@ -0,0 +1,6 @@ +HTTP/1.1 200 OK +x-amzn-RequestId: deeb35e5-4ecb-4bf1-9af5-84a54ff0af0e +Content-Type: application/x-amz-json-1.1 +Content-Length: 233 + +{"KeyId": "arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0", "Plaintext": "TqhXy3tKckECjy4/ZNykMWG8amBF46isVPzeOgeusKrwheBmYaU8TMG5AHR/NeUDKukqo8hBGgogiQOVpLPkqBQHD8YkLsNbDmHoGOill5QAHnniF/Lz405bGucB5TfR"} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/list-collections-filter.json b/mongodb-crypt/src/test/resources/list-collections-filter.json new file mode 100644 index 00000000000..2f37dc5b093 --- /dev/null +++ b/mongodb-crypt/src/test/resources/list-collections-filter.json @@ -0,0 +1,3 @@ +{ + "name": "test" +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/mongocryptd-command.json b/mongodb-crypt/src/test/resources/mongocryptd-command.json new file mode 100644 index 00000000000..2ec0612d7e9 --- /dev/null +++ b/mongodb-crypt/src/test/resources/mongocryptd-command.json @@ -0,0 +1,22 @@ +{ + "find": "test", + "filter": { + "ssn": "457-55-5462" + }, + "jsonSchema": { + "properties": { + "ssn": { + "encrypt": { + "keyId": { + "$binary": "YWFhYWFhYWFhYWFhYWFhYQ==", + "$type": "04" + }, + "type": "string", + "algorithm": "AEAD_AES_CBC_HMAC_SHA512-Deterministic" + } + } + }, + "bsonType": "object" + }, + "isRemoteSchema": true +} \ No newline at end of file diff --git a/mongodb-crypt/src/test/resources/mongocryptd-reply.json b/mongodb-crypt/src/test/resources/mongocryptd-reply.json new file mode 100644 index 00000000000..0d1873de7e2 --- /dev/null +++ b/mongodb-crypt/src/test/resources/mongocryptd-reply.json @@ -0,0 +1,18 @@ +{ + "schemaRequiresEncryption": true, + "ok": { + "$numberInt": "1" + }, + "result": { + "filter": { + "ssn": { + "$binary": { + "base64": "ADgAAAAQYQABAAAABWtpABAAAAAEYWFhYWFhYWFhYWFhYWFhYQJ2AAwAAAA0NTctNTUtNTQ2MgAA", + "subType": "06" + } + } + }, + "find": "test" + }, + "hasEncryptedPlaceholders": true +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index ab252727079..b1c5e185d37 100644 --- a/settings.gradle +++ b/settings.gradle @@ -29,6 +29,7 @@ include ':driver-kotlin-sync' include ':driver-kotlin-coroutine' include ':bson-scala' include ':driver-scala' +include ':mongodb-crypt' include 'util:spock' include 'util:taglets' include ':graalvm-native-image-app'