Java Meets Queryable Encryption: Developing a Secure Bank Account Application
Ricardo Mello14 min read • Published Oct 02, 2024 • Updated Oct 08, 2024
FULL APPLICATION
Rate this tutorial
Encryption is essential for protecting data by converting readable information (plaintext) into an unintelligible form (ciphertext) using cryptographic techniques. This process ensures that, even if unauthorized parties gain access, they cannot interpret the data without the proper decryption key. It plays a vital role in safeguarding sensitive information like personal details, financial records, and confidential communications, whether the data is stored or in transit.
Traditional encryption methods, while securing data from unauthorized access, often require decrypting the data before it can be queried or analyzed. This can pose security risks since the data is exposed in its unencrypted form during processing. The goal is to find a way to perform queries on encrypted data without compromising its security.
MongoDB Queryable Encryption makes protecting your data easier than ever. It ensures your data stays encrypted, even while running queries. All the encryption and decryption happens on the client side, meaning the server never gets access to plain, readable data. This also applies to services like MongoDB Atlas and cloud providers — they only see encrypted data, which significantly boosts your data’s privacy and security. This protects you from external threats and ensures that database administrators, who have access to the database but not to the encryption keys, can’t view or understand your data. So, even if someone manages to break into the server or cloud infrastructure, all they’ll find is encrypted information that they can’t decrypt without the proper keys.
In this tutorial, we’ll create a Java application called "BankAccount" to demonstrate how Queryable Encryption works. Our goal is to build an app that saves and retrieves documents from a database named "digitalBank."
These documents will include:
- accountHolderName.
- accountNumber (encrypted).
- cardVerificationCode (encrypted).
- accountBalance (encrypted).
We’ll set up the application to create an endpoint that allows us to search for documents using two types of queries:
- Equality queries: To find documents that match a specific accountNumber
- Range queries: To find documents with values within a certain range (e.g., greater than a specified amount)
The Automatic Encryption feature will handle the encryption and decryption of data for us, so we can focus on building the application without needing to be encryption experts.
Here’s what you’ll need to follow along in this tutorial:
- Get started with MongoDB Atlas for free! If you don’t already have an account, MongoDB offers a free-forever Atlas cluster.
- IDE of your choice
Queryable Encryption is a MongoDB feature designed to enable secure queries on encrypted data. It allows encrypted fields to be queried using various operators, such as equality and range, while maintaining the confidentiality of the data.
With Queryable Encryption, you can:
- Encrypt sensitive fields directly from the client side.
- Execute expressive queries on the encrypted data.
These processes are carried out without the server having access to the unencrypted data. Sensitive information remains encrypted at all stages — in transit, at rest, during use, and in backups — and is only decrypted on the client side, where you retain control over the encryption keys.
This advanced encryption method allows for both secure storage and searching of data, developed by leading experts in the field. Importantly, the technology behind it has been published and thoroughly reviewed by peers, ensuring its robustness and credibility.
Additionally, this encryption solution is not only secure but also built to perform well in real-world scenarios, balancing both high security and practical usability.
The image above demonstrates the Queryable Encryption flow for the application we will implement
- The user will perform two queries using accountNumber and accountBalance.
- These queries will be detected by the MongoDB Java driver, which will identify that these fields are encrypted.
- The driver will then send an encrypted query along with a token to the MongoDB server.
- MongoDB will return the encrypted data from the database.
- The Java driver will decrypt the data and present the results to the user.
It’s important to note that the data remains encrypted at all times
in the database. Without the decryption key, even with root access,
it would be impossible to retrieve the information from the
encrypted fields.
To achieve our goal, we will develop a Spring Boot application that will provide endpoints for inserting and retrieving data. To do this, we will use Spring Initializr, as usual, to set up our application. Configure your application according to the image below.
At this stage, we will add some dependencies to the project and configure the automatic encryption model. To start, open the pom.xml file and include the following dependencies:
1 <dependency> 2 <groupId>org.projectlombok</groupId> 3 <artifactId>lombok</artifactId> 4 <optional>true</optional> 5 </dependency> 6 <dependency> 7 <groupId>org.mongodb</groupId> 8 <artifactId>mongodb-driver-sync</artifactId> 9 <version>5.1.3</version> 10 </dependency> 11 <dependency> 12 <groupId>org.mongodb</groupId> 13 <artifactId>mongodb-crypt</artifactId> 14 <version>1.11.0</version> 15 </dependency>
To enable automatic Queryable Encryption in our Java project, we need to configure settings in the application.properties file. This setup involves two key components:
- Automatic Encryption Shared Library: The Automatic Encryption Shared Library is crucial for managing encrypted data in your application. It handles encryption and decryption, ensures that only supported operations are performed on encrypted fields, and manages which fields are encrypted.
- Download the library: Obtain the Automatic Encryption Shared Library from the MongoDB Download Center. Select the appropriate version (e.g., 0.0) and platform. Choose the crypt_shared package and download it.
2. Key Management Service (KMS):
The KMS is responsible for managing and storing the encryption keys used to secure data. For this example, we are using a local KMS setup. However, for production environments, it is highly recommended to use a KMS service to ensure enhanced security and scalability.
Here are the settings you'll need to include in your application.properties file:
1 app.mongodb.uri=mongodb+srv://user:password@cluster0.cluster.mongodb.net/ 2 app.mongodb.kmsProviderName=local 3 app.mongodb.keyVaultNamespace=encryption.__keyVault 4 app.mongodb.encryptedDatabaseName=digitalBank 5 app.mongodb.encryptedCollectionName=accounts 6 app.mongodb.cryptSharedLibPath=/<path-to-lib>/mongo_crypt_v1.dylib
Understanding the configurations:
- app.mongodb.uri=YOUR_CONNECTION_STRING
- Specifies your MongoDB Atlas connection string
- app.mongodb.kmsProviderName=local
- Specifies the KMS provider name
- app.mongodb.keyVaultNamespace=encryption.__keyVault
- Defines the namespace for the key vault where encryption keys are stored. In this setup, keys are stored in the __keyVault collection within the encryption database. These keys are secured using the customer's key, so the server never sees them in an unencrypted state. This ensures that the keys remain protected and confidential.
- app.mongodb.encryptedDatabaseName=digitalBank
- Specifies the name of the database where encrypted data will be stored
- app.mongodb.encryptedCollectionName=accounts
- Indicates the collection within the database where encrypted account details will be held
- app.mongodb.cryptSharedLibPath=/PATH_TO_LIB/mongo_crypt_v1.dylib
- Points to the location of the Automatic Encryption Shared Library on your system. This dynamic library is essential for enabling automatic encryption and decryption. Replace /PATH_TO_LIB/ with the actual path to where the mongo_crypt_v1.dylib file is located on your machine.
In this section, we will configure the encryption-related classes to enable automatic encryption and decryption in our Spring Boot application using MongoDB. Our goal is to set up the necessary components to manage encrypted data securely. Create a new Java file named EncryptionConfig in the resources/config package:
1 package com.mongodb.bankaccount.resources.config; 2 3 import com.mongodb.AutoEncryptionSettings; 4 import com.mongodb.ClientEncryptionSettings; 5 import com.mongodb.ConnectionString; 6 import com.mongodb.MongoClientSettings; 7 import com.mongodb.client.MongoDatabase; 8 import lombok.Data; 9 import org.springframework.boot.ApplicationRunner; 10 import org.springframework.boot.context.properties.ConfigurationProperties; 11 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Configuration; 13 import java.io.File; 14 import java.io.FileInputStream; 15 import java.io.FileOutputStream; 16 import java.io.IOException; 17 import java.security.SecureRandom; 18 import java.util.HashMap; 19 import java.util.Map; 20 21 22 23 24 public class EncryptionConfig { 25 26 private String uri; 27 private String kmsProviderName; 28 private String keyVaultNamespace; 29 private String encryptedDatabaseName; 30 private String encryptedCollectionName; 31 private String cryptSharedLibPath; 32 33 private static final String CUSTOMER_KEY_PATH = "src/main/resources/customer-key.txt"; 34 private static final int KEY_SIZE = 96; 35 private final Map<String, Map<String, Object>> kmsProviderCredentials = new HashMap<>(); 36 37 38 public ApplicationRunner createEncryptedCollectionRunner(MongoDatabase db, EncryptionFieldConfig encryptionFieldConfig) { 39 return args -> { 40 if (!encryptionFieldConfig.collectionExists(db, encryptedCollectionName)) { 41 encryptionFieldConfig.createEncryptedCollection(db, clientEncryptionSettings()); 42 } 43 }; 44 } 45 46 private ClientEncryptionSettings clientEncryptionSettings() throws Exception { 47 return ClientEncryptionSettings.builder() 48 .keyVaultMongoClientSettings(getMongoClientSettings()) 49 .keyVaultNamespace(keyVaultNamespace) 50 .kmsProviders(kmsProviderCredentials) 51 .build(); 52 } 53 54 protected MongoClientSettings getMongoClientSettings() throws Exception { 55 kmsProviderCredentials.put("local", createLocalKmsProvider()); 56 57 AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() 58 .keyVaultNamespace(keyVaultNamespace) 59 .kmsProviders(kmsProviderCredentials) 60 .extraOptions(createExtraOptions()) 61 .build(); 62 63 return MongoClientSettings.builder() 64 .applyConnectionString(new ConnectionString(uri)) 65 .autoEncryptionSettings(autoEncryptionSettings) 66 .build(); 67 } 68 69 private Map<String, Object> createLocalKmsProvider() throws IOException { 70 if (!isCustomerMasterKeyFileExists()) { 71 generateCustomerMasterKey(); 72 } 73 74 byte[] localCustomerMasterKey = readCustomerMasterKey(); 75 Map<String, Object> keyMap = new HashMap<>(); 76 keyMap.put("key", localCustomerMasterKey); 77 return keyMap; 78 } 79 80 private boolean isCustomerMasterKeyFileExists() { 81 return new File(CUSTOMER_KEY_PATH).isFile(); 82 } 83 84 private void generateCustomerMasterKey() throws IOException { 85 byte[] localCustomerMasterKey = new byte[KEY_SIZE]; 86 new SecureRandom().nextBytes(localCustomerMasterKey); 87 try (FileOutputStream stream = new FileOutputStream(CUSTOMER_KEY_PATH)) { 88 stream.write(localCustomerMasterKey); 89 } catch (IOException e) { 90 throw new IOException("Unable to write Customer Master Key file: " + e.getMessage(), e); 91 } 92 } 93 94 private byte[] readCustomerMasterKey() throws IOException { 95 byte[] localCustomerMasterKey = new byte[KEY_SIZE]; 96 97 try (FileInputStream fis = new FileInputStream(CUSTOMER_KEY_PATH)) { 98 int bytesRead = fis.read(localCustomerMasterKey); 99 if (bytesRead != KEY_SIZE) { 100 throw new IOException("Expected the customer master key file to be " + KEY_SIZE + " bytes, but read " + bytesRead + " bytes."); 101 } 102 } catch (IOException e) { 103 throw new IOException("Unable to read the Customer Master Key: " + e.getMessage(), e); 104 } 105 106 return localCustomerMasterKey; 107 } 108 109 private Map<String, Object> createExtraOptions() { 110 Map<String, Object> extraOptions = new HashMap<>(); 111 extraOptions.put("cryptSharedLibPath", cryptSharedLibPath); 112 return extraOptions; 113 } 114 }
In our application, the EncryptionConfig class plays a critical role in setting up and managing encryption for MongoDB. Here’s a breakdown of its key functionalities, focusing particularly on the Key Management Service (KMS) and the cryptographic library path (cryptSharedLibPath).
- Key Management Service (KMS) configuration:
- KMS provider: The class is configured to use a KMS provider, specified by the kmsProviderName property. For local development and testing, we use a local KMS provider. This local setup simplifies development but is not recommended for production.
- Customer master key: The class handles the generation and management of a local customer master key. It first checks if the key file (customer-key.txt) exists. If it does not, the class generates a new key and saves it. This key is crucial for encrypting and decrypting data. The key is then read from the file and used in the encryption process.
- KMS provider credentials: The method createLocalKmsProvider() creates a map with the customer master key, which is used in both clientEncryptionSettings() and getMongoClientSettings() methods. This setup ensures that the MongoDB client can handle encryption correctly.
- Cryptographic Library Path (cryptSharedLibPath):
- Library Path Configuration: The cryptSharedLibPath property specifies the path to the Automatic Encryption Shared Library. This library is essential for enabling automatic encryption and decryption of data within MongoDB. The path to this library is included in the encryption settings through the createExtraOptions() method, allowing the MongoDB client to utilize the cryptographic functionalities provided by the library.
Key functionalities of the class:
- Client encryption settings: The clientEncryptionSettings() method configures the settings required for managing encryption keys and processes. This setup is crucial for ensuring that data is encrypted and decrypted according to our specifications.
- MongoDB client settings: The getMongoClientSettings() method incorporates auto-encryption settings into the MongoDB client configuration. It ensures that the client can connect to the MongoDB instance using the provided URI and handle encryption as required.
- Encrypted collection creation: The createEncryptedCollectionRunner() method ensures that an encrypted collection is created if it does not already exist. This setup uses the defined encryption settings to ensure data security.
Next, create the EncryptionFieldConfig in the resources/config package:
1 package com.mongodb.bankaccount.resources.config; 2 3 import com.mongodb.ClientEncryptionSettings; 4 import com.mongodb.client.MongoDatabase; 5 import com.mongodb.client.model.CreateCollectionOptions; 6 import com.mongodb.client.model.CreateEncryptedCollectionParams; 7 import com.mongodb.client.vault.ClientEncryptions; 8 import org.bson.*; 9 import org.springframework.context.annotation.Configuration; 10 import java.util.ArrayList; 11 import java.util.Arrays; 12 13 14 public class EncryptionFieldConfig { 15 16 protected boolean collectionExists(MongoDatabase db, String collectionName) { 17 return db.listCollectionNames().into(new ArrayList<>()).contains(collectionName); 18 } 19 20 protected void createEncryptedCollection(MongoDatabase db, ClientEncryptionSettings clientEncryptionSettings) { 21 var clientEncryption = ClientEncryptions.create(clientEncryptionSettings); 22 var encryptedCollectionParams = new CreateEncryptedCollectionParams("local") 23 .masterKey(new BsonDocument()); 24 25 var createCollectionOptions = new CreateCollectionOptions().encryptedFields(encryptFields()); 26 clientEncryption.createEncryptedCollection(db, "accounts", createCollectionOptions, encryptedCollectionParams); 27 } 28 29 private BsonDocument encryptFields() { 30 return new BsonDocument().append("fields", 31 new BsonArray(Arrays.asList( 32 createEncryptedField("accountNumber", "string", equalityQueryType()), 33 createEncryptedField("cardVerificationCode", "int", equalityQueryType()), 34 createEncryptedField("accountBalance", "double", rangeQueryType() 35 )))); 36 } 37 38 private BsonDocument createEncryptedField(String path, String bsonType, BsonDocument query) { 39 return new BsonDocument() 40 .append("keyId", new BsonNull()) 41 .append("path", new BsonString(path)) 42 .append("bsonType", new BsonString(bsonType)) 43 .append("queries", query); 44 } 45 46 private BsonDocument rangeQueryType() { 47 return new BsonDocument() 48 .append("queryType", new BsonString("range")) 49 .append("min", new BsonDouble(0)) 50 .append("max", new BsonDouble(999999999)) 51 .append("precision", new BsonInt32(2)); 52 } 53 54 private BsonDocument equalityQueryType() { 55 return new BsonDocument().append("queryType", new BsonString("equality")); 56 } 57 }
In the EncryptionFieldConfig class, we focus on specifying which fields in our MongoDB collection will be encrypted to ensure data security. Here’s a brief overview of what this class does, with an emphasis on the fields that will be encrypted:
- Defining encrypted fields:
- The primary role of this class is to configure field-level encryption. Using the encryptFields() method, we define which fields in the MongoDB collection need to be encrypted. In our case, we’re encrypting three critical fields:
- accountNumber (type: string, query type: equality): This field contains sensitive account information that needs protection.
- cardVerificationCode (type: int, query type: equality): This field holds sensitive verification codes, which must be encrypted to ensure security.
- accountBalance (type: double, query type: range): This field represents financial data, and encryption is applied with support for range-based queries to handle financial computations securely. For range queries, the optional min, max, and precision are pretty important for performance. You can find more about this in Encrypted Fields and Enabled Queries
- Creating and managing encrypted collections:
- The createEncryptedCollection() method is used to check whether the specified collection exists in the database. If it doesn’t, the method creates a new collection with the defined encryption settings. This setup is managed through ClientEncryptions.create(), which applies the encryption configurations.
- Field encryption metadata:
- For each field, the createEncryptedField() method specifies essential metadata, including the field path, BSON type, and query type. This metadata helps MongoDB understand how to encrypt and decrypt the data while supporting various query operations.
- Handling query types:
- Different query types are assigned to the fields to control how encrypted data can be queried. For example, equality queries allow us to search for exact matches, while range queries enable us to perform range-based searches on encrypted financial data.
And now, create the MongoConfig class in the resources/config package:
1 package com.mongodb.bankaccount.resources.config; 2 3 import com.mongodb.MongoClientSettings; 4 import com.mongodb.client.MongoClient; 5 import com.mongodb.client.MongoClients; 6 import com.mongodb.client.MongoDatabase; 7 import org.bson.codecs.configuration.CodecProvider; 8 import org.bson.codecs.configuration.CodecRegistry; 9 import org.bson.codecs.pojo.PojoCodecProvider; 10 import org.springframework.context.annotation.Bean; 11 import org.springframework.context.annotation.Configuration; 12 import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; 13 import static org.bson.codecs.configuration.CodecRegistries.fromProviders; 14 import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; 15 16 17 public class MongoConfig { 18 19 private final EncryptionConfig encryptionConfig; 20 21 public MongoConfig(EncryptionConfig encryptionConfig) { 22 this.encryptionConfig = encryptionConfig; 23 } 24 25 26 public MongoClient mongoClient() throws Exception { 27 MongoClientSettings mongoClientSettings = encryptionConfig.getMongoClientSettings(); 28 return MongoClients.create(mongoClientSettings); 29 } 30 31 32 public MongoDatabase mongoDatabase(MongoClient mongoClient) { 33 CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build(); 34 CodecRegistry pojoCodecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider)); 35 36 return mongoClient.getDatabase(encryptionConfig.getEncryptedDatabaseName()).withCodecRegistry(pojoCodecRegistry); 37 } 38 }
This class is straightforward; it only contains our MongoDB configuration.
Now, to finalize, simply open the BankaccountApplication class and add the annotation @EnableConfigurationProperties(EncryptionConfig.class) to enable reading of our properties in this class.
1 package com.mongodb.bankaccount; 2 3 import com.mongodb.bankaccount.resources.config.EncryptionConfig; 4 import org.springframework.boot.SpringApplication; 5 import org.springframework.boot.autoconfigure.SpringBootApplication; 6 import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 8 9 10 public class BankaccountApplication { 11 12 public static void main(String[] args) { 13 SpringApplication.run(BankaccountApplication.class, args); 14 } 15 }
By the end, we will have this structure configured:
Now that we have our encryption and database connection files defined, let's create our repository that will handle communication with Atlas and create our queries. To do this, first create a BankAccountEntity record in the resources package:
1 package com.mongodb.bankaccount.resources; 2 3 import com.mongodb.bankaccount.domain.BankAccount; 4 5 public record BankAccountEntity( 6 String accountHolderName, 7 String accountNumber, 8 int cardVerificationCode, 9 Double accountBalance) { 10 public BankAccount toDomain() { 11 return new BankAccount(accountHolderName, accountNumber, cardVerificationCode, accountBalance); 12 } 13 }
Then, create the BankAccountRepository class also in the resources package:
1 package com.mongodb.bankaccount.resources; 2 3 import com.mongodb.bankaccount.domain.BankAccount; 4 import com.mongodb.bankaccount.domain.BankAccountPort; 5 import com.mongodb.client.MongoCollection; 6 import com.mongodb.client.MongoDatabase; 7 import com.mongodb.client.result.InsertOneResult; 8 import org.bson.types.ObjectId; 9 import org.slf4j.Logger; 10 import org.slf4j.LoggerFactory; 11 import org.springframework.stereotype.Repository; 12 import java.util.ArrayList; 13 import java.util.List; 14 import java.util.Objects; 15 import java.util.stream.Collectors; 16 import static com.mongodb.client.model.Filters.eq; 17 import static com.mongodb.client.model.Filters.gt; 18 19 20 public class BankAccountRepository implements BankAccountPort { 21 private static final Logger logger = LoggerFactory.getLogger(BankAccountRepository.class); 22 private static final String COLLECTION_NAME = "accounts"; 23 private final MongoCollection<BankAccountEntity> collection; 24 25 BankAccountRepository(MongoDatabase mongoDatabase) { 26 this.collection = mongoDatabase.getCollection(COLLECTION_NAME, BankAccountEntity.class); 27 } 28 29 30 public String insert(BankAccount bankAccount) { 31 try { 32 InsertOneResult insertOneResult = collection.insertOne(bankAccount.toEntity()); 33 ObjectId result = Objects.requireNonNull(insertOneResult.getInsertedId()).asObjectId().getValue(); 34 35 logger.info("{} was created", result); 36 37 return result.toHexString(); 38 } catch (Exception e) { 39 logger.error(e.getMessage(), e); 40 throw new RuntimeException("Error inserting bank account", e); 41 } 42 } 43 44 45 public List<BankAccount> find() { 46 try { 47 ArrayList<BankAccountEntity> bankAccounts = new ArrayList<>(); 48 collection.find().into(bankAccounts); 49 50 return bankAccounts.stream() 51 .map(BankAccountEntity::toDomain) 52 .collect(Collectors.toList()); 53 54 } catch (Exception e) { 55 logger.error(e.getMessage(), e); 56 throw new RuntimeException("Error finding bank account", e); 57 } 58 } 59 60 61 public List<BankAccount> findByBalanceGreaterThan(double value) { 62 try { 63 ArrayList<BankAccountEntity> bankAccounts = new ArrayList<>(); 64 collection.find(gt("accountBalance", value)).into(bankAccounts); 65 66 return bankAccounts.stream() 67 .map(BankAccountEntity::toDomain) 68 .collect(Collectors.toList()); 69 70 } catch (Exception e) { 71 logger.error(e.getMessage(), e); 72 throw new RuntimeException("Error finding bank account", e); 73 } 74 } 75 76 77 public BankAccount findByAccountNumber(String accountNumber) { 78 try { 79 BankAccountEntity result = collection.find(eq("accountNumber", accountNumber)).first(); 80 81 if (result == null) { 82 return null; 83 } 84 85 return result.toDomain(); 86 87 } catch (Exception e) { 88 logger.error(e.getMessage(), e); 89 throw new RuntimeException("Error finding bank account", e); 90 } 91 92 } 93 }
You might be wondering about some files that we haven't created yet, but don't worry, we'll get to those soon. Before we do, let's examine what our repository class is doing:
The BankAccountRepository class is set up to interact with our accounts collection in MongoDB, providing methods for querying and inserting data.
Notice that we have a findByBalanceGreaterThan method that performs a range query to find accounts with a balance greater than a specified value. We also have the findByAccountNumber method, which uses an equality query to locate accounts with a specific account number.
Additionally, you'll see that BankAccountRepository implements an interface called BankAccountPort. This interface acts as a contract, defining the methods that our repository class must implement. It helps to isolate the domain logic from the data access layer, promoting a cleaner architecture and easier testing.
Let’s continue by creating the remaining domain classes. To do this, we'll create a domain package and include the BankAccount record:
1 package com.mongodb.bankaccount.domain; 2 3 import com.mongodb.bankaccount.application.web.BankResponse; 4 import com.mongodb.bankaccount.resources.BankAccountEntity; 5 6 public record BankAccount( 7 String accountHolderName, 8 String accountNumber, 9 int cardVerificationCode, 10 Double accountBalance) { 11 public BankResponse toResponse() { 12 return new BankResponse(accountHolderName, accountNumber, cardVerificationCode, accountBalance); 13 } 14 15 public BankAccountEntity toEntity() { 16 return new BankAccountEntity(accountHolderName, accountNumber, cardVerificationCode, accountBalance); 17 } 18 }
Next, in the same domain package, the BankAccountPort:
1 package com.mongodb.bankaccount.domain; 2 3 import java.util.List; 4 5 public interface BankAccountPort { 6 String insert(BankAccount bankAccount); 7 List<BankAccount> find(); 8 List<BankAccount> findByBalanceGreaterThan(double value); 9 BankAccount findByAccountNumber(String accountNumber); 10 }
And finally, the BankAccountService:
1 package com.mongodb.bankaccount.domain; 2 3 import org.springframework.stereotype.Service; 4 import java.util.List; 5 import java.util.Objects; 6 7 8 public class BankAccountService { 9 10 BankAccountPort bankAccountPort; 11 12 BankAccountService(BankAccountPort bankAccountPort) { 13 this.bankAccountPort = bankAccountPort; 14 } 15 16 public String insert(BankAccount bankAccount) { 17 Objects.requireNonNull(bankAccount, "Bank account must not be null"); 18 return bankAccountPort.insert(bankAccount); 19 } 20 21 public List<BankAccount> find() { 22 return bankAccountPort.find(); 23 } 24 25 public List<BankAccount> findByBalanceGreaterThan(double value) { 26 return bankAccountPort.findByBalanceGreaterThan(value); 27 } 28 29 public BankAccount findByAccountNumber(String accountNumber) { 30 return bankAccountPort.findByAccountNumber(accountNumber); 31 } 32 33 }
In this final step, we will provide our controller endpoints. To do this, create a package called application.web and within it, create the first class BankController class:
1 package com.mongodb.bankaccount.application.web; 2 3 import com.mongodb.bankaccount.domain.BankAccount; 4 import com.mongodb.bankaccount.domain.BankAccountService; 5 import org.springframework.http.ResponseEntity; 6 import org.springframework.web.bind.annotation.*; 7 import java.util.List; 8 9 10 11 public class BankController { 12 13 BankAccountService bankAccountService; 14 15 BankController(BankAccountService bankAccountService) { 16 this.bankAccountService = bankAccountService; 17 } 18 19 20 ResponseEntity<String> create( BankRequest bankRequest) { 21 return ResponseEntity.ok(bankAccountService.insert(bankRequest.toDomain())); 22 } 23 24 25 ResponseEntity<List<BankResponse>> getAllAccounts() { 26 return ResponseEntity.ok(bankAccountService.find().stream().map(BankAccount::toResponse).toList()); 27 } 28 29 30 ResponseEntity<List<BankResponse>> findByBalanceGreaterThan( Double value) { 31 return ResponseEntity.ok(bankAccountService.findByBalanceGreaterThan(value).stream().map(BankAccount::toResponse).toList()); 32 } 33 34 35 ResponseEntity<BankResponse> getAccountByNumber( String accountNumber) { 36 BankAccount bankAccount = bankAccountService.findByAccountNumber(accountNumber); 37 if (bankAccount != null) { 38 return ResponseEntity.ok(bankAccount.toResponse()); 39 } else { 40 return ResponseEntity.notFound().build(); 41 } 42 } 43 }
Now, let's create the two missing records and adjust the necessary imports. Still within the application.web package, create the BankRequest record:
1 package com.mongodb.bankaccount.application.web; 2 3 import com.mongodb.bankaccount.domain.BankAccount; 4 5 record BankRequest( 6 String accountHolderName, 7 String accountNumber, 8 int cardVerificationCode, 9 Double accountBalance) { 10 public BankAccount toDomain() { 11 return new BankAccount(accountHolderName, accountNumber, cardVerificationCode, accountBalance); 12 } 13 }
And BankResponse:
1 package com.mongodb.bankaccount.application.web; 2 3 public record BankResponse(String accountHolderName, 4 String accountNumber, 5 int cardVerificationCode, 6 Double accountBalance) { 7 }
Perfect! Now, just correct the imports in the classes that use our records, and everything will be set. Our structure will look like this:
In this final step, simply execute the following command to run the application:
1 ./mvnw spring-boot:run
If the application starts correctly, open the database we’re connecting to, and you will notice that the digitalBank and encryption databases, along with the accounts and __keyVault collections, have been created. This step happens once when we configure the files in the resources/config folder.
To create a new document, simply access the @POST endpoint described in the BankAccountController. Below is a curl command to insert a new document:
1 curl --location 'http://localhost:8080/bank' \ 2 --header 'Content-Type: application/json' \ 3 --data '{ 4 "accountHolderName": "Ricardo Mello", 5 "accountNumber": "4527876391233218", 6 "cardVerificationCode": "761", 7 "accountBalance": 5000.2 8 }'
After running this curl command, you will see that the document has been created with the fields accountNumber, cardVerificationCode, and accountBalance encrypted as defined in EncryptionFieldConfig class, as shown in the image below:
Note: You will see that a safeContent field has been created. This
field will be present in all encrypted documents, and it is crucial
that this data is not modified or deleted. For more information, refer
to encrypted-collection-management.
To explore the equality type of encrypted search, you can search for documents by accountNumber. To do this, simply run the following curl command:
1 curl --location 'http://localhost:8080/bank/accountNumber/{accountNumber}
Result:
To explore range queries (e.g., greaterThan), use the following endpoint:
1 curl --location 'http://localhost:8080/bank/balance/greaterThan/{value}'
Result:
Our demonstration using the "BankAccount" Java application clearly illustrates how MongoDB Queryable Encryption can be implemented. By integrating automatic encryption, the workflow is simplified, enabling developers to focus on the core features without needing deep expertise in security. Queryable Encryption ensures secure execution of equality and range queries on encrypted data, offering a seamless balance between strong data protection and the ability to handle complex operations.
This tutorial illustrates one approach to leveraging Queryable Encryption, but it's important to recognize that alternative methods and configurations exist. I encourage you to explore these options to identify the most suitable solution for your specific requirements. Doing so will deepen your understanding of secure data practices and enhance your ability to implement effective data protection strategies.
For a complete view of the Java project, you can access the repository at github-mongodb-developer.
Any questions? Come chat with us in the MongoDB Developer Community
Top Comments in Forums
There are no comments on this article yet.