The plugin is available since version 4.0.0.
The KMS Encryption Plugin provides transparent client-side encryption using AWS Key Management Service (KMS). This plugin automatically encrypts sensitive data before storing it in the database and decrypts it when retrieving data, based on metadata configuration.
- Transparent Encryption: Automatically encrypts and decrypts data without changing your application code
- AWS KMS Integration: Uses AWS KMS for secure key management and encryption operations
- Metadata-Driven: Configurable encryption based on table and column metadata
- Audit Logging: Optional audit logging for encryption operations
- Minimal Performance Impact: Efficient encryption with caching and optimized operations
- AWS KMS key with appropriate permissions
- Database table to store encryption metadata
- AWS credentials configured (via IAM roles, profiles, or environment variables)
- JSqlParser 4.9 dependency - Required for SQL parsing and analysis
- The
sqlParserplugin - Must be loaded together withkmsEncryption(for examplewrapperPlugins=sqlParser,kmsEncryption). It parses each statement and maps its parameters to columns; without it,kmsEncryptionrefuses to prepare statements
- Create a KMS Key in AWS Console or using AWS CLI:
aws kms create-key --description "Database encryption master key" --key-usage ENCRYPT_DECRYPT-
Note the Key ARN from the response - you'll need this when setting up encryption metadata.
-
Set Key Permissions - Ensure your application has the following KMS permissions:
kms:Encryptkms:Decryptkms:GenerateDataKeykms:DescribeKey
The plugin automatically manages data keys:
- Data keys are generated automatically using the master key when encrypting new data
- Data keys are cached in memory for performance (configurable via
dataKeyCache*properties) - Data keys are encrypted with the master key and stored alongside encrypted data
- No manual data key creation is required
Create the required metadata tables to store encryption configuration:
-- Key storage table (must be created first due to foreign key)
CREATE TABLE key_storage (
id SERIAL PRIMARY KEY,
key_id VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
master_key_arn VARCHAR(512) NOT NULL,
encrypted_data_key TEXT NOT NULL,
key_spec VARCHAR(50) DEFAULT 'AES_256',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Encryption metadata table
CREATE TABLE encryption_metadata (
table_name VARCHAR(255) NOT NULL,
column_name VARCHAR(255) NOT NULL,
encryption_algorithm VARCHAR(50) NOT NULL,
key_id INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (table_name, column_name),
FOREIGN KEY (key_id) REFERENCES key_storage(id)
);Use the KeyManagementUtility to properly configure encryption for your columns:
// Initialize KeyManagementUtility
KeyManagementUtility keyManagementUtility = new KeyManagementUtility(
keyManager, metadataManager, dataSource, kmsClient);
// Configure encryption for a column
String keyId = keyManagementUtility.initializeEncryptionForColumn(
"users", // table name
"ssn", // column name
masterKeyArn, // KMS master key ARN
"AES-256-GCM" // encryption algorithm
);Alternative: Direct metadata insertion (not recommended for production):
INSERT INTO encryption_metadata (table_name, column_name, encryption_algorithm, key_id)
VALUES ('users', 'ssn', 'AES-256-GCM', 'your-generated-key-id');The KMS Encryption Plugin requires JSqlParser 4.9 for SQL statement analysis. Add this dependency to your project:
Maven:
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.9</version>
</dependency>Gradle:
implementation 'com.github.jsqlparser:jsqlparser:4.9'| Property | Available Since Version | Description | Required | Default |
|---|---|---|---|---|
kms.region |
4.0.0 | AWS KMS region for encryption operations | Yes | None |
encryptionMetadataSchema |
4.0.0 | Schema name for encryption metadata tables | No | encrypt |
encryptionFailOnUnmappedParameter |
4.6.0 | When true (default), the plugin rejects a statement on a table with encrypted columns with an SQLException at prepare time if it cannot reliably map the statement's parameters to columns (see Limitations). When false, the plugin only logs a WARNING and prepares the statement; values bound to encrypted columns may then be written unencrypted. |
No | true |
metadataCacheEnabled |
4.0.0 | Enable/disable metadata caching | No | true |
metadataCacheExpirationMinutes |
4.0.0 | Metadata cache expiration time in minutes | No | 60 |
metadataCacheRefreshIntervalMs |
4.0.0 | Metadata cache refresh interval in milliseconds | No | 300000 |
keyManagementMaxRetries |
4.0.0 | Maximum number of retries for key management operations | No | 3 |
keyManagementRetryBackoffBaseMs |
4.0.0 | Base backoff time in milliseconds for key management retries | No | 100 |
auditLoggingEnabled |
4.0.0 | Enable/disable audit logging | No | false |
dataKeyCacheEnabled |
4.0.0 | Enable/disable data key caching | No | true |
dataKeyCacheMaxSize |
4.0.0 | Maximum size of data key cache | No | 1000 |
dataKeyCacheExpirationMs |
4.0.0 | Data key cache expiration in milliseconds | No | 3600000 |
String url = "jdbc:aws-wrapper:postgresql://your-cluster.cluster-xyz.us-east-1.rds.amazonaws.com:5432/mydb";
Properties props = new Properties();
props.setProperty("user", "username");
props.setProperty("password", "password");
props.setProperty("wrapperPlugins", "sqlParser,kmsEncryption"); // kmsEncryption requires sqlParser
props.setProperty("kms.region", "us-east-1");
props.setProperty("auditLoggingEnabled", "true");
Connection conn = DriverManager.getConnection(url, props);The sqlParser plugin is required and must run before kmsEncryption. List it first; with autoSortWrapperPluginOrder enabled (the default) the driver also sorts it ahead of kmsEncryption. If sqlParser is missing, preparing a statement fails with SQL parse results not found in call context.
First, create the required tables to store encryption metadata and keys:
-- Key storage table (must be created first due to foreign key)
CREATE TABLE key_storage (
id SERIAL PRIMARY KEY,
key_id VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
master_key_arn VARCHAR(512) NOT NULL,
encrypted_data_key TEXT NOT NULL,
key_spec VARCHAR(50) DEFAULT 'AES_256',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
last_used_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Encryption metadata table
CREATE TABLE encryption_metadata (
table_name VARCHAR(255) NOT NULL,
column_name VARCHAR(255) NOT NULL,
encryption_algorithm VARCHAR(50) NOT NULL,
key_id INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (table_name, column_name),
FOREIGN KEY (key_id) REFERENCES key_storage(id)
);Recommended: Use KeyManagementUtility for proper key management:
KeyManagementUtility keyManagementUtility = new KeyManagementUtility(
keyManager, metadataManager, dataSource, kmsClient);
// Configure encryption for sensitive columns
keyManagementUtility.initializeEncryptionForColumn("customers", "ssn", masterKeyArn);
keyManagementUtility.initializeEncryptionForColumn("customers", "credit_card", masterKeyArn);
keyManagementUtility.initializeEncryptionForColumn("customers", "phone", masterKeyArn);
keyManagementUtility.initializeEncryptionForColumn("customers", "address", masterKeyArn);Alternative: Direct SQL insertion (for testing only):
-- Configure encryption for sensitive columns in the customers table
INSERT INTO encryption_metadata (table_name, column_name, encryption_algorithm, key_id)
VALUES
('customers', 'ssn', 'AES-256-GCM', 'generated-key-id-1'),
('customers', 'credit_card', 'AES-256-GCM', 'generated-key-id-2'),
('customers', 'phone', 'AES-256-GCM', 'generated-key-id-3'),
('customers', 'address', 'AES-256-GCM', 'generated-key-id-4');Create your application tables normally:
CREATE TABLE customers (
customer_id SERIAL PRIMARY KEY,
first_name VARCHAR(100),
last_name VARCHAR(100),
email VARCHAR(255),
phone BYTEA, -- Will be encrypted
ssn BYTEA, -- Will be encrypted
credit_card BYTEA, -- Will be encrypted
address BYTEA, -- Will be encrypted
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);Once configured, the plugin works transparently:
// Insert data - sensitive fields are automatically encrypted
String sql = "INSERT INTO customers (first_name, last_name, email, phone, ssn, credit_card, address) VALUES (?, ?, ?, ?, ?, ?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, "John");
stmt.setString(2, "Doe");
stmt.setString(3, "john.doe@example.com");
stmt.setString(4, "555-123-4567"); // Automatically encrypted
stmt.setString(5, "123-45-6789"); // Automatically encrypted
stmt.setString(6, "4111-1111-1111-1111"); // Automatically encrypted
stmt.setString(7, "123 Main St, City, ST 12345"); // Automatically encrypted
stmt.executeUpdate();
}
// Query data - encrypted fields are automatically decrypted
String query = "SELECT * FROM customers WHERE customer_id = ?";
try (PreparedStatement stmt = connection.prepareStatement(query)) {
stmt.setInt(1, customerId);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
String phone = rs.getString("phone"); // Automatically decrypted
String ssn = rs.getString("ssn"); // Automatically decrypted
String creditCard = rs.getString("credit_card"); // Automatically decrypted
String address = rs.getString("address"); // Automatically decrypted
// Use the decrypted data normally
System.out.println("Phone: " + phone);
}
}
}In addition to metadata-based encryption, you can use SQL comment annotations to explicitly mark parameters for encryption. This provides explicit control over which parameters are encrypted, regardless of column position or metadata configuration.
Use SQL comments with the format /*@encrypt:table.column*/ immediately before the parameter placeholder:
// Basic annotation usage
String sql = "INSERT INTO customers (name, ssn, email) VALUES (?, /*@encrypt:customers.ssn*/ ?, ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, "John Doe");
stmt.setString(2, "123-45-6789"); // Encrypted via annotation
stmt.setString(3, "john@example.com");
stmt.executeUpdate();
}
// Update with annotation
String updateSql = "UPDATE customers SET ssn = /*@encrypt:customers.ssn*/ ? WHERE customer_id = ?";
try (PreparedStatement stmt = connection.prepareStatement(updateSql)) {
stmt.setString(1, "987-65-4321"); // Encrypted
stmt.setInt(2, customerId);
stmt.executeUpdate();
}
// Different column order - annotation ensures correct encryption
String sql2 = "INSERT INTO customers (name, email, ssn) VALUES (?, ?, /*@encrypt:customers.ssn*/ ?)";
try (PreparedStatement stmt = connection.prepareStatement(sql2)) {
stmt.setString(1, "Jane Smith");
stmt.setString(2, "jane@example.com");
stmt.setString(3, "555-66-7777"); // Encrypted even though it's the 3rd parameter
stmt.executeUpdate();
}Use annotations when:
- You need explicit control over which parameters are encrypted
- Column order varies in different queries
- Working with dynamic SQL where column positions might change
- You want to encrypt specific parameters without modifying metadata
Use metadata (recommended) when:
- You want centralized encryption configuration
- You don't want to modify SQL statements
- You need automatic encryption based on column names
- You want easier maintenance and consistency
Note: Both approaches can be used together. When an annotation is present, it takes precedence over metadata configuration for that specific parameter. If the annotation names a different column than the one the statement assigns the parameter to, the plugin uses the annotation and logs a WARNING.
- The annotation must be a SQL comment:
/*@encrypt:table.column*/ - It must immediately precede the
?parameter placeholder. It applies to the next real placeholder that follows it - The format is:
table.column(both table and column names required) - Whitespace between the annotation and
?is allowed but not required - Only an annotation that starts its own block comment outside string literals and other comments counts. Annotation-like text inside a string literal, a line comment (
-- ..., MySQL# ...) or another block comment is ignored - A
?inside a string literal, a quoted identifier, a comment or a PostgreSQL dollar-quoted string is not a parameter placeholder, and neither is the PostgreSQL??operator. Such characters do not shift the parameter numbering - An annotation that is not followed by a placeholder is ignored, and the plugin logs a WARNING
- Annotations do not bypass the fail-closed check described in Limitations. A statement that the plugin cannot map reliably on a table with encrypted columns is rejected even if it contains annotations
Warning
The annotation is sent to the database as part of the statement. It is a valid SQL comment, so it does not affect how the statement executes, but it is not removed from the SQL text: it appears in server-side statement logs, in pg_stat_statements, in slow-query logs, and in any query-capture tooling. Since the annotation names the table and column being encrypted, using it discloses which columns hold sensitive data to anyone who can read those logs. If that matters in your environment, mark columns through the metadata table instead of through annotations.
Valid examples:
/*@encrypt:users.ssn*/ ?
/*@encrypt:customers.credit_card*/ ?
/*@encrypt:orders.payment_info*/ ?Invalid examples:
? /*@encrypt:users.ssn*/ -- Annotation must come BEFORE the ?
/*@encrypt:ssn*/ ? -- Missing table name
/* @encrypt:users.ssn */ ? -- Space after /* not allowedEnsure your application has the necessary KMS permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
}
]
}- Encrypted data is stored as binary data in the database
- The original data never leaves your application - encryption/decryption happens locally using data keys from KMS
- Only encryption keys are managed by AWS KMS, not the actual data
- Consider using different KMS keys for different environments (dev, staging, prod)
- KMS calls are only needed for data key generation/decryption, not for each data encryption/decryption
- Data key caching significantly reduces KMS API calls for repeated operations
- Rotating the KMS key has no runtime impact. Rotating a column's data key re-encrypts every row of the column and needs a maintenance window; see Key Rotation
- KMS has rate limits that may affect very high-volume key operations
- The plugin caches both metadata and data keys to minimize external calls
-
Missing KMS Permissions: Ensure your AWS credentials have the necessary KMS permissions
-
Metadata Table Not Found: Verify the encryption metadata table exists and is accessible
-
Region Mismatch: Ensure the KMS region matches where your key is located
-
Invalid Key ID: Verify the KMS key ID or ARN is correct and accessible
-
Refusing to prepare a statement on table '...': it has encrypted columns, but the statement's parameters cannot be reliably mapped to columns (...): The statement writes to a table with encrypted columns, but the plugin cannot tell which parameter belongs to which column, so it cannot guarantee that the encrypted column values get encrypted. The text in parentheses names the reason, for exampleINSERT without a column listorcolumn ssn receives parameter(s) 2 inside an expression. The message never contains the SQL text or parameter values. To fix it, rewrite the statement in one of the supported shapes listed in Limitations:- list the columns explicitly:
INSERT INTO users (name, ssn) VALUES (?, ?); - bind each encrypted column value with its own
?, for exampleSET ssn = ?instead ofSET ssn = lower(?)orSET ssn = (SELECT ...); - do not write literals or expressions into encrypted columns;
- use
INSERTorUPDATEinstead ofREPLACEorMERGE, and plain?placeholders instead of numbered ones (?1).
Only if you have verified that the statement never writes an unencrypted value to an encrypted column, set
encryptionFailOnUnmappedParameter=false. The plugin then logs a WARNING and prepares the statement. - list the columns explicitly:
-
... is not supported for encrypted columns. Use setString, setNString, setBytes or setObject.: A stream or LOB setter (setCharacterStream,setBinaryStream,setClob,setBlob, and similar) was called for a parameter that maps to an encrypted column. Pass the value with one of the listed setters.
Enable audit logging to track encryption operations:
props.setProperty("auditLoggingEnabled", "true");Check the application logs for encryption-related messages.
- Currently supports string data types for encryption
- Requires metadata configuration for each encrypted column
- Performance impact mainly during data key operations, mitigated by caching
Automatic encryption works for PreparedStatement parameters in these statement shapes:
INSERTwith a column list: single-row and multi-rowVALUES,INSERT ... SELECT(includingUNION), and MySQLINSERT ... SETUPDATE ... SET, including row assignments such asSET (name, ssn) = (?, ?)- PostgreSQL
INSERT ... ON CONFLICT ... DO UPDATE SETand MySQLINSERT ... ON DUPLICATE KEY UPDATE
A value is encrypted only when it is bound by its own ? placeholder directly to the encrypted column. A cast around the placeholder and parentheses are allowed, for example CAST(? AS bytea), ?::bytea or (?). String literals and other expressions are never encrypted: in SET ssn = '123-45-6789' or SET ssn = lower(?) the plugin cannot encrypt the value. Literals and expressions in other columns of the same statement are fine and do not shift the parameter numbering.
On a table with encrypted columns, the plugin rejects the following statements with an SQLException when they are prepared, so that no value is written unencrypted (controlled by encryptionFailOnUnmappedParameter, default true):
INSERTwithout a column list- parameters inside an expression (function call, arithmetic, concatenation,
CASE, sub-select) that is assigned to an encrypted column - parameters whose target column cannot be determined, for example a sub-select or an unknown target table
REPLACEandMERGEstatements- statements the SQL parser cannot read, for example PostgreSQL
E'...'strings, MySQL backslash escapes inside string literals ('it\'s'), or the PostgreSQL JSONB??operator - numbered placeholders such as
?1 - statements where the number of parameters found by the parser differs from the number of placeholders the driver sends
Statements on tables without encrypted columns are never rejected. Versions before 4.6.0 did not reject these statements and could write values bound to encrypted columns unencrypted without an error or a log entry. See the Troubleshooting section for how to fix a rejected statement.
- Stream and LOB setters (
setCharacterStream,setNCharacterStream,setAsciiStream,setBinaryStream,setUnicodeStream,setClob,setNClob,setBlob) throw anSQLExceptionwhen the parameter maps to an encrypted column. UsesetString,setNString,setBytesorsetObject. CallableStatementsetters are not intercepted; values passed to stored procedures are not encrypted.- Values that come from a CTE or from another column (
INSERT INTO users (ssn) SELECT ssn FROM staging) are not encrypted by the plugin. - Table names are matched against the metadata case-sensitively. Use the same spelling in statements as in
encryption_metadata.
- Use IAM Roles: Use IAM roles instead of hardcoded credentials when possible
- Separate Keys: Use different KMS keys for different environments
- Monitor Usage: Monitor KMS usage and costs
- Test Performance: Test the performance impact in your specific use case
- Backup Metadata: Ensure the encryption metadata table is included in backups
- Key Rotation: Enable automatic rotation on the KMS key; it is transparent to the plugin. Rotating a column's data key requires re-encrypting its rows; see Key Rotation. Never delete an old data key or KMS key while data or backups encrypted under it exist.
See KmsEncryptionIntegrationTest.java for a complete working example.