diff --git a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ParserBenchmark.java b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ParserBenchmark.java
new file mode 100644
index 000000000..41a2d6220
--- /dev/null
+++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ParserBenchmark.java
@@ -0,0 +1,94 @@
+package software.amazon.jdbc.benchmarks;
+
+import org.openjdk.jmh.annotations.*;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+import software.amazon.jdbc.plugin.encryption.parser.PostgreSqlParser;
+
+import java.util.concurrent.TimeUnit;
+
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+@State(Scope.Benchmark)
+@Fork(1)
+@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+public class ParserBenchmark {
+
+ private PostgreSqlParser parser;
+
+ @Setup
+ public void setup() {
+ parser = new PostgreSqlParser();
+ }
+
+ @Benchmark
+ public void parseSimpleSelect() {
+ parser.parse("SELECT * FROM users");
+ }
+
+ @Benchmark
+ public void parseSelectWithWhere() {
+ parser.parse("SELECT id, name FROM users WHERE age > 25");
+ }
+
+ @Benchmark
+ public void parseSelectWithOrderBy() {
+ parser.parse("SELECT * FROM products ORDER BY price DESC");
+ }
+
+ @Benchmark
+ public void parseComplexSelect() {
+ parser.parse("SELECT u.name, o.total FROM users u, orders o WHERE u.id = o.user_id AND o.total > 100");
+ }
+
+ @Benchmark
+ public void parseInsert() {
+ parser.parse("INSERT INTO users (name, age, email) VALUES ('John', 30, 'john@example.com')");
+ }
+
+ @Benchmark
+ public void parseInsertWithPlaceholders() {
+ parser.parse("INSERT INTO users (name, age, email) VALUES (?, ?, ?)");
+ }
+
+ @Benchmark
+ public void parseUpdate() {
+ parser.parse("UPDATE users SET name = 'Jane', age = 25 WHERE id = 1");
+ }
+
+ @Benchmark
+ public void parseUpdateWithPlaceholders() {
+ parser.parse("UPDATE users SET name = ?, age = ? WHERE id = ?");
+ }
+
+ @Benchmark
+ public void parseDelete() {
+ parser.parse("DELETE FROM users WHERE age < 18");
+ }
+
+ @Benchmark
+ public void parseCreateTable() {
+ parser.parse("CREATE TABLE products (id INTEGER PRIMARY KEY, name VARCHAR NOT NULL, price DECIMAL)");
+ }
+
+ @Benchmark
+ public void parseComplexExpression() {
+ parser.parse("SELECT * FROM orders WHERE (total > 100 AND status = 'pending') OR (total > 500 AND status = 'shipped')");
+ }
+
+ @Benchmark
+ public void parseScientificNotation() {
+ parser.parse("INSERT INTO measurements VALUES (42, 3.14159, 2.5e10)");
+ }
+
+ public static void main(String[] args) throws RunnerException {
+ Options opt = new OptionsBuilder()
+ .include(ParserBenchmark.class.getSimpleName())
+ .build();
+
+ new Runner(opt).run();
+ }
+}
diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheKmsEncryptionPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheKmsEncryptionPlugin.md
new file mode 100644
index 000000000..436f3ea94
--- /dev/null
+++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheKmsEncryptionPlugin.md
@@ -0,0 +1,337 @@
+# Using the KMS Encryption Plugin
+
+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.
+
+## Features
+
+- **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
+
+## Prerequisites
+
+- AWS KMS key with appropriate permissions
+- Database table to store encryption metadata
+- AWS credentials configured (via IAM roles, profiles, or environment variables)
+- **JSqlParser 4.5.x dependency** - Required for SQL parsing and analysis
+
+### Creating AWS KMS Master Key
+
+1. **Create a KMS Key** in AWS Console or using AWS CLI:
+```bash
+aws kms create-key --description "Database encryption master key" --key-usage ENCRYPT_DECRYPT
+```
+
+2. **Note the Key ARN** from the response - you'll need this for the `kms.MasterKeyArn` property.
+
+3. **Set Key Permissions** - Ensure your application has the following KMS permissions:
+ - `kms:Encrypt`
+ - `kms:Decrypt`
+ - `kms:GenerateDataKey`
+ - `kms:DescribeKey`
+
+### Data Key Management
+
+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
+
+### Metadata Storage
+
+Create the required metadata tables to store encryption configuration:
+
+```sql
+-- 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)
+);
+```
+
+### Setting Up Encryption Metadata
+
+Use the KeyManagementUtility to properly configure encryption for your columns:
+
+```java
+// 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):**
+```sql
+INSERT INTO encryption_metadata (table_name, column_name, encryption_algorithm, key_id)
+VALUES ('users', 'ssn', 'AES-256-GCM', 'your-generated-key-id');
+```
+
+### Adding JSqlParser Dependency
+
+The KMS Encryption Plugin requires JSqlParser 4.5.x for SQL statement analysis. Add this dependency to your project:
+
+**Maven:**
+```xml
+
+ com.github.jsqlparser
+ jsqlparser
+ 4.5
+
+```
+
+**Gradle:**
+```gradle
+implementation 'com.github.jsqlparser:jsqlparser:4.5'
+```
+
+## Configuration
+
+### Connection Properties
+
+| Property | Description | Required | Default |
+|----------|-------------|----------|---------|
+| `kms.region` | AWS KMS region for encryption operations | Yes | None |
+| `kms.MasterKeyArn` | Master key ARN for encryption | Yes | None |
+| `key.rotationDays` | Number of days for key rotation | No | `30` |
+| `metadataCache.enabled` | Enable/disable metadata caching | No | `true` |
+| `metadataCache.expirationMinutes` | Metadata cache expiration time in minutes | No | `60` |
+| `metadataCache.refreshIntervalMs` | Metadata cache refresh interval in milliseconds | No | `300000` |
+| `keyManagement.maxRetries` | Maximum number of retries for key management operations | No | `3` |
+| `keyManagement.retryBackoffBaseMs` | Base backoff time in milliseconds for key management retries | No | `100` |
+| `audit.loggingEnabled` | Enable/disable audit logging | No | `false` |
+| `kms.connectionTimeoutMs` | KMS connection timeout in milliseconds | No | `5000` |
+| `dataKeyCache.enabled` | Enable/disable data key caching | No | `true` |
+| `dataKeyCache.maxSize` | Maximum size of data key cache | No | `1000` |
+| `dataKeyCache.expirationMs` | Data key cache expiration in milliseconds | No | `3600000` |
+
+### Example Connection String
+
+```java
+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", "kmsEncryption");
+props.setProperty("kms.MasterKeyArn", "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012");
+props.setProperty("kms.region", "us-east-1");
+props.setProperty("audit.loggingEnabled", "true");
+
+Connection conn = DriverManager.getConnection(url, props);
+```
+
+## Setup
+
+### 1. Create Encryption Metadata Table
+
+First, create the required tables to store encryption metadata and keys:
+
+```sql
+-- 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)
+);
+```
+
+### 2. Configure Column Encryption
+
+**Recommended: Use KeyManagementUtility for proper key management:**
+
+```java
+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):**
+```sql
+-- 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');
+```
+
+### 3. Create Your Application Tables
+
+Create your application tables normally:
+
+```sql
+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
+);
+```
+
+## Usage
+
+Once configured, the plugin works transparently:
+
+```java
+// 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);
+ }
+ }
+}
+```
+
+## Security Considerations
+
+### KMS Key Permissions
+
+Ensure your application has the necessary KMS permissions:
+
+```json
+{
+ "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"
+ }
+ ]
+}
+```
+
+### Data Protection
+
+- 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)
+
+### Performance Considerations
+
+- 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
+- Consider the impact on performance for high-throughput applications during 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
+
+## Troubleshooting
+
+### Common Issues
+
+1. **Missing KMS Permissions**: Ensure your AWS credentials have the necessary KMS permissions
+2. **Metadata Table Not Found**: Verify the encryption metadata table exists and is accessible
+3. **Region Mismatch**: Ensure the KMS region matches where your key is located
+4. **Invalid Key ID**: Verify the KMS key ID or ARN is correct and accessible
+
+### Debugging
+
+Enable audit logging to track encryption operations:
+
+```java
+props.setProperty("enableAuditLogging", "true");
+```
+
+Check the application logs for encryption-related messages.
+
+## Limitations
+
+- Currently supports string data types for encryption
+- Requires metadata configuration for each encrypted column
+- Performance impact mainly during data key operations, mitigated by caching
+- Limited to INSERT and UPDATE operations for automatic encryption
+
+## Best Practices
+
+1. **Use IAM Roles**: Use IAM roles instead of hardcoded credentials when possible
+2. **Separate Keys**: Use different KMS keys for different environments
+3. **Monitor Usage**: Monitor KMS usage and costs
+4. **Test Performance**: Test the performance impact in your specific use case
+5. **Backup Metadata**: Ensure the encryption metadata table is included in backups
+6. **Key Rotation**: Implement a strategy for KMS key rotation
+
+## Example Application
+
+See the [KmsEncryptionExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/KmsEncryptionExample.java) for a complete working example.
diff --git a/environment.txt b/environment.txt
new file mode 100644
index 000000000..848941aea
--- /dev/null
+++ b/environment.txt
@@ -0,0 +1,2 @@
+AWS_KMS_KEY_ARN=arn:aws:kms:us-east-1:000579002577:key/d69090ec-8a8c-48ca-a1bc-36333d551e01
+TEST_ENV_INFO_JSON={"request":{"features":[]},"databaseInfo":{"username":"postgres","password":"password","defaultDbName":"postgres","clusterEndpoint":"database-1.cgnh50a2ovor.us-east-1.rds.amazonaws.com","clusterEndpointPort":5432,"instances":[]},"region":"us-east-1","databaseEngine":"postgresql"}
diff --git a/gradle.properties b/gradle.properties
index c1bf6b8ec..ce1f17f3c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,7 +15,7 @@
aws-advanced-jdbc-wrapper.version.major=2
aws-advanced-jdbc-wrapper.version.minor=6
aws-advanced-jdbc-wrapper.version.subminor=4
-snapshot=false
+snapshot=true
nexus.publish=true
org.gradle.jvmargs=-Xmx16384m -Xms8096m -XshowSettings:all
diff --git a/wrapper/build.gradle.kts b/wrapper/build.gradle.kts
index 48873b114..01f512440 100644
--- a/wrapper/build.gradle.kts
+++ b/wrapper/build.gradle.kts
@@ -39,6 +39,7 @@ dependencies {
optionalImplementation("software.amazon.awssdk:http-client-spi:2.33.5") // Required for IAM (light implementation)
optionalImplementation("software.amazon.awssdk:sts:2.33.5")
optionalImplementation("software.amazon.awssdk:secretsmanager:2.33.5")
+ optionalImplementation("software.amazon.awssdk:kms:2.33.5")
optionalImplementation("com.fasterxml.jackson.core:jackson-databind:2.19.0")
optionalImplementation("com.zaxxer:HikariCP:4.0.3") // Version 4.+ is compatible with Java 8
optionalImplementation("com.mchange:c3p0:0.11.0")
@@ -49,6 +50,7 @@ dependencies {
optionalImplementation("io.opentelemetry:opentelemetry-api:1.52.0")
optionalImplementation("io.opentelemetry:opentelemetry-sdk:1.52.0")
optionalImplementation("io.opentelemetry:opentelemetry-sdk-metrics:1.52.0")
+ optionalImplementation("com.github.jsqlparser:jsqlparser:4.5") // JSqlParser SQL parser (Java 8 compatible)
compileOnly("org.checkerframework:checker-qual:3.49.5")
compileOnly("com.mysql:mysql-connector-j:9.4.0")
@@ -106,6 +108,7 @@ dependencies {
testImplementation("de.vandermeer:asciitable:0.3.2")
testImplementation("org.hibernate:hibernate-core:5.6.15.Final") // the latest version compatible with Java 8
testImplementation("jakarta.persistence:jakarta.persistence-api:2.2.3")
+ testImplementation("software.amazon.awssdk:kms:2.33.5")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.2")
}
@@ -434,6 +437,7 @@ tasks.register("test-all-multi-az") {
tasks.register("test-all-pg-aurora") {
group = "verification"
filter.includeTestsMatching("integration.host.TestRunner.runTests")
+ filter.includeTestsMatching("integration.container.tests.KmsEncryptionIntegrationTest")
doFirst {
systemProperty("test-no-docker", "true")
systemProperty("test-no-performance", "true")
@@ -1046,3 +1050,30 @@ tasks.register("test-metrics-pg-multi-az") {
systemProperty("test-no-mysql-engine", "true")
}
}
+
+tasks.register("test-kms-encryption") {
+ group = "verification"
+ filter.includeTestsMatching("integration.container.tests.KmsEncryptionPluginTest")
+ classpath = sourceSets.test.get().runtimeClasspath
+ dependsOn("jar")
+ systemProperty("java.util.logging.config.file", "${project.layout.buildDirectory.get()}/resources/test/logging-test.properties")
+ systemProperty("jdbc.drivers", "software.amazon.jdbc.Driver")
+}
+
+tasks.register("test-kms-encryption-integration") {
+ group = "verification"
+ filter.includeTestsMatching("integration.container.tests.KmsEncryptionIntegrationTest")
+ classpath = sourceSets.test.get().runtimeClasspath
+ dependsOn("jar")
+ systemProperty("java.util.logging.config.file", "${project.layout.buildDirectory.get()}/resources/test/logging-test.properties")
+ systemProperty("jdbc.drivers", "software.amazon.jdbc.Driver")
+}
+
+tasks.register("test-key-management-utility") {
+ group = "verification"
+ filter.includeTestsMatching("integration.container.tests.KeyManagementUtilityIntegrationTest")
+ classpath = sourceSets.test.get().runtimeClasspath
+ dependsOn("jar")
+ systemProperty("java.util.logging.config.file", "${project.layout.buildDirectory.get()}/resources/test/logging-test.properties")
+ systemProperty("jdbc.drivers", "software.amazon.jdbc.Driver")
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java
index 411e40cd8..0f5d591fb 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginChainBuilder.java
@@ -42,6 +42,7 @@
import software.amazon.jdbc.plugin.customendpoint.CustomEndpointPluginFactory;
import software.amazon.jdbc.plugin.dev.DeveloperConnectionPluginFactory;
import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPluginFactory;
+import software.amazon.jdbc.plugin.encryption.KmsEncryptionConnectionPluginFactory;
import software.amazon.jdbc.plugin.failover.FailoverConnectionPluginFactory;
import software.amazon.jdbc.plugin.federatedauth.FederatedAuthPluginFactory;
import software.amazon.jdbc.plugin.federatedauth.OktaAuthPluginFactory;
@@ -88,6 +89,7 @@ public class ConnectionPluginChainBuilder {
put("initialConnection", new AuroraInitialConnectionStrategyPluginFactory());
put("limitless", new LimitlessConnectionPluginFactory());
put("bg", new BlueGreenConnectionPluginFactory());
+ put("kmsEncryption", new KmsEncryptionConnectionPluginFactory());
}
};
diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java
index 8757e2c0a..f82308ee1 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java
@@ -40,6 +40,7 @@
import software.amazon.jdbc.plugin.LogQueryConnectionPlugin;
import software.amazon.jdbc.plugin.customendpoint.CustomEndpointPlugin;
import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin;
+import software.amazon.jdbc.plugin.encryption.KmsEncryptionConnectionPlugin;
import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin;
import software.amazon.jdbc.plugin.federatedauth.FederatedAuthPlugin;
import software.amazon.jdbc.plugin.federatedauth.OktaAuthPlugin;
@@ -90,6 +91,7 @@ public class ConnectionPluginManager implements CanReleaseResources, Wrapper {
put(DefaultConnectionPlugin.class, "plugin:targetDriver");
put(AuroraInitialConnectionStrategyPlugin.class, "plugin:initialConnection");
put(CustomEndpointPlugin.class, "plugin:customEndpoint");
+ put(KmsEncryptionConnectionPlugin.class,"plugin.kmsEncryption");
}
};
diff --git a/wrapper/src/main/java/software/amazon/jdbc/factory/EncryptingDataSourceFactory.java b/wrapper/src/main/java/software/amazon/jdbc/factory/EncryptingDataSourceFactory.java
new file mode 100644
index 000000000..e6d1aedad
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/factory/EncryptingDataSourceFactory.java
@@ -0,0 +1,281 @@
+package software.amazon.jdbc.factory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import software.amazon.jdbc.plugin.encryption.wrapper.EncryptingDataSource;
+
+import javax.sql.DataSource;
+import java.sql.SQLException;
+import java.util.Properties;
+
+/**
+ * Factory for creating EncryptingDataSource instances that integrate with the AWS Advanced JDBC Wrapper.
+ * This factory provides convenient methods to wrap existing DataSources with encryption capabilities.
+ */
+public class EncryptingDataSourceFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(EncryptingDataSourceFactory.class);
+
+ /**
+ * Creates an EncryptingDataSource that wraps the provided DataSource with encryption capabilities.
+ *
+ * @param dataSource The underlying DataSource to wrap
+ * @param encryptionProperties Properties for configuring encryption
+ * @return An EncryptingDataSource instance
+ * @throws SQLException if encryption initialization fails
+ */
+ public static EncryptingDataSource create(DataSource dataSource, Properties encryptionProperties) throws SQLException {
+ logger.info("Creating EncryptingDataSource with encryption properties");
+
+ // Validate required properties
+ validateEncryptionProperties(encryptionProperties);
+
+ return new EncryptingDataSource(dataSource, encryptionProperties);
+ }
+
+ /**
+ * Creates an EncryptingDataSource using AWS JDBC Wrapper with encryption.
+ * This method creates an AWS Wrapper DataSource and then wraps it with encryption.
+ *
+ * @param jdbcUrl The JDBC URL for the database
+ * @param username Database username
+ * @param password Database password
+ * @param encryptionProperties Properties for configuring encryption
+ * @return An EncryptingDataSource instance
+ * @throws SQLException if DataSource creation or encryption initialization fails
+ */
+ public static EncryptingDataSource createWithAwsWrapper(String jdbcUrl, String username, String password,
+ Properties encryptionProperties) throws SQLException {
+ logger.info("Creating EncryptingDataSource with AWS JDBC Wrapper for URL: {}", jdbcUrl);
+
+ try {
+ // Create properties for AWS JDBC Wrapper
+ Properties awsWrapperProperties = new Properties();
+ awsWrapperProperties.setProperty("jdbcUrl", jdbcUrl);
+ awsWrapperProperties.setProperty("username", username);
+ awsWrapperProperties.setProperty("password", password);
+
+ // Add any additional AWS wrapper properties from encryption properties
+ copyAwsWrapperProperties(encryptionProperties, awsWrapperProperties);
+
+ // Create AWS Wrapper DataSource using reflection to avoid compile-time dependency
+ DataSource awsDataSource = createAwsWrapperDataSource(awsWrapperProperties);
+
+ // Wrap with encryption
+ return create(awsDataSource, encryptionProperties);
+
+ } catch (Exception e) {
+ logger.error("Failed to create EncryptingDataSource with AWS Wrapper", e);
+ throw new SQLException("Failed to create encrypted DataSource: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates an EncryptingDataSource with default encryption properties.
+ *
+ * @param dataSource The underlying DataSource to wrap
+ * @param kmsKeyArn The KMS key ARN for encryption
+ * @param region The AWS region
+ * @return An EncryptingDataSource instance
+ * @throws SQLException if encryption initialization fails
+ */
+ public static EncryptingDataSource createWithDefaults(DataSource dataSource, String kmsKeyArn, String region) throws SQLException {
+ Properties encryptionProperties = createDefaultEncryptionProperties(kmsKeyArn, region);
+ return create(dataSource, encryptionProperties);
+ }
+
+ /**
+ * Validates that required encryption properties are present.
+ *
+ * @param properties The properties to validate
+ * @throws SQLException if required properties are missing
+ */
+ private static void validateEncryptionProperties(Properties properties) throws SQLException {
+ if (properties == null) {
+ throw new SQLException("Encryption properties cannot be null");
+ }
+
+ // Check for required properties (these will be validated by EncryptionConfig)
+ logger.debug("Validating encryption properties");
+
+ // The actual validation is done by EncryptionConfig.validate() in the plugin
+ // We just do basic null checks here
+ }
+
+ /**
+ * Copies AWS Wrapper specific properties from encryption properties.
+ *
+ * @param encryptionProperties Source properties
+ * @param awsWrapperProperties Target properties
+ */
+ private static void copyAwsWrapperProperties(Properties encryptionProperties, Properties awsWrapperProperties) {
+ // Copy AWS wrapper specific properties
+ String[] awsWrapperKeys = {
+ "wrapperPlugins",
+ "wrapperLogUnclosedConnections",
+ "wrapperLoggerLevel",
+ "aws.region"
+ };
+
+ for (String key : awsWrapperKeys) {
+ String value = encryptionProperties.getProperty(key);
+ if (value != null) {
+ awsWrapperProperties.setProperty(key, value);
+ }
+ }
+ }
+
+ /**
+ * Creates an AWS Wrapper DataSource using reflection to avoid compile-time dependency issues.
+ *
+ * @param properties Properties for the AWS Wrapper DataSource
+ * @return DataSource instance
+ * @throws Exception if DataSource creation fails
+ */
+ private static DataSource createAwsWrapperDataSource(Properties properties) throws Exception {
+ try {
+ // Try to create AWS Wrapper DataSource using reflection
+ Class> awsDataSourceClass = Class.forName("software.amazon.jdbc.AwsWrapperDataSource");
+ return (DataSource) awsDataSourceClass.getConstructor(Properties.class).newInstance(properties);
+ } catch (ClassNotFoundException e) {
+ logger.warn("AWS JDBC Wrapper not found, falling back to direct PostgreSQL DataSource");
+ return createPostgreSqlDataSource(properties);
+ }
+ }
+
+ /**
+ * Creates a PostgreSQL DataSource as fallback when AWS Wrapper is not available.
+ *
+ * @param properties Properties for the DataSource
+ * @return DataSource instance
+ * @throws Exception if DataSource creation fails
+ */
+ private static DataSource createPostgreSqlDataSource(Properties properties) throws Exception {
+ // Create a basic PostgreSQL DataSource
+ Class> pgDataSourceClass = Class.forName("org.postgresql.ds.PGSimpleDataSource");
+ DataSource dataSource = (DataSource) pgDataSourceClass.getDeclaredConstructor().newInstance();
+
+ // Set properties using reflection
+ String jdbcUrl = properties.getProperty("jdbcUrl");
+ String username = properties.getProperty("username");
+ String password = properties.getProperty("password");
+
+ if (jdbcUrl != null) {
+ // Parse URL to extract host, port, database
+ // This is a simplified implementation
+ pgDataSourceClass.getMethod("setUrl", String.class).invoke(dataSource, jdbcUrl);
+ }
+
+ if (username != null) {
+ pgDataSourceClass.getMethod("setUser", String.class).invoke(dataSource, username);
+ }
+
+ if (password != null) {
+ pgDataSourceClass.getMethod("setPassword", String.class).invoke(dataSource, password);
+ }
+
+ return dataSource;
+ }
+
+ /**
+ * Creates default encryption properties.
+ *
+ * @param kmsKeyArn The KMS key ARN
+ * @param region The AWS region
+ * @return Properties with default encryption settings
+ */
+ private static Properties createDefaultEncryptionProperties(String kmsKeyArn, String region) {
+ Properties properties = new Properties();
+
+ // KMS configuration
+ properties.setProperty("kms.region", region != null ? region : "us-east-1");
+ properties.setProperty("kms.keyArn", kmsKeyArn);
+
+ // Cache configuration
+ properties.setProperty("cache.enabled", "true");
+ properties.setProperty("cache.expirationMinutes", "30");
+ properties.setProperty("cache.maxSize", "1000");
+
+ // Retry configuration
+ properties.setProperty("kms.maxRetries", "3");
+ properties.setProperty("kms.retryBackoffBaseMs", "100");
+
+ // Metadata configuration
+ properties.setProperty("metadata.refreshIntervalMinutes", "5");
+
+ logger.debug("Created default encryption properties for KMS key: {}, region: {}", kmsKeyArn, region);
+
+ return properties;
+ }
+
+ /**
+ * Builder class for creating EncryptingDataSource with fluent API.
+ */
+ public static class Builder {
+ private DataSource dataSource;
+ private String jdbcUrl;
+ private String username;
+ private String password;
+ private final Properties encryptionProperties = new Properties();
+
+ public Builder dataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ return this;
+ }
+
+ public Builder jdbcUrl(String jdbcUrl) {
+ this.jdbcUrl = jdbcUrl;
+ return this;
+ }
+
+ public Builder username(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public Builder password(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder kmsKeyArn(String kmsKeyArn) {
+ encryptionProperties.setProperty("kms.keyArn", kmsKeyArn);
+ return this;
+ }
+
+ public Builder region(String region) {
+ encryptionProperties.setProperty("kms.region", region);
+ return this;
+ }
+
+ public Builder cacheEnabled(boolean enabled) {
+ encryptionProperties.setProperty("cache.enabled", String.valueOf(enabled));
+ return this;
+ }
+
+ public Builder cacheExpirationMinutes(int minutes) {
+ encryptionProperties.setProperty("cache.expirationMinutes", String.valueOf(minutes));
+ return this;
+ }
+
+ public Builder cacheMaxSize(int maxSize) {
+ encryptionProperties.setProperty("cache.maxSize", String.valueOf(maxSize));
+ return this;
+ }
+
+ public Builder property(String key, String value) {
+ encryptionProperties.setProperty(key, value);
+ return this;
+ }
+
+ public EncryptingDataSource build() throws SQLException {
+ if (dataSource != null) {
+ return create(dataSource, encryptionProperties);
+ } else if (jdbcUrl != null && username != null && password != null) {
+ return createWithAwsWrapper(jdbcUrl, username, password, encryptionProperties);
+ } else {
+ throw new SQLException("Either dataSource or (jdbcUrl, username, password) must be provided");
+ }
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPlugin.java
new file mode 100644
index 000000000..99ba015d2
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPlugin.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption;
+
+import java.util.logging.Logger;
+import software.amazon.jdbc.*;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+/**
+ * ConnectionPlugin implementation that integrates KmsEncryptionPlugin with AWS JDBC Wrapper.
+ * This class acts as a bridge between the AWS JDBC Wrapper plugin system and our encryption functionality.
+ */
+public class KmsEncryptionConnectionPlugin implements ConnectionPlugin {
+
+ private static final Logger LOGGER = Logger.getLogger(KmsEncryptionConnectionPlugin.class.getName());
+
+ private final KmsEncryptionPlugin encryptionPlugin;
+ private final PluginService pluginService;
+
+ public static final String KMS_ENCRYPTION_PLUGIN_CODE = "kmsEncryption";
+
+ /**
+ * Constructor that creates the encryption plugin with PluginService.
+ *
+ * @param pluginService The PluginService instance from AWS JDBC Wrapper
+ * @param properties Configuration properties
+ */
+ public KmsEncryptionConnectionPlugin(PluginService pluginService, Properties properties) {
+ this.pluginService = pluginService;
+ this.encryptionPlugin = new KmsEncryptionPlugin(pluginService);
+
+ try {
+ this.encryptionPlugin.initialize(properties);
+ LOGGER.info(()->"KmsEncryptionConnectionPlugin initialized successfully");
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Failed to initialize KmsEncryptionConnectionPlugin %s", e.getMessage()));
+ throw new RuntimeException("Failed to initialize encryption plugin", e);
+ }
+ }
+
+ /**
+ * Returns the underlying encryption plugin.
+ *
+ * @return KmsEncryptionPlugin instance
+ */
+ public KmsEncryptionPlugin getEncryptionPlugin() {
+ return encryptionPlugin;
+ }
+
+ /**
+ * Executes JDBC method calls and applies encryption/decryption wrapping when needed.
+ *
+ * @param Return type
+ * @param Exception type
+ * @param methodClass Method class
+ * @param methodReturnType Return type class
+ * @param methodInvokeOn Object to invoke method on
+ * @param methodName Method name
+ * @param jdbcCallable Callable to execute
+ * @param args Method arguments
+ * @return Method result, potentially wrapped with encryption/decryption
+ * @throws E if method execution fails
+ */
+ @Override
+ public T execute(Class methodClass, Class methodReturnType, Object methodInvokeOn,
+ String methodName, JdbcCallable jdbcCallable, Object... args) throws E {
+ // Execute the original method first
+ T result = jdbcCallable.call();
+
+ try {
+ // Apply encryption/decryption wrapping if needed
+ if (result instanceof java.sql.PreparedStatement && args.length > 0 && args[0] instanceof String) {
+ String sql = (String) args[0];
+ @SuppressWarnings("unchecked")
+ T wrappedResult = (T) encryptionPlugin.wrapPreparedStatement((java.sql.PreparedStatement) result, sql);
+ return wrappedResult;
+ } else if (result instanceof java.sql.ResultSet) {
+ @SuppressWarnings("unchecked")
+ T wrappedResult = (T) encryptionPlugin.wrapResultSet((java.sql.ResultSet) result);
+ return wrappedResult;
+ }
+ } catch (SQLException e) {
+ // If E is SQLException or a superclass, we can throw it
+ if (methodReturnType.isAssignableFrom(SQLException.class)) {
+ @SuppressWarnings("unchecked")
+ E exception = (E) e;
+ throw exception;
+ } else {
+ // Otherwise wrap in RuntimeException
+ throw new RuntimeException("Failed to wrap JDBC object with encryption", e);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Delegates connection creation to the original function.
+ *
+ * @param driverProtocol Driver protocol
+ * @param hostSpec Host specification
+ * @param props Connection properties
+ * @param isInitialConnection Whether this is initial connection
+ * @param connectFunc Connection function
+ * @return Database connection
+ * @throws SQLException if connection fails
+ */
+ @Override
+ public Connection connect(String driverProtocol, HostSpec hostSpec, Properties props,
+ boolean isInitialConnection, JdbcCallable connectFunc) throws SQLException {
+ // Delegate to the original connection function
+ return connectFunc.call();
+ }
+
+ /**
+ * Returns the set of JDBC methods this plugin subscribes to.
+ *
+ * @return Set of method names to intercept
+ */
+ @Override
+ public Set getSubscribedMethods() {
+ // Subscribe to PreparedStatement and ResultSet creation methods
+ return new HashSet<>(Arrays.asList(
+ "Connection.prepareStatement",
+ "Connection.prepareCall",
+ "Statement.executeQuery",
+ "PreparedStatement.executeQuery"
+ ));
+ }
+
+ /**
+ * Delegates host provider initialization to the original function.
+ *
+ * @param driverProtocol Driver protocol
+ * @param initialUrl Initial URL
+ * @param props Properties
+ * @param hostListProviderService Host list provider service
+ * @param initFunc Initialization function
+ * @throws SQLException if initialization fails
+ */
+ @Override
+ public void initHostProvider(String driverProtocol, String initialUrl, Properties props,
+ HostListProviderService hostListProviderService, JdbcCallable initFunc) throws SQLException {
+ // Delegate to the original initialization
+ initFunc.call();
+ }
+
+ /**
+ * Handles node list change notifications (no action needed for encryption).
+ *
+ * @param changes Map of node changes
+ */
+ @Override
+ public void notifyNodeListChanged(Map> changes) {
+ // No action needed for encryption plugin
+ }
+
+ /**
+ * Accepts all strategies since encryption is transparent.
+ *
+ * @param role Host role
+ * @param strategy Strategy name
+ * @return Always true
+ */
+ @Override
+ public boolean acceptsStrategy(HostRole role, String strategy) {
+ // Accept all strategies - encryption is transparent
+ return true;
+ }
+
+ /**
+ * Not supported - encryption plugin does not provide host selection.
+ *
+ * @param role Host role
+ * @param strategy Strategy name
+ * @return Never returns
+ * @throws SQLException Always throws UnsupportedOperationException
+ */
+ @Override
+ public HostSpec getHostSpecByStrategy(HostRole role, String strategy) throws SQLException {
+ throw new UnsupportedOperationException("Encryption plugin does not provide host selection");
+ }
+
+
+ /**
+ * Not supported - encryption plugin does not provide host selection.
+ *
+ * @param hosts List of host specs
+ * @param role Host role
+ * @param strategy Strategy name
+ * @return Never returns
+ * @throws SQLException Always throws UnsupportedOperationException
+ */
+ public HostSpec getHostSpecByStrategy(List hosts, HostRole role, String strategy) throws SQLException {
+ throw new UnsupportedOperationException("Encryption plugin does not provide host selection");
+ }
+
+ /**
+ * Forces connection creation by delegating to the original function.
+ *
+ * @param driverProtocol Driver protocol
+ * @param hostSpec Host specification
+ * @param props Connection properties
+ * @param isInitialConnection Whether this is initial connection
+ * @param connectFunc Connection function
+ * @return Database connection
+ * @throws SQLException if connection fails
+ */
+ @Override
+ public Connection forceConnect(String driverProtocol, HostSpec hostSpec, Properties props,
+ boolean isInitialConnection, JdbcCallable connectFunc) throws SQLException {
+ // Delegate to the original connection function
+ return connectFunc.call();
+ }
+
+ /**
+ * Handles connection change notifications (no special action needed).
+ *
+ * @param changes Set of node change options
+ * @return NO_OPINION - no special action required
+ */
+ @Override
+ public OldConnectionSuggestedAction notifyConnectionChanged(EnumSet changes) {
+ // No special action needed for connection changes
+ return OldConnectionSuggestedAction.NO_OPINION;
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPluginFactory.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPluginFactory.java
new file mode 100644
index 000000000..fd2ff8b59
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionConnectionPluginFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption;
+
+import java.util.logging.Logger;
+import software.amazon.jdbc.ConnectionPlugin;
+import software.amazon.jdbc.ConnectionPluginFactory;
+import software.amazon.jdbc.PluginService;
+
+import java.util.Properties;
+
+/**
+ * Factory for creating KmsEncryptionConnectionPlugin instances.
+ * This factory is used by the AWS JDBC Wrapper to create plugin instances.
+ */
+public class KmsEncryptionConnectionPluginFactory implements ConnectionPluginFactory {
+
+ private static final Logger LOGGER = Logger.getLogger(KmsEncryptionConnectionPluginFactory.class.getName());
+
+ /**
+ * Creates a new KmsEncryptionConnectionPlugin instance.
+ *
+ * @param pluginService The PluginService instance from AWS JDBC Wrapper
+ * @param properties Configuration properties for the plugin
+ * @return New plugin instance
+ */
+ @Override
+ public ConnectionPlugin getInstance(PluginService pluginService, Properties properties) {
+ LOGGER.info(()->"Creating KmsEncryptionConnectionPlugin instance");
+ return new KmsEncryptionConnectionPlugin(pluginService, properties);
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionPlugin.java
new file mode 100644
index 000000000..b1e169d8f
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/KmsEncryptionPlugin.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption;
+
+
+import software.amazon.jdbc.PluginService;
+import software.amazon.jdbc.plugin.encryption.factory.IndependentDataSource;
+import software.amazon.jdbc.plugin.encryption.logging.AuditLogger;
+import software.amazon.jdbc.plugin.encryption.metadata.MetadataManager;
+import software.amazon.jdbc.plugin.encryption.metadata.MetadataException;
+import software.amazon.jdbc.plugin.encryption.model.EncryptionConfig;
+import software.amazon.jdbc.plugin.encryption.key.KeyManager;
+import software.amazon.jdbc.plugin.encryption.sql.SqlAnalysisService;
+import software.amazon.jdbc.plugin.encryption.wrapper.EncryptingPreparedStatement;
+import software.amazon.jdbc.plugin.encryption.service.EncryptionService;
+import software.amazon.jdbc.plugin.encryption.wrapper.DecryptingResultSet;
+
+import java.util.logging.Logger;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.kms.KmsClient;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Main encryption plugin that integrates with the AWS Advanced JDBC Wrapper
+ * to provide transparent client-side encryption using AWS KMS.
+ *
+ * This plugin intercepts JDBC operations to automatically encrypt data before storage
+ * and decrypt data upon retrieval based on metadata configuration.
+ */
+public class KmsEncryptionPlugin {
+
+ private static final Logger LOGGER = Logger.getLogger(KmsEncryptionPlugin.class.getName());
+
+ // Plugin configuration
+ private EncryptionConfig config;
+ private MetadataManager metadataManager;
+ private KeyManager keyManager;
+ private EncryptionService encryptionService;
+ private KmsClient kmsClient;
+
+ // Plugin services
+ private PluginService pluginService;
+ private IndependentDataSource independentDataSource;
+
+ // SQL Analysis
+ private SqlAnalysisService sqlAnalysisService;
+
+ // Monitoring and metrics
+ private AuditLogger auditLogger;
+
+ // Plugin lifecycle state
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final AtomicBoolean closed = new AtomicBoolean(false);
+
+ // Track connections where custom types have been registered
+ private final java.util.Map registeredConnections =
+ new java.util.WeakHashMap<>();
+
+ // Plugin properties
+ private Properties pluginProperties;
+
+ /**
+ * Constructor that accepts PluginService for integration with AWS JDBC Wrapper.
+ *
+ * @param pluginService The PluginService instance from AWS JDBC Wrapper
+ */
+ public KmsEncryptionPlugin(PluginService pluginService) {
+ this.pluginService = pluginService;
+ LOGGER.fine(() -> String.format("KmsEncryptionPlugin created with PluginService: %s", pluginService != null ? "available" : "null"));
+ }
+
+ /**
+ * Default constructor for backward compatibility.
+ */
+ public KmsEncryptionPlugin() {
+ this.pluginService = null;
+ LOGGER.warning("KmsEncryptionPlugin created without PluginService - connection parameter extraction may fail");
+ }
+
+ /**
+ * Sets the PluginService instance. This method can be called to provide
+ * the PluginService after construction if it wasn't available during construction.
+ *
+ * @param pluginService The PluginService instance from AWS JDBC Wrapper
+ */
+ public void setPluginService(PluginService pluginService) {
+ if (this.pluginService == null) {
+ this.pluginService = pluginService;
+ LOGGER.info(() -> String.format("PluginService set after construction: %s", pluginService != null ? "available" : "null"));
+ } else {
+ LOGGER.warning("PluginService already set, ignoring new instance");
+ }
+ }
+
+ /**
+ * Initializes the plugin with the provided configuration.
+ * This method is called by the AWS JDBC Wrapper during plugin loading.
+ *
+ * @param properties Configuration properties for the plugin
+ * @throws SQLException if initialization fails
+ */
+ public void initialize(Properties properties) throws SQLException {
+ if (initialized.get()) {
+ LOGGER.warning("Plugin already initialized, skipping re-initialization");
+ return;
+ }
+
+ LOGGER.info("Initializing KmsEncryptionPlugin");
+
+ try {
+ // Store properties for later use
+ this.pluginProperties = new Properties();
+ this.pluginProperties.putAll(properties);
+
+ // Load and validate configuration
+ this.config = loadConfiguration(properties);
+ config.validate();
+
+ // Initialize AWS KMS client
+ this.kmsClient = createKmsClient(config);
+
+ // Initialize core services
+ this.encryptionService = new EncryptionService();
+
+ // Initialize audit LOGGER
+ this.auditLogger = new AuditLogger(config.isAuditLoggingEnabled());
+
+ LOGGER.info("KmsEncryptionPlugin initialized successfully");
+ initialized.set(true);
+
+ } catch (Exception e) {
+ LOGGER.severe(() -> String.format("Failed to initialize KmsEncryptionPlugin %s", e.getMessage()));
+ throw new SQLException("Plugin initialization failed: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Initializes plugin components that require a database connection.
+ * This method uses PluginService to get connection parameters instead of extraction.
+ *
+ * @throws SQLException if initialization fails
+ */
+ private void initializeWithDataSource() throws SQLException {
+ if (metadataManager != null) {
+ return; // Already initialized
+ }
+
+ try {
+ if (pluginService != null) {
+ // Create independent DataSource using PluginService
+ this.independentDataSource = new IndependentDataSource(pluginService, pluginProperties);
+
+ // Log success
+ auditLogger.logConnectionParameterExtraction("PluginService", "PLUGIN_SERVICE", true, null);
+
+ // Initialize managers with PluginService
+ this.keyManager = new KeyManager(kmsClient, pluginService, config);
+ this.metadataManager = new MetadataManager(pluginService, config);
+ metadataManager.initialize();
+
+ // Initialize SQL analysis service
+ this.sqlAnalysisService = new SqlAnalysisService(pluginService, metadataManager);
+
+ LOGGER.info("Plugin initialized with PluginService connection parameters");
+
+ } else {
+ LOGGER.severe("PluginService not available - cannot create independent connections");
+
+ auditLogger.logConnectionParameterExtraction("PluginService", "PLUGIN_SERVICE", false, "PluginService not available");
+
+ throw new SQLException("PluginService not available - cannot create independent connections");
+ }
+
+ } catch (MetadataException e) {
+ LOGGER.severe(()->String.format("Failed to initialize plugin components with database %s", e.getMessage()));
+ throw new SQLException("Failed to initialize plugin with database: " + e.getMessage(), e);
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to initialize plugin with PluginService %s", e.getMessage()));
+ throw new SQLException("Failed to initialize plugin: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Registers custom PostgreSQL types with the JDBC driver for a specific connection.
+ * Only registers once per connection.
+ */
+ private void registerPostgresTypesForConnection(java.sql.Connection conn) {
+ if (conn == null) {
+ return;
+ }
+
+ synchronized (registeredConnections) {
+ if (registeredConnections.containsKey(conn)) {
+ return; // Already registered for this connection
+ }
+
+ try {
+ org.postgresql.PGConnection pgConn = conn.unwrap(org.postgresql.PGConnection.class);
+ pgConn.addDataType("encrypted_data", software.amazon.jdbc.plugin.encryption.wrapper.EncryptedData.class);
+ registeredConnections.put(conn, Boolean.TRUE);
+ LOGGER.fine("Registered encrypted_data type for connection");
+ } catch (Exception e) {
+ LOGGER.fine(() -> "Failed to register PostgreSQL custom types: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Wraps a PreparedStatement to add encryption capabilities.
+ *
+ * @param statement The original PreparedStatement
+ * @param sql The SQL statement
+ * @return Wrapped PreparedStatement with encryption support
+ * @throws SQLException if wrapping fails
+ */
+ public PreparedStatement wrapPreparedStatement(PreparedStatement statement, String sql)
+ throws SQLException {
+ if (!initialized.get()) {
+ throw new SQLException("Plugin not initialized");
+ }
+
+ // Initialize with DataSource if needed (lazy initialization)
+ if (metadataManager == null) {
+ try {
+ initializeWithDataSource();
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to initialize plugin with connection %s", e.getMessage()));
+ throw new SQLException("Failed to initialize plugin: " + e.getMessage(), e);
+ }
+ }
+
+ // Register custom types for this connection
+ registerPostgresTypesForConnection(statement.getConnection());
+
+ LOGGER.fine(()->String.format("Wrapping PreparedStatement for SQL: %s", sql));
+
+ // Analyze SQL to determine if encryption is needed
+ SqlAnalysisService.SqlAnalysisResult analysisResult;
+ if (sqlAnalysisService != null) {
+ analysisResult = sqlAnalysisService.analyzeSql(sql);
+ LOGGER.fine(()->String.format("SQL analysis result: %s", analysisResult));
+ } else {
+ analysisResult = null;
+ }
+
+ return new EncryptingPreparedStatement(
+ statement,
+ metadataManager,
+ encryptionService,
+ keyManager,
+ sqlAnalysisService,
+ sql
+ );
+ }
+
+ /**
+ * Wraps a ResultSet to add decryption capabilities.
+ *
+ * @param resultSet The original ResultSet
+ * @return Wrapped ResultSet with decryption support
+ * @throws SQLException if wrapping fails
+ */
+ public ResultSet wrapResultSet(ResultSet resultSet) throws SQLException {
+ if (!initialized.get()) {
+ throw new SQLException("Plugin not initialized");
+ }
+
+ // Initialize with DataSource if needed (lazy initialization)
+ if (metadataManager == null) {
+ try {
+ initializeWithDataSource();
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to initialize plugin with connection %s", e.getMessage()));
+ throw new SQLException("Failed to initialize plugin: " + e.getMessage(), e);
+ }
+ }
+
+ // Register custom types for this connection
+ try {
+ registerPostgresTypesForConnection(resultSet.getStatement().getConnection());
+ } catch (Exception e) {
+ LOGGER.fine(() -> "Could not register types for ResultSet connection: " + e.getMessage());
+ }
+
+ LOGGER.finest(()->"Wrapping ResultSet");
+
+ return new DecryptingResultSet(
+ resultSet,
+ metadataManager,
+ encryptionService,
+ keyManager
+ );
+ }
+
+ /**
+ * Returns the plugin name for identification.
+ *
+ * @return Plugin name
+ */
+ public String getPluginName() {
+ return "KmsEncryptionPlugin";
+ }
+
+ /**
+ * Cleans up plugin resources.
+ * This method is called when the plugin is being unloaded.
+ */
+ public void cleanup() {
+ if (closed.get()) {
+ return;
+ }
+
+ LOGGER.info("Cleaning up KmsEncryptionPlugin resources");
+
+ // Log final connection status
+ if (independentDataSource != null) {
+ try {
+ independentDataSource.logHealthStatus();
+ } catch (Exception e) {
+ LOGGER.warning(()->String.format("Error logging final DataSource health status %s", e.getMessage()));
+ }
+ }
+
+ try {
+ if (kmsClient != null) {
+ kmsClient.close();
+ }
+ } catch (Exception e) {
+ LOGGER.warning(()->String.format("Error closing KMS client %s", e.getMessage()));
+ }
+
+ closed.set(true);
+ LOGGER.info("KmsEncryptionPlugin cleanup completed");
+ }
+
+ /**
+ * Loads configuration from properties.
+ *
+ * @param properties Configuration properties
+ * @return EncryptionConfig instance
+ * @throws SQLException if configuration is invalid
+ */
+ private EncryptionConfig loadConfiguration(Properties properties) throws SQLException {
+ try {
+ // Set default region if not provided
+ if (!properties.containsKey("kms.region")) {
+ properties.setProperty("kms.region", "us-east-1");
+ }
+
+ EncryptionConfig config = EncryptionConfig.fromProperties(properties);
+
+ LOGGER.info(()->String.format("Loaded encryption configuration: region=%s, cacheEnabled=%s, maxRetries=%s",
+ config.getKmsRegion(), config.isCacheEnabled(), config.getMaxRetries()));
+
+ return config;
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to load configuration from properties %s", e.getMessage()));
+ throw new SQLException("Invalid configuration: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates a KMS client with the specified configuration.
+ *
+ * @param config Encryption configuration
+ * @return Configured KMS client
+ */
+ private KmsClient createKmsClient(EncryptionConfig config) {
+ LOGGER.fine(()->String.format("Creating KMS client for region: %s", config.getKmsRegion()));
+
+ return KmsClient.builder()
+ .region(Region.of(config.getKmsRegion()))
+ .build();
+ }
+
+
+ // Getters for testing and monitoring
+
+ /**
+ * Returns the current configuration.
+ *
+ * @return EncryptionConfig instance
+ */
+ public EncryptionConfig getConfig() {
+ return config;
+ }
+
+ /**
+ * Returns the metadata manager.
+ *
+ * @return MetadataManager instance
+ */
+ public MetadataManager getMetadataManager() {
+ return metadataManager;
+ }
+
+ /**
+ * Returns the key manager.
+ *
+ * @return KeyManager instance
+ */
+ public KeyManager getKeyManager() {
+ return keyManager;
+ }
+
+ /**
+ * Returns the encryption service.
+ *
+ * @return EncryptionService instance
+ */
+ public EncryptionService getEncryptionService() {
+ return encryptionService;
+ }
+
+ /**
+ * Checks if the plugin is initialized.
+ *
+ * @return true if initialized, false otherwise
+ */
+ public boolean isInitialized() {
+ return initialized.get();
+ }
+
+ /**
+ * Checks if the plugin is closed.
+ *
+ * @return true if closed, false otherwise
+ */
+ public boolean isClosed() {
+ return closed.get();
+ }
+
+ /**
+ * Returns the plugin service.
+ *
+ * @return PluginService instance
+ */
+ public PluginService getPluginService() {
+ return pluginService;
+ }
+
+ /**
+ * Returns the independent DataSource used by MetadataManager.
+ *
+ * @return IndependentDataSource instance, or null if not initialized
+ */
+ public IndependentDataSource getIndependentDataSource() {
+ return independentDataSource;
+ }
+
+ /**
+ * Checks if the plugin is using independent connections.
+ *
+ * @return true if using independent connections, false otherwise
+ */
+ public boolean isUsingIndependentConnections() {
+ return independentDataSource != null;
+ }
+
+
+ /**
+ * Creates a detailed status message about the current connection mode.
+ *
+ * @return a comprehensive status message
+ */
+ public String getConnectionModeStatus() {
+ if (isUsingIndependentConnections()) {
+ return "Plugin is using independent connections via PluginService";
+ } else {
+ return "Plugin connection mode is not yet determined";
+ }
+ }
+
+ /**
+ * Logs the current connection status and performance metrics.
+ * This method can be called for troubleshooting purposes.
+ */
+ public void logCurrentStatus() {
+ LOGGER.info("=== KmsEncryptionPlugin Status Report ===");
+
+ // Log connection mode status
+ LOGGER.info(()->String.format("Connection Mode: %s", getConnectionModeStatus()));
+
+ // Log DataSource health
+ if (independentDataSource != null) {
+ independentDataSource.logHealthStatus();
+ } else {
+ LOGGER.info("Independent DataSource: Not configured");
+ }
+
+ LOGGER.info("=== End Status Report ===");
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/cache/DataKeyCache.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/cache/DataKeyCache.java
new file mode 100644
index 000000000..a9d7b4f0e
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/cache/DataKeyCache.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.cache;
+
+import software.amazon.jdbc.plugin.encryption.model.EncryptionConfig;
+import java.util.logging.Logger;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Thread-safe cache for data keys with configurable expiration and size limits.
+ * Provides metrics for cache performance monitoring.
+ */
+public class DataKeyCache {
+
+ private static final Logger LOGGER = Logger.getLogger(DataKeyCache.class.getName());
+
+ private final Map cache;
+ private final ReadWriteLock cacheLock;
+ private final ScheduledExecutorService cleanupExecutor;
+ private final EncryptionConfig config;
+
+ // Metrics
+ private final AtomicLong hitCount = new AtomicLong(0);
+ private final AtomicLong missCount = new AtomicLong(0);
+ private final AtomicLong evictionCount = new AtomicLong(0);
+
+ public DataKeyCache(EncryptionConfig config) {
+ this.config = config;
+ this.cache = new ConcurrentHashMap<>();
+ this.cacheLock = new ReentrantReadWriteLock();
+ this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread t = new Thread(r, "DataKeyCache-Cleanup");
+ t.setDaemon(true);
+ return t;
+ });
+
+ // Schedule periodic cleanup of expired entries
+ long cleanupIntervalMs = Math.max(config.getDataKeyCacheExpiration().toMillis() / 4, 30000);
+ cleanupExecutor.scheduleAtFixedRate(this::cleanupExpiredEntries,
+ cleanupIntervalMs, cleanupIntervalMs, TimeUnit.MILLISECONDS);
+
+ LOGGER.info(()->String.format("DataKeyCache initialized with maxSize=%s, expiration=%s, cleanupInterval=%sms",
+ config.getDataKeyCacheMaxSize(), config.getDataKeyCacheExpiration(), cleanupIntervalMs));
+ }
+
+ /**
+ * Retrieves a data key from the cache.
+ *
+ * @param keyId the key identifier
+ * @return decrypted data key bytes, or null if not found or expired
+ */
+ public byte[] get(String keyId) {
+ if (!config.isDataKeyCacheEnabled() || keyId == null) {
+ return null;
+ }
+
+ cacheLock.readLock().lock();
+ try {
+ CacheEntry entry = cache.get(keyId);
+ if (entry == null) {
+ missCount.incrementAndGet();
+ LOGGER.finest(()->String.format("Cache miss for key: %s", keyId));
+ return null;
+ }
+
+ if (entry.isExpired(config.getDataKeyCacheExpiration())) {
+ missCount.incrementAndGet();
+ LOGGER.finest(()->String.format("Cache entry expired for key: %s", keyId));
+ // Remove expired entry (will be cleaned up by background thread)
+ return null;
+ }
+
+ hitCount.incrementAndGet();
+ LOGGER.finest(()->String.format("Cache hit for key: %s", keyId));
+ return entry.getDataKey();
+
+ } finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Stores a data key in the cache.
+ *
+ * @param keyId the key identifier
+ * @param dataKey the decrypted data key bytes
+ */
+ public void put(String keyId, byte[] dataKey) {
+ if (!config.isDataKeyCacheEnabled() || keyId == null || dataKey == null) {
+ return;
+ }
+
+ cacheLock.writeLock().lock();
+ try {
+ // Check if we need to evict entries to make room
+ if (cache.size() >= config.getDataKeyCacheMaxSize()) {
+ evictOldestEntry();
+ }
+
+ CacheEntry entry = new CacheEntry(dataKey.clone());
+ cache.put(keyId, entry);
+
+ LOGGER.finest(()->String.format("Cached data key for: %s", keyId));
+
+ } finally {
+ cacheLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Removes a specific key from the cache.
+ *
+ * @param keyId the key identifier to remove
+ */
+ public void remove(String keyId) {
+ if (keyId == null) {
+ return;
+ }
+
+ cacheLock.writeLock().lock();
+ try {
+ CacheEntry removed = cache.remove(keyId);
+ if (removed != null) {
+ removed.clear();
+ LOGGER.finest(()->String.format("Removed key from cache: %s", keyId));
+ }
+ } finally {
+ cacheLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Clears all entries from the cache.
+ */
+ public void clear() {
+ cacheLock.writeLock().lock();
+ try {
+ // Clear sensitive data before removing entries
+ cache.values().forEach(CacheEntry::clear);
+ cache.clear();
+ LOGGER.info("Cache cleared");
+ } finally {
+ cacheLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Returns cache statistics.
+ *
+ * @return CacheStats object with current metrics
+ */
+ public CacheStats getStats() {
+ cacheLock.readLock().lock();
+ try {
+ return new CacheStats(
+ cache.size(),
+ hitCount.get(),
+ missCount.get(),
+ evictionCount.get(),
+ calculateHitRate());
+ } finally {
+ cacheLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Shuts down the cache and cleans up resources.
+ */
+ public void shutdown() {
+ LOGGER.info("Shutting down DataKeyCache");
+
+ cleanupExecutor.shutdown();
+ try {
+ if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
+ cleanupExecutor.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ cleanupExecutor.shutdownNow();
+ Thread.currentThread().interrupt();
+ }
+
+ clear();
+ }
+
+ /**
+ * Removes expired entries from the cache.
+ */
+ private void cleanupExpiredEntries() {
+ if (!config.isDataKeyCacheEnabled()) {
+ return;
+ }
+
+ cacheLock.writeLock().lock();
+ try {
+ Duration expiration = config.getDataKeyCacheExpiration();
+ int removedCount = 0;
+
+ Iterator> iterator = cache.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ if (entry.getValue().isExpired(expiration)) {
+ entry.getValue().clear();
+ iterator.remove();
+ removedCount++;
+ }
+ }
+
+ if (removedCount > 0) {
+ int finalRemovedCount = removedCount;
+ LOGGER.finest(()->String.format("Cleaned up %d expired cache entries", finalRemovedCount));
+ }
+
+ } finally {
+ cacheLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Evicts the oldest entry from the cache to make room for new entries.
+ */
+ private void evictOldestEntry() {
+ if (cache.isEmpty()) {
+ return;
+ }
+
+ // Find the oldest entry
+ String oldestKey = null;
+ Instant oldestTime = Instant.MAX;
+
+ for (Map.Entry entry : cache.entrySet()) {
+ if (entry.getValue().getCreatedAt().isBefore(oldestTime)) {
+ oldestTime = entry.getValue().getCreatedAt();
+ oldestKey = entry.getKey();
+ }
+ }
+
+ if (oldestKey != null) {
+ CacheEntry removed = cache.remove(oldestKey);
+ if (removed != null) {
+ removed.clear();
+ evictionCount.incrementAndGet();
+ String finalOldestKey = oldestKey;
+ LOGGER.finest(()->String.format("Evicted oldest cache entry: %s", finalOldestKey));
+ }
+ }
+ }
+
+ /**
+ * Calculates the current cache hit rate.
+ */
+ private double calculateHitRate() {
+ long hits = hitCount.get();
+ long misses = missCount.get();
+ long total = hits + misses;
+
+ return total > 0 ? (double) hits / total : 0.0;
+ }
+
+ /**
+ * Cache entry wrapper that tracks creation time and provides secure cleanup.
+ */
+ private static class CacheEntry {
+ private final byte[] dataKey;
+ private final Instant createdAt;
+ private volatile boolean cleared = false;
+
+ public CacheEntry(byte[] dataKey) {
+ this.dataKey = dataKey;
+ this.createdAt = Instant.now();
+ }
+
+ public byte[] getDataKey() {
+ if (cleared) {
+ return null;
+ }
+ return dataKey.clone(); // Return copy for security
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public boolean isExpired(Duration expiration) {
+ return Instant.now().isAfter(createdAt.plus(expiration));
+ }
+
+ public void clear() {
+ if (!cleared && dataKey != null) {
+ Arrays.fill(dataKey, (byte) 0);
+ cleared = true;
+ }
+ }
+ }
+
+ /**
+ * Cache statistics data class.
+ */
+ public static class CacheStats {
+ private final int size;
+ private final long hitCount;
+ private final long missCount;
+ private final long evictionCount;
+ private final double hitRate;
+
+ public CacheStats(int size, long hitCount, long missCount, long evictionCount, double hitRate) {
+ this.size = size;
+ this.hitCount = hitCount;
+ this.missCount = missCount;
+ this.evictionCount = evictionCount;
+ this.hitRate = hitRate;
+ }
+
+ public int getSize() {
+ return size;
+ }
+
+ public long getHitCount() {
+ return hitCount;
+ }
+
+ public long getMissCount() {
+ return missCount;
+ }
+
+ public long getEvictionCount() {
+ return evictionCount;
+ }
+
+ public double getHitRate() {
+ return hitRate;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("CacheStats{size=%d, hits=%d, misses=%d, evictions=%d, hitRate=%.2f%%}",
+ size, hitCount, missCount, evictionCount, hitRate * 100);
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/AwsWrapperEncryptionExample.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/AwsWrapperEncryptionExample.java
new file mode 100644
index 000000000..fedbee002
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/AwsWrapperEncryptionExample.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.example;
+
+import software.amazon.jdbc.factory.EncryptingDataSourceFactory;
+import java.util.logging.Logger;
+import software.amazon.jdbc.plugin.encryption.wrapper.EncryptingDataSource;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+
+/**
+ * Example demonstrating how to use the encryption functionality with AWS Advanced JDBC Wrapper.
+ * This example shows different ways to configure and use encrypted database connections.
+ */
+public class AwsWrapperEncryptionExample {
+
+ private static final Logger LOGGER = Logger.getLogger(AwsWrapperEncryptionExample.class.getName());
+
+ public static void main(String[] args) {
+ try {
+ // Example 1: Using builder pattern
+ demonstrateBuilderPattern();
+
+ // Example 2: Using factory with properties
+ demonstrateFactoryWithProperties();
+
+ // Example 3: Using existing DataSource
+ demonstrateWrappingExistingDataSource();
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Example execution failed", e));
+ }
+ }
+
+ /**
+ * Demonstrates using the builder pattern to create an encrypted DataSource.
+ */
+ private static void demonstrateBuilderPattern() throws SQLException {
+ LOGGER.info("=== Builder Pattern Example ===");
+
+ EncryptingDataSource dataSource = new EncryptingDataSourceFactory.Builder()
+ .jdbcUrl("jdbc:postgresql://localhost:5432/mydb")
+ .username("myuser")
+ .password("mypassword")
+ .kmsKeyArn("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012")
+ .region("us-east-1")
+ .cacheEnabled(true)
+ .cacheExpirationMinutes(30)
+ .cacheMaxSize(1000)
+ .build();
+
+ // Use the DataSource
+ performDatabaseOperations(dataSource, "Builder Pattern");
+
+ // Clean up
+ dataSource.close();
+ }
+
+ /**
+ * Demonstrates using the factory with explicit properties.
+ */
+ private static void demonstrateFactoryWithProperties() throws SQLException {
+ LOGGER.info("=== Factory with Properties Example ===");
+
+ Properties encryptionProperties = new Properties();
+
+ // KMS configuration
+ encryptionProperties.setProperty("kms.keyArn", "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012");
+ encryptionProperties.setProperty("kms.region", "us-east-1");
+
+ // Cache configuration
+ encryptionProperties.setProperty("cache.enabled", "true");
+ encryptionProperties.setProperty("cache.expirationMinutes", "30");
+ encryptionProperties.setProperty("cache.maxSize", "1000");
+
+ // Retry configuration
+ encryptionProperties.setProperty("kms.maxRetries", "3");
+ encryptionProperties.setProperty("kms.retryBackoffBaseMs", "100");
+
+ // AWS Wrapper configuration (optional)
+ encryptionProperties.setProperty("wrapperLogUnclosedConnections", "true");
+ encryptionProperties.setProperty("wrapperLoggerLevel", "INFO");
+
+ EncryptingDataSource dataSource = EncryptingDataSourceFactory.createWithAwsWrapper(
+ "jdbc:postgresql://localhost:5432/mydb",
+ "myuser",
+ "mypassword",
+ encryptionProperties
+ );
+
+ // Use the DataSource
+ performDatabaseOperations(dataSource, "Factory with Properties");
+
+ // Clean up
+ dataSource.close();
+ }
+
+ /**
+ * Demonstrates wrapping an existing DataSource with encryption.
+ */
+ private static void demonstrateWrappingExistingDataSource() throws SQLException {
+ LOGGER.info("=== Wrapping Existing DataSource Example ===");
+
+ // Create an existing DataSource (this could be from a connection pool, etc.)
+ DataSource existingDataSource = createExistingDataSource();
+
+ // Wrap it with encryption
+ EncryptingDataSource encryptingDataSource = EncryptingDataSourceFactory.createWithDefaults(
+ existingDataSource,
+ "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
+ "us-east-1"
+ );
+
+ // Use the encrypted DataSource
+ performDatabaseOperations(encryptingDataSource, "Wrapped Existing DataSource");
+
+ // Clean up
+ encryptingDataSource.close();
+ }
+
+ /**
+ * Performs sample database operations to demonstrate encryption/decryption.
+ */
+ private static void performDatabaseOperations(DataSource dataSource, String exampleName) {
+ LOGGER.info(()->String.format("Performing database operations for: %s", exampleName));
+
+ try (Connection connection = dataSource.getConnection()) {
+
+ // Create test table (if not exists)
+ createTestTable(connection);
+
+ // Insert encrypted data
+ insertTestData(connection);
+
+ // Query and decrypt data
+ queryTestData(connection);
+
+ LOGGER.info(()->String.format("Database operations completed successfully for: %s", exampleName));
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Database operations failed for: " + exampleName, e));
+ }
+ }
+
+ /**
+ * Creates a test table for demonstration.
+ */
+ private static void createTestTable(Connection connection) throws SQLException {
+ String createTableSql = "CREATE TABLE IF NOT EXISTS test_users (" +
+ "id SERIAL PRIMARY KEY, " +
+ "name VARCHAR(100) NOT NULL, " +
+ "email VARCHAR(100), " +
+ "ssn VARCHAR(20), " +
+ "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" +
+ ")";
+
+ try (PreparedStatement stmt = connection.prepareStatement(createTableSql)) {
+ stmt.executeUpdate();
+ LOGGER.finest(()->"Test table created or already exists");
+ }
+ }
+
+ /**
+ * Inserts test data that will be automatically encrypted for configured columns.
+ */
+ private static void insertTestData(Connection connection) throws SQLException {
+ String insertSql = "INSERT INTO test_users (name, email, ssn) VALUES (?, ?, ?)";
+
+ try (PreparedStatement stmt = connection.prepareStatement(insertSql)) {
+ // Insert first user
+ stmt.setString(1, "John Doe");
+ stmt.setString(2, "john.doe@example.com"); // Will be encrypted if configured
+ stmt.setString(3, "123-45-6789"); // Will be encrypted if configured
+ stmt.executeUpdate();
+
+ // Insert second user
+ stmt.setString(1, "Jane Smith");
+ stmt.setString(2, "jane.smith@example.com"); // Will be encrypted if configured
+ stmt.setString(3, "987-65-4321"); // Will be encrypted if configured
+ stmt.executeUpdate();
+
+ LOGGER.info("Inserted test data with automatic encryption");
+ }
+ }
+
+ /**
+ * Queries test data that will be automatically decrypted for configured columns.
+ */
+ private static void queryTestData(Connection connection) throws SQLException {
+ String selectSql = "SELECT id, name, email, ssn FROM test_users ORDER BY id";
+
+ try (PreparedStatement stmt = connection.prepareStatement(selectSql);
+ ResultSet rs = stmt.executeQuery()) {
+
+ LOGGER.info("Querying test data with automatic decryption:");
+
+ while (rs.next()) {
+ int id = rs.getInt("id");
+ String name = rs.getString("name");
+ String email = rs.getString("email"); // Will be decrypted if configured
+ String ssn = rs.getString("ssn"); // Will be decrypted if configured
+
+ LOGGER.info(()->String.format("User %s: Name=%s, Email=%s, SSN=%s", id, name, email, ssn));
+ }
+ }
+ }
+
+ /**
+ * Creates a sample existing DataSource for demonstration.
+ * In a real application, this might come from a connection pool or dependency injection.
+ */
+ private static DataSource createExistingDataSource() {
+ // This is a simplified example - in practice you might use HikariCP, etc.
+ return new DataSource() {
+ @Override
+ public Connection getConnection() throws SQLException {
+ return java.sql.DriverManager.getConnection(
+ "jdbc:postgresql://localhost:5432/mydb",
+ "myuser",
+ "mypassword"
+ );
+ }
+
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ return java.sql.DriverManager.getConnection(
+ "jdbc:postgresql://localhost:5432/mydb",
+ username,
+ password
+ );
+ }
+
+ // Other DataSource methods with default implementations
+ @Override
+ public java.io.PrintWriter getLogWriter() throws SQLException {
+ return null;
+ }
+
+ @Override
+ public void setLogWriter(java.io.PrintWriter out) throws SQLException {
+ // No-op
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ // No-op
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ return 0;
+ }
+
+ @Override
+ public java.util.logging.Logger getParentLogger() {
+ return java.util.logging.Logger.getLogger("javax.sql.DataSource");
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ throw new SQLException("Cannot unwrap to " + iface.getName());
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return false;
+ }
+ };
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/DataSourceLifecycleExample.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/DataSourceLifecycleExample.java
new file mode 100644
index 000000000..270020c55
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/DataSourceLifecycleExample.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.example;
+
+import software.amazon.jdbc.factory.EncryptingDataSourceFactory;
+import java.util.logging.Logger;
+import software.amazon.jdbc.plugin.encryption.wrapper.EncryptingDataSource;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+
+/**
+ * Example demonstrating proper DataSource lifecycle management with encryption.
+ * Shows how to handle connection failures and DataSource state management.
+ */
+public class DataSourceLifecycleExample {
+
+ private static final Logger LOGGER = Logger.getLogger(DataSourceLifecycleExample.class.getName());
+
+ public static void main(String[] args) {
+ EncryptingDataSource dataSource = null;
+
+ try {
+ // Create the DataSource
+ dataSource = createDataSource();
+
+ // Demonstrate proper usage patterns
+ demonstrateHealthyUsage(dataSource);
+
+ // Demonstrate error handling
+ demonstrateErrorHandling(dataSource);
+
+ // Demonstrate lifecycle management
+ demonstrateLifecycleManagement(dataSource);
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Example execution failed %s", e.getMessage()));
+ } finally {
+ // Always clean up resources
+ if (dataSource != null) {
+ dataSource.close();
+ LOGGER.info("DataSource closed in finally block");
+ }
+ }
+ }
+
+ /**
+ * Creates an EncryptingDataSource for demonstration.
+ */
+ private static EncryptingDataSource createDataSource() throws SQLException {
+ LOGGER.info("=== Creating EncryptingDataSource ===");
+
+ EncryptingDataSource dataSource = new EncryptingDataSourceFactory.Builder()
+ .jdbcUrl("jdbc:postgresql://localhost:5432/mydb")
+ .username("myuser")
+ .password("mypassword")
+ .kmsKeyArn("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012")
+ .region("us-east-1")
+ .cacheEnabled(true)
+ .build();
+
+ LOGGER.info("EncryptingDataSource created successfully");
+ return dataSource;
+ }
+
+ /**
+ * Demonstrates healthy DataSource usage patterns.
+ */
+ private static void demonstrateHealthyUsage(EncryptingDataSource dataSource) {
+ LOGGER.info("=== Demonstrating Healthy Usage ===");
+
+ // Check if DataSource is available before using
+ if (!dataSource.isConnectionAvailable()) {
+ LOGGER.warning("DataSource is not available - skipping operations");
+ return;
+ }
+
+ // Use try-with-resources for proper connection management
+ try (Connection connection = dataSource.getConnection()) {
+ LOGGER.info(()->String.format("Successfully obtained connection: %s", connection.getClass().getSimpleName()));
+
+ // Verify connection is valid
+ if (connection.isValid(5)) {
+ LOGGER.info(()->"Connection is valid");
+ } else {
+ LOGGER.warning(()->"Connection is not valid");
+ }
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Failed to get or use connection %s", e.getMessage()));
+ }
+ }
+
+ /**
+ * Demonstrates error handling patterns.
+ */
+ private static void demonstrateErrorHandling(EncryptingDataSource dataSource) {
+ LOGGER.info(()->"=== Demonstrating Error Handling ===");
+
+ // Attempt to get multiple connections to test resilience
+ for (int i = 0; i < 3; i++) {
+ try (Connection connection = dataSource.getConnection()) {
+ int finalI = i;
+ LOGGER.info(()->String.format("Connection attempt %d: Success", finalI + 1));
+
+ // Simulate some work
+ Thread.sleep(100);
+
+ } catch (SQLException e) {
+ int finalI1 = i;
+ LOGGER.severe(()->String.format("Connection attempt %s failed: %s", finalI1 + 1, e.getMessage()));
+
+ // Check if DataSource is still healthy
+ if (!dataSource.isConnectionAvailable()) {
+ LOGGER.severe("DataSource is no longer available - stopping attempts");
+ break;
+ }
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ }
+
+ /**
+ * Demonstrates DataSource lifecycle management.
+ */
+ private static void demonstrateLifecycleManagement(EncryptingDataSource dataSource) {
+ LOGGER.info("=== Demonstrating Lifecycle Management ===");
+
+ // Check initial state
+ LOGGER.info(()->String.format("DataSource closed: %s", dataSource.isClosed()));
+ LOGGER.info(()->String.format("Connection available: %s", dataSource.isConnectionAvailable()));
+
+ // Get a connection before closing
+ try (Connection connection = dataSource.getConnection()) {
+ LOGGER.info(()->String.format("Got connection before close: %s", connection.getClass().getSimpleName()));
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Failed to get connection before close %s", e.getMessage()));
+ }
+
+ // Close the DataSource
+ dataSource.close();
+ LOGGER.info(()->String.format("DataSource closed: %s", dataSource.isClosed()));
+ LOGGER.info(()->String.format("Connection available after close: %s", dataSource.isConnectionAvailable()));
+
+ // Try to get connection after close (should fail)
+ try (Connection connection = dataSource.getConnection()) {
+ LOGGER.severe(()->"Unexpectedly got connection after close!");
+ } catch (SQLException e) {
+ LOGGER.info(()->String.format("Expected failure getting connection after close: %s", e.getMessage()));
+ }
+
+ // Multiple close calls should be safe
+ dataSource.close();
+ dataSource.close();
+ LOGGER.info(()->"Multiple close calls completed safely");
+ }
+
+ /**
+ * Demonstrates connection validation and recovery patterns.
+ *
+ * @param originalDataSource Original data source to wrap
+ */
+ public static void demonstrateConnectionRecovery(DataSource originalDataSource) {
+ LOGGER.info(()->"=== Demonstrating Connection Recovery ===");
+
+ EncryptingDataSource dataSource = null;
+
+ try {
+ // Wrap the original DataSource
+ dataSource = EncryptingDataSourceFactory.createWithDefaults(
+ originalDataSource,
+ "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012",
+ "us-east-1"
+ );
+
+ // Implement retry logic for connection failures
+ Connection connection = getConnectionWithRetry(dataSource, 3, 1000);
+
+ if (connection != null) {
+ try (Connection conn = connection) {
+ LOGGER.info(()->"Successfully recovered connection");
+ }
+ } else {
+ LOGGER.severe(()->"Failed to recover connection after retries");
+ }
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Connection recovery demonstration failed %s", e.getMessage()));
+ } finally {
+ if (dataSource != null) {
+ dataSource.close();
+ }
+ }
+ }
+
+ /**
+ * Attempts to get a connection with retry logic.
+ */
+ private static Connection getConnectionWithRetry(EncryptingDataSource dataSource, int maxRetries, long delayMs) {
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ int finalAttempt = attempt;
+ try {
+ LOGGER.info(()->String.format("Connection attempt %s of %s", finalAttempt, maxRetries));
+
+ if (!dataSource.isConnectionAvailable()) {
+ LOGGER.warning(()->String.format("DataSource not available on attempt %s", finalAttempt));
+ Thread.sleep(delayMs);
+ continue;
+ }
+
+ Connection connection = dataSource.getConnection();
+ LOGGER.info(()->String.format("Successfully got connection on attempt %s", finalAttempt));
+ return connection;
+
+ } catch (SQLException e) {
+ LOGGER.warning(()->String.format("Connection attempt %s failed: %s", finalAttempt, e.getMessage()));
+
+ if (attempt < maxRetries) {
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/PropertiesFileExample.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/PropertiesFileExample.java
new file mode 100644
index 000000000..78e037868
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/example/PropertiesFileExample.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.example;
+
+import software.amazon.jdbc.factory.EncryptingDataSourceFactory;
+import java.util.logging.Logger;
+import software.amazon.jdbc.plugin.encryption.wrapper.EncryptingDataSource;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Properties;
+
+/**
+ * Example demonstrating how to use the encryption functionality with a properties file.
+ */
+public class PropertiesFileExample {
+
+ private static final Logger LOGGER = Logger.getLogger(PropertiesFileExample.class.getName());
+
+ public static void main(String[] args) {
+ try {
+ // Load properties from file
+ Properties properties = loadPropertiesFromFile("example-jdbc-wrapper.properties");
+
+ // Create EncryptingDataSource using the properties
+ EncryptingDataSource dataSource = createDataSourceFromProperties(properties);
+
+ // Use the DataSource
+ demonstrateEncryptedOperations(dataSource);
+
+ // Clean up
+ dataSource.close();
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Example execution failed %s", e.getMessage()));
+ }
+ }
+
+ /**
+ * Loads properties from a file in the classpath.
+ */
+ private static Properties loadPropertiesFromFile(String filename) throws IOException {
+ Properties properties = new Properties();
+
+ try (InputStream inputStream = PropertiesFileExample.class.getClassLoader()
+ .getResourceAsStream(filename)) {
+
+ if (inputStream == null) {
+ throw new IOException("Properties file not found: " + filename);
+ }
+
+ properties.load(inputStream);
+ LOGGER.info(()->String.format("Loaded properties from file: %s", filename));
+ }
+
+ return properties;
+ }
+
+ /**
+ * Creates an EncryptingDataSource from properties.
+ */
+ private static EncryptingDataSource createDataSourceFromProperties(Properties properties) throws SQLException {
+ String jdbcUrl = properties.getProperty("jdbcUrl");
+ String username = properties.getProperty("username");
+ String password = properties.getProperty("password");
+
+ if (jdbcUrl == null || username == null || password == null) {
+ throw new SQLException("Missing required database connection properties");
+ }
+
+ LOGGER.info(()->String.format("Creating EncryptingDataSource for URL: %s", jdbcUrl));
+
+ return EncryptingDataSourceFactory.createWithAwsWrapper(jdbcUrl, username, password, properties);
+ }
+
+ /**
+ * Demonstrates encrypted database operations.
+ */
+ private static void demonstrateEncryptedOperations(EncryptingDataSource dataSource) throws SQLException {
+ LOGGER.info(()->"Demonstrating encrypted database operations");
+
+ try (Connection connection = dataSource.getConnection()) {
+
+ // Create test table
+ createTestTable(connection);
+
+ // Insert encrypted data
+ insertTestData(connection);
+
+ // Query and decrypt data
+ queryTestData(connection);
+
+ LOGGER.info("Encrypted operations completed successfully");
+ }
+ }
+
+ /**
+ * Creates a test table for demonstration.
+ */
+ private static void createTestTable(Connection connection) throws SQLException {
+ String createTableSql = "CREATE TABLE IF NOT EXISTS test_users (" +
+ "id SERIAL PRIMARY KEY, " +
+ "name VARCHAR(100) NOT NULL, " +
+ "email VARCHAR(100), " +
+ "ssn VARCHAR(20), " +
+ "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" +
+ ")";
+
+ try (PreparedStatement stmt = connection.prepareStatement(createTableSql)) {
+ stmt.executeUpdate();
+ LOGGER.finest(()->"Test table created or already exists");
+ }
+ }
+
+ /**
+ * Inserts test data that will be automatically encrypted for configured columns.
+ */
+ private static void insertTestData(Connection connection) throws SQLException {
+ String insertSql = "INSERT INTO test_users (name, email, ssn) VALUES (?, ?, ?)";
+
+ try (PreparedStatement stmt = connection.prepareStatement(insertSql)) {
+ // Insert test user
+ stmt.setString(1, "Jane Doe");
+ stmt.setString(2, "jane.doe@example.com"); // Will be encrypted if configured
+ stmt.setString(3, "987-65-4321"); // Will be encrypted if configured
+ stmt.executeUpdate();
+
+ LOGGER.info("Inserted test data with automatic encryption");
+ }
+ }
+
+ /**
+ * Queries test data that will be automatically decrypted for configured columns.
+ */
+ private static void queryTestData(Connection connection) throws SQLException {
+ String selectSql = "SELECT id, name, email, ssn FROM test_users ORDER BY id DESC LIMIT 1";
+
+ try (PreparedStatement stmt = connection.prepareStatement(selectSql);
+ ResultSet rs = stmt.executeQuery()) {
+
+ if (rs.next()) {
+ int id = rs.getInt("id");
+ String name = rs.getString("name");
+ String email = rs.getString("email"); // Will be decrypted if configured
+ String ssn = rs.getString("ssn"); // Will be decrypted if configured
+
+ LOGGER.info(()->String.format("Retrieved user %s: Name=%s, Email=%s, SSN=%s", id, name, email, ssn));
+ }
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/exception/IndependentConnectionException.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/exception/IndependentConnectionException.java
new file mode 100644
index 000000000..7f44da22e
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/exception/IndependentConnectionException.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.exception;
+
+import software.amazon.jdbc.plugin.encryption.model.ConnectionParameters;
+
+import java.sql.SQLException;
+
+/**
+ * Exception thrown when independent connection creation fails.
+ * This exception provides detailed context about the connection creation failure,
+ * including the connection parameters that were attempted.
+ */
+public class IndependentConnectionException extends SQLException {
+
+ private final ConnectionParameters attemptedParameters;
+ private final String connectionAttempt;
+ private final String failureReason;
+
+ /**
+ * Creates a new IndependentConnectionException with a message and connection parameters.
+ *
+ * @param message the detailed error message
+ * @param attemptedParameters the connection parameters that failed to create a connection
+ */
+ public IndependentConnectionException(String message, ConnectionParameters attemptedParameters) {
+ super(formatMessage(message, attemptedParameters, null));
+ this.attemptedParameters = attemptedParameters;
+ this.connectionAttempt = null;
+ this.failureReason = null;
+ }
+
+ /**
+ * Creates a new IndependentConnectionException with a message, cause, and connection parameters.
+ *
+ * @param message the detailed error message
+ * @param cause the underlying cause of the connection failure
+ * @param attemptedParameters the connection parameters that failed to create a connection
+ */
+ public IndependentConnectionException(String message, Throwable cause, ConnectionParameters attemptedParameters) {
+ super(formatMessage(message, attemptedParameters, cause), cause);
+ this.attemptedParameters = attemptedParameters;
+ this.connectionAttempt = null;
+ this.failureReason = null;
+ }
+
+ /**
+ * Creates a new IndependentConnectionException with detailed context.
+ *
+ * @param message the detailed error message
+ * @param attemptedParameters the connection parameters that failed to create a connection
+ * @param connectionAttempt description of what connection creation was attempted
+ * @param failureReason specific reason for the connection failure
+ */
+ public IndependentConnectionException(String message, ConnectionParameters attemptedParameters,
+ String connectionAttempt, String failureReason) {
+ super(formatMessage(message, attemptedParameters, null, connectionAttempt, failureReason));
+ this.attemptedParameters = attemptedParameters;
+ this.connectionAttempt = connectionAttempt;
+ this.failureReason = failureReason;
+ }
+
+ /**
+ * Creates a new IndependentConnectionException with detailed context and cause.
+ *
+ * @param message the detailed error message
+ * @param cause the underlying cause of the connection failure
+ * @param attemptedParameters the connection parameters that failed to create a connection
+ * @param connectionAttempt description of what connection creation was attempted
+ * @param failureReason specific reason for the connection failure
+ */
+ public IndependentConnectionException(String message, Throwable cause, ConnectionParameters attemptedParameters,
+ String connectionAttempt, String failureReason) {
+ super(formatMessage(message, attemptedParameters, cause, connectionAttempt, failureReason), cause);
+ this.attemptedParameters = attemptedParameters;
+ this.connectionAttempt = connectionAttempt;
+ this.failureReason = failureReason;
+ }
+
+ /**
+ * Gets the connection parameters that failed to create a connection.
+ *
+ * @return the attempted connection parameters
+ */
+ public ConnectionParameters getAttemptedParameters() {
+ return attemptedParameters;
+ }
+
+ /**
+ * Gets the description of what connection creation was attempted.
+ *
+ * @return the connection attempt description, or null if not provided
+ */
+ public String getConnectionAttempt() {
+ return connectionAttempt;
+ }
+
+ /**
+ * Gets the specific reason for the connection failure.
+ *
+ * @return the failure reason, or null if not provided
+ */
+ public String getFailureReason() {
+ return failureReason;
+ }
+
+ /**
+ * Formats the error message with connection parameters and cause information.
+ */
+ private static String formatMessage(String message, ConnectionParameters attemptedParameters, Throwable cause) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Independent connection creation failed");
+
+ if (message != null && !message.isEmpty()) {
+ sb.append(" - ").append(message);
+ }
+
+ if (attemptedParameters != null) {
+ sb.append(" (attempted URL: ");
+ String jdbcUrl = attemptedParameters.getJdbcUrl();
+ if (jdbcUrl != null) {
+ // Mask sensitive information in URL
+ sb.append(maskSensitiveUrl(jdbcUrl));
+ } else {
+ sb.append("null");
+ }
+ sb.append(")");
+ }
+
+ if (cause != null) {
+ sb.append(" (caused by: ").append(cause.getClass().getSimpleName());
+ if (cause.getMessage() != null) {
+ sb.append(": ").append(cause.getMessage());
+ }
+ sb.append(")");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the error message with detailed context information.
+ */
+ private static String formatMessage(String message, ConnectionParameters attemptedParameters, Throwable cause,
+ String connectionAttempt, String failureReason) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Independent connection creation failed");
+
+ if (connectionAttempt != null && !connectionAttempt.isEmpty()) {
+ sb.append(" while attempting: ").append(connectionAttempt);
+ }
+
+ if (message != null && !message.isEmpty()) {
+ sb.append(" - ").append(message);
+ }
+
+ if (failureReason != null && !failureReason.isEmpty()) {
+ sb.append(" (reason: ").append(failureReason).append(")");
+ }
+
+ if (attemptedParameters != null) {
+ sb.append(" (attempted URL: ");
+ String jdbcUrl = attemptedParameters.getJdbcUrl();
+ if (jdbcUrl != null) {
+ sb.append(maskSensitiveUrl(jdbcUrl));
+ } else {
+ sb.append("null");
+ }
+ sb.append(")");
+ }
+
+ if (cause != null) {
+ sb.append(" (caused by: ").append(cause.getClass().getSimpleName());
+ if (cause.getMessage() != null) {
+ sb.append(": ").append(cause.getMessage());
+ }
+ sb.append(")");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Masks sensitive information in JDBC URLs for logging purposes.
+ * Removes passwords and other sensitive parameters while preserving
+ * useful debugging information.
+ */
+ private static String maskSensitiveUrl(String jdbcUrl) {
+ if (jdbcUrl == null) {
+ return null;
+ }
+
+ // Remove password parameters from URL
+ String masked = jdbcUrl.replaceAll("([?&]password=)[^&]*", "$1***");
+ masked = masked.replaceAll("([?&]pwd=)[^&]*", "$1***");
+
+ // Remove user credentials from URL if present
+ masked = masked.replaceAll("://[^:/@]+:[^@]*@", "://***:***@");
+
+ return masked;
+ }
+}
\ No newline at end of file
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/factory/IndependentDataSource.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/factory/IndependentDataSource.java
new file mode 100644
index 000000000..d73aebff9
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/factory/IndependentDataSource.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.factory;
+
+import software.amazon.jdbc.PluginService;
+import software.amazon.jdbc.HostSpec;
+import software.amazon.jdbc.plugin.encryption.logging.ErrorContext;
+import java.util.logging.Logger;
+import org.slf4j.MDC;
+
+import javax.sql.DataSource;
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * DataSource implementation that creates independent connections using PluginService.
+ * This ensures that MetadataManager gets its own connections and doesn't share with client applications.
+ */
+public class IndependentDataSource implements DataSource {
+
+ private static final Logger LOGGER = Logger.getLogger(IndependentDataSource.class.getName());
+
+ private final PluginService pluginService;
+ private final Properties connectionProperties;
+ private int loginTimeout = 0;
+ private PrintWriter logWriter;
+
+ // Connection monitoring metrics
+ private final AtomicLong connectionRequestCount = new AtomicLong(0);
+ private final AtomicLong successfulConnectionCount = new AtomicLong(0);
+ private final AtomicLong failedConnectionCount = new AtomicLong(0);
+ private volatile long lastSuccessfulConnectionTime = 0;
+ private volatile long lastFailedConnectionTime = 0;
+
+ /**
+ * Creates an IndependentDataSource with the given PluginService.
+ *
+ * @param pluginService the PluginService to use for creating connections
+ * @throws IllegalArgumentException if pluginService is null
+ */
+ public IndependentDataSource(PluginService pluginService) {
+ this(pluginService, new Properties());
+ }
+
+ /**
+ * Creates an IndependentDataSource with PluginService and connection properties.
+ *
+ * @param pluginService the PluginService to use for creating connections
+ * @param connectionProperties additional connection properties
+ * @throws IllegalArgumentException if pluginService is null
+ */
+ public IndependentDataSource(PluginService pluginService, Properties connectionProperties) {
+ if (pluginService == null) {
+ throw new IllegalArgumentException("PluginService cannot be null");
+ }
+
+ this.pluginService = pluginService;
+ this.connectionProperties = connectionProperties != null ? connectionProperties : new Properties();
+
+ LOGGER.info(()->"Created IndependentDataSource with PluginService");
+ LOGGER.finest(()->String.format("IndependentDataSource configuration: PropertiesCount=%s",
+ this.connectionProperties.size()));
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException {
+ long requestId = connectionRequestCount.incrementAndGet();
+
+ MDC.put("operation", "GET_INDEPENDENT_CONNECTION");
+ MDC.put("requestId", String.valueOf(requestId));
+
+ try {
+ LOGGER.finest(()->String.format("Connection request #%s - creating new independent connection via PluginService", requestId));
+ return createNewConnection();
+ } finally {
+ MDC.remove("operation");
+ MDC.remove("requestId");
+ }
+ }
+
+ @Override
+ public Connection getConnection(String username, String password) throws SQLException {
+ long requestId = connectionRequestCount.incrementAndGet();
+
+ MDC.put("operation", "GET_INDEPENDENT_CONNECTION_WITH_CREDENTIALS");
+ MDC.put("requestId", String.valueOf(requestId));
+
+ try {
+ LOGGER.finest(()->String.format("Connection request #%s - creating new independent connection with provided credentials", requestId));
+
+ // Create modified properties with the provided credentials
+ Properties modifiedProps = new Properties(connectionProperties);
+ modifiedProps.setProperty("user", username);
+ modifiedProps.setProperty("password", password);
+
+ return createNewConnection(modifiedProps);
+ } finally {
+ MDC.remove("operation");
+ MDC.remove("requestId");
+ }
+ }
+
+ /**
+ * Creates a new independent connection using the PluginService.
+ *
+ * @return a new database connection
+ * @throws SQLException if connection creation fails
+ */
+ private Connection createNewConnection() throws SQLException {
+ return createNewConnection(connectionProperties);
+ }
+
+ /**
+ * Creates a new independent connection using the PluginService with specified properties.
+ *
+ * @param props the connection properties to use
+ * @return a new database connection
+ * @throws SQLException if connection creation fails
+ */
+ private Connection createNewConnection(Properties props) throws SQLException {
+ long startTime = System.currentTimeMillis();
+
+ LOGGER.finest(()->"Creating new independent connection via PluginService");
+
+ try {
+ // Get current host spec from PluginService
+ HostSpec hostSpec = pluginService.getCurrentHostSpec();
+
+ // Create connection using PluginService
+ Connection connection = pluginService.forceConnect(hostSpec, props);
+
+ long duration = System.currentTimeMillis() - startTime;
+ successfulConnectionCount.incrementAndGet();
+ lastSuccessfulConnectionTime = System.currentTimeMillis();
+
+ LOGGER.info(()->String.format("Successfully created independent connection via PluginService in %sms " +
+ "(total successful: %s, total failed: %s)",
+ duration, successfulConnectionCount.get(), failedConnectionCount.get()));
+
+ return connection;
+
+ } catch (SQLException e) {
+ long duration = System.currentTimeMillis() - startTime;
+ failedConnectionCount.incrementAndGet();
+ lastFailedConnectionTime = System.currentTimeMillis();
+
+ LOGGER.severe(()->String.format("Failed to create independent connection via PluginService after %sms: %s " +
+ "(total successful: %d, total failed: %d)",
+ duration, e.getMessage(),
+ successfulConnectionCount.get(), failedConnectionCount.get()));
+
+ // Create detailed error context for troubleshooting
+ String errorDetails = ErrorContext.builder()
+ .operation("CREATE_INDEPENDENT_CONNECTION_VIA_PLUGIN_SERVICE")
+ .buildMessage("Connection creation failed: " + e.getMessage());
+
+ LOGGER.severe(()->String.format("Connection creation error details: %s", errorDetails));
+
+ throw new SQLException(
+ "Failed to create independent connection via PluginService: " + e.getMessage(),
+ e
+ );
+ }
+ }
+
+ /**
+ * Validates that a connection can be created with the current PluginService.
+ *
+ * @return true if a connection can be created, false otherwise
+ */
+ public boolean validateConnection() {
+ try (Connection conn = getConnection()) {
+ return conn != null && !conn.isClosed();
+ } catch (SQLException e) {
+ LOGGER.finest(()->String.format("Connection validation failed", e));
+ return false;
+ }
+ }
+
+ /**
+ * Gets the PluginService used by this DataSource.
+ *
+ * @return the PluginService
+ */
+ public PluginService getPluginService() {
+ return pluginService;
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException {
+ if (iface.isInstance(this)) {
+ return iface.cast(this);
+ }
+ throw new SQLException("Cannot unwrap to " + iface.getName());
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException {
+ return iface.isInstance(this);
+ }
+
+ @Override
+ public PrintWriter getLogWriter() throws SQLException {
+ return logWriter;
+ }
+
+ @Override
+ public void setLogWriter(PrintWriter out) throws SQLException {
+ this.logWriter = out;
+ }
+
+ @Override
+ public void setLoginTimeout(int seconds) throws SQLException {
+ this.loginTimeout = seconds;
+ }
+
+ @Override
+ public int getLoginTimeout() throws SQLException {
+ return loginTimeout;
+ }
+
+ @Override
+ public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
+ throw new SQLFeatureNotSupportedException("getParentLogger is not supported");
+ }
+
+ // Connection monitoring and metrics methods
+
+ /**
+ * Gets the total number of connection requests made to this DataSource.
+ *
+ * @return the total connection request count
+ */
+ public long getConnectionRequestCount() {
+ return connectionRequestCount.get();
+ }
+
+ /**
+ * Gets the number of successful connection creations.
+ *
+ * @return the successful connection count
+ */
+ public long getSuccessfulConnectionCount() {
+ return successfulConnectionCount.get();
+ }
+
+ /**
+ * Gets the number of failed connection creation attempts.
+ *
+ * @return the failed connection count
+ */
+ public long getFailedConnectionCount() {
+ return failedConnectionCount.get();
+ }
+
+ /**
+ * Gets the timestamp of the last successful connection creation.
+ *
+ * @return the timestamp in milliseconds, or 0 if no successful connections
+ */
+ public long getLastSuccessfulConnectionTime() {
+ return lastSuccessfulConnectionTime;
+ }
+
+ /**
+ * Gets the timestamp of the last failed connection attempt.
+ *
+ * @return the timestamp in milliseconds, or 0 if no failed connections
+ */
+ public long getLastFailedConnectionTime() {
+ return lastFailedConnectionTime;
+ }
+
+ /**
+ * Calculates the connection success rate as a percentage.
+ *
+ * @return the success rate (0.0 to 1.0), or 1.0 if no attempts have been made
+ */
+ public double getConnectionSuccessRate() {
+ long total = connectionRequestCount.get();
+ if (total == 0) return 1.0;
+
+ return (double) successfulConnectionCount.get() / total;
+ }
+
+ /**
+ * Checks if the DataSource is currently healthy based on recent connection attempts.
+ *
+ * @return true if the DataSource appears healthy, false otherwise
+ */
+ public boolean isHealthy() {
+ // Consider healthy if success rate is above 80% or if we haven't had failures recently
+ double successRate = getConnectionSuccessRate();
+ long timeSinceLastFailure = System.currentTimeMillis() - lastFailedConnectionTime;
+
+ return successRate >= 0.8 || (lastFailedConnectionTime == 0) || (timeSinceLastFailure > 300000); // 5 minutes
+ }
+
+ /**
+ * Gets a comprehensive status message about the DataSource health and metrics.
+ *
+ * @return a detailed status message
+ */
+ public String getHealthStatus() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("IndependentDataSource Status: ");
+ sb.append("Healthy=").append(isHealthy()).append(", ");
+ sb.append("Requests=").append(connectionRequestCount.get()).append(", ");
+ sb.append("Successful=").append(successfulConnectionCount.get()).append(", ");
+ sb.append("Failed=").append(failedConnectionCount.get()).append(", ");
+ sb.append("SuccessRate=").append(String.format("%.2f%%", getConnectionSuccessRate() * 100));
+
+ if (lastSuccessfulConnectionTime > 0) {
+ long timeSinceSuccess = System.currentTimeMillis() - lastSuccessfulConnectionTime;
+ sb.append(", LastSuccess=").append(timeSinceSuccess).append("ms ago");
+ }
+
+ if (lastFailedConnectionTime > 0) {
+ long timeSinceFailure = System.currentTimeMillis() - lastFailedConnectionTime;
+ sb.append(", LastFailure=").append(timeSinceFailure).append("ms ago");
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Logs the current health status and metrics.
+ */
+ public void logHealthStatus() {
+ String status = getHealthStatus();
+
+ if (isHealthy()) {
+ LOGGER.info(()->String.format("IndependentDataSource health check: %s", status));
+ } else {
+ LOGGER.warning(()->String.format("IndependentDataSource health check - UNHEALTHY: %s", status));
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementExample.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementExample.java
new file mode 100644
index 000000000..5142b9ed4
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementExample.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.key;
+
+import software.amazon.jdbc.plugin.encryption.metadata.MetadataManager;
+import software.amazon.jdbc.plugin.encryption.model.EncryptionConfig;
+import java.util.logging.Logger;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.kms.KmsClient;
+
+import javax.sql.DataSource;
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * Example demonstrating how to use the KeyManagementUtility for administrative tasks.
+ * This class shows typical workflows for setting up and managing encryption keys.
+ */
+public class KeyManagementExample {
+
+ private static final Logger LOGGER = Logger.getLogger(KeyManagementExample.class.getName());
+
+ private final KeyManagementUtility keyManagementUtility;
+
+ public KeyManagementExample(DataSource dataSource, KmsClient kmsClient) {
+ // Create encryption configuration
+ EncryptionConfig config = EncryptionConfig.builder()
+ .kmsRegion("us-east-1")
+ .defaultMasterKeyArn("arn:aws:kms:us-east-1:123456789012:key/default-key")
+ .cacheEnabled(true)
+ .cacheExpirationMinutes(30)
+ .maxRetries(3)
+ .retryBackoffBase(Duration.ofMillis(100))
+ .build();
+
+ // Create managers
+ KeyManager keyManager = null; //new KeyManager(kmsClient, dataSource, config);
+ MetadataManager metadataManager = null; //new MetadataManager(dataSource, config);
+
+ // Create utility
+ this.keyManagementUtility = new KeyManagementUtility(
+ keyManager, metadataManager, dataSource, kmsClient, config);
+ }
+
+ /**
+ * Example: Setting up encryption for a new application.
+ *
+ * @throws KeyManagementException if key management operations fail
+ */
+ public void setupNewApplication() throws KeyManagementException {
+ LOGGER.info("Setting up encryption for new application");
+
+ // 1. Create a master key for the application
+ String masterKeyArn = keyManagementUtility.createMasterKeyWithPermissions(
+ "JDBC Encryption Master Key for MyApp");
+
+ LOGGER.info(()->String.format("Created master key: %s", masterKeyArn));
+
+ // 2. Initialize encryption for sensitive columns
+ String userEmailKeyId = keyManagementUtility.initializeEncryptionForColumn(
+ "users", "email", masterKeyArn);
+
+ String userSsnKeyId = keyManagementUtility.initializeEncryptionForColumn(
+ "users", "ssn", masterKeyArn);
+
+ String orderCreditCardKeyId = keyManagementUtility.initializeEncryptionForColumn(
+ "orders", "credit_card_number", masterKeyArn);
+
+ LOGGER.info(()->String.format("Initialized encryption for users.email with key: %s", userEmailKeyId));
+ LOGGER.info(()->String.format("Initialized encryption for users.ssn with key: %s", userSsnKeyId));
+ LOGGER.info(()->String.format("Initialized encryption for orders.credit_card_number with key: %s", orderCreditCardKeyId));
+ }
+
+ /**
+ * Example: Adding encryption to an existing column.
+ *
+ * @throws KeyManagementException if key management operations fail
+ */
+ public void addEncryptionToExistingColumn() throws KeyManagementException {
+ LOGGER.info("Adding encryption to existing column");
+
+ String masterKeyArn = "arn:aws:kms:us-east-1:123456789012:key/existing-master-key";
+
+ // Validate the master key first
+ if (!keyManagementUtility.validateMasterKey(masterKeyArn)) {
+ throw new KeyManagementException("Master key is not valid or accessible: " + masterKeyArn);
+ }
+
+ // Initialize encryption for the column
+ String keyId = keyManagementUtility.initializeEncryptionForColumn(
+ "customers", "phone_number", masterKeyArn, "AES-256-GCM");
+
+ LOGGER.info(()->String.format("Added encryption to customers.phone_number with key: %s", keyId));
+ }
+
+ /**
+ * Example: Rotating keys for security compliance.
+ *
+ * @throws KeyManagementException if key management operations fail
+ */
+ public void performKeyRotation() throws KeyManagementException {
+ LOGGER.info("Performing key rotation for security compliance");
+
+ // Rotate key for a specific column
+ String newKeyId = keyManagementUtility.rotateDataKey("users", "ssn", null);
+ LOGGER.info(()->String.format("Rotated key for users.ssn, new key ID: %s", newKeyId));
+
+ // Rotate with a new master key
+ String newMasterKeyArn = keyManagementUtility.createMasterKeyWithPermissions(
+ "New Master Key for Enhanced Security");
+
+ String newKeyIdWithNewMaster = keyManagementUtility.rotateDataKey(
+ "orders", "credit_card_number", newMasterKeyArn);
+
+ LOGGER.info(()->String.format("Rotated key for orders.credit_card_number with new master key, new key ID: %s",
+ newKeyIdWithNewMaster));
+ }
+
+ /**
+ * Example: Auditing and managing existing keys.
+ *
+ * @throws KeyManagementException if key management operations fail
+ */
+ public void auditExistingKeys() throws KeyManagementException {
+ LOGGER.info("Auditing existing encryption keys");
+
+ // Find all columns using a specific key
+ String keyIdToAudit = "some-existing-key-id";
+ List columnsUsingKey = keyManagementUtility.getColumnsUsingKey(keyIdToAudit);
+
+ LOGGER.info(()->String.format("Key %s is used by %s columns: %s",
+ keyIdToAudit, columnsUsingKey.size(), columnsUsingKey));
+
+ // Validate all master keys are still accessible
+ String[] masterKeysToValidate = {
+ "arn:aws:kms:us-east-1:123456789012:key/key1",
+ "arn:aws:kms:us-east-1:123456789012:key/key2",
+ "arn:aws:kms:us-east-1:123456789012:key/key3"
+ };
+
+ for (String masterKeyArn : masterKeysToValidate) {
+ boolean isValid = keyManagementUtility.validateMasterKey(masterKeyArn);
+ LOGGER.info(()->String.format("Master key %s validation: %s", masterKeyArn, isValid ? "VALID" : "INVALID"));
+ }
+ }
+
+ /**
+ * Example: Removing encryption from a column (for decommissioning).
+ *
+ * @throws KeyManagementException if key management operations fail
+ */
+ public void removeEncryptionFromColumn() throws KeyManagementException {
+ LOGGER.info("Removing encryption from decommissioned column");
+
+ // Remove encryption configuration (keys remain for data recovery)
+ keyManagementUtility.removeEncryptionForColumn("old_table", "deprecated_column");
+
+ LOGGER.info("Removed encryption configuration for old_table.deprecated_column");
+ }
+
+ /**
+ * Main method demonstrating the complete workflow.
+ *
+ * @param args Command line arguments
+ */
+ public static void main(String[] args) {
+ try {
+ // In a real application, you would configure these properly
+ DataSource dataSource = null; // Configure your DataSource
+ KmsClient kmsClient = KmsClient.builder()
+ .region(Region.US_EAST_1)
+ .build();
+
+ KeyManagementExample example = new KeyManagementExample(dataSource, kmsClient);
+
+ // Run examples (commented out since we don't have real connections)
+ // example.setupNewApplication();
+ // example.addEncryptionToExistingColumn();
+ // example.performKeyRotation();
+ // example.auditExistingKeys();
+ // example.removeEncryptionFromColumn();
+
+ LOGGER.info("Key management examples completed successfully");
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Error running key management examples", e));
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementException.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementException.java
new file mode 100644
index 000000000..1419cf2c9
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementException.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.key;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Exception thrown when key management operations fail.
+ * Extends SQLException to integrate with JDBC error handling.
+ * Provides enhanced error context information for better troubleshooting.
+ */
+public class KeyManagementException extends SQLException {
+
+ private static final long serialVersionUID = 1L;
+
+ // SQL State codes for different key management error types
+ public static final String KEY_CREATION_FAILED_STATE = "KEY01";
+ public static final String KEY_RETRIEVAL_FAILED_STATE = "KEY02";
+ public static final String KEY_DECRYPTION_FAILED_STATE = "KEY03";
+ public static final String KEY_STORAGE_FAILED_STATE = "KEY04";
+ public static final String KMS_CONNECTION_FAILED_STATE = "KEY05";
+ public static final String INVALID_KEY_METADATA_STATE = "KEY06";
+
+ private final Map errorContext = new HashMap<>();
+
+ /**
+ * Constructs a KeyManagementException with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public KeyManagementException(String message) {
+ super(message, KEY_RETRIEVAL_FAILED_STATE);
+ }
+
+ /**
+ * Constructs a KeyManagementException with the specified detail message and cause.
+ *
+ * @param message the detail message
+ * @param cause the cause of this exception
+ */
+ public KeyManagementException(String message, Throwable cause) {
+ super(message, KEY_RETRIEVAL_FAILED_STATE, cause);
+ }
+
+ /**
+ * Constructs a KeyManagementException with the specified cause.
+ *
+ * @param cause the cause of this exception
+ */
+ public KeyManagementException(Throwable cause) {
+ super(cause.getMessage(), KEY_RETRIEVAL_FAILED_STATE, cause);
+ }
+
+ /**
+ * Constructs a KeyManagementException with the specified detail message,
+ * SQL state, and vendor code.
+ *
+ * @param message the detail message
+ * @param sqlState the SQL state
+ * @param vendorCode the vendor-specific error code
+ */
+ public KeyManagementException(String message, String sqlState, int vendorCode) {
+ super(message, sqlState, vendorCode);
+ }
+
+ /**
+ * Constructs a KeyManagementException with the specified detail message,
+ * SQL state, vendor code, and cause.
+ *
+ * @param message the detail message
+ * @param sqlState the SQL state
+ * @param vendorCode the vendor-specific error code
+ * @param cause the cause of this exception
+ */
+ public KeyManagementException(String message, String sqlState, int vendorCode, Throwable cause) {
+ super(message, sqlState, vendorCode, cause);
+ }
+
+ /**
+ * Constructs a KeyManagementException with the specified detail message, SQL state and cause.
+ *
+ * @param message the detail message
+ * @param sqlState the SQL state
+ * @param cause the cause of this exception
+ */
+ public KeyManagementException(String message, String sqlState, Throwable cause) {
+ super(message, sqlState, cause);
+ }
+
+ /**
+ * Adds context information to the exception.
+ *
+ * @param key the context key
+ * @param value the context value
+ * @return this exception for method chaining
+ */
+ public KeyManagementException withContext(String key, Object value) {
+ errorContext.put(key, value);
+ return this;
+ }
+
+ /**
+ * Adds key ID to the error context (sanitized).
+ *
+ * @param keyId the key ID
+ * @return this exception for method chaining
+ */
+ public KeyManagementException withKeyId(String keyId) {
+ return withContext("keyId", sanitizeKeyId(keyId));
+ }
+
+ /**
+ * Adds master key ARN to the error context (sanitized).
+ *
+ * @param masterKeyArn the master key ARN
+ * @return this exception for method chaining
+ */
+ public KeyManagementException withMasterKeyArn(String masterKeyArn) {
+ return withContext("masterKeyArn", sanitizeArn(masterKeyArn));
+ }
+
+ /**
+ * Adds operation type to the error context.
+ *
+ * @param operation the operation being performed
+ * @return this exception for method chaining
+ */
+ public KeyManagementException withOperation(String operation) {
+ return withContext("operation", operation);
+ }
+
+ /**
+ * Adds retry attempt information to the error context.
+ *
+ * @param attempt the current attempt number
+ * @param maxAttempts the maximum number of attempts
+ * @return this exception for method chaining
+ */
+ public KeyManagementException withRetryInfo(int attempt, int maxAttempts) {
+ return withContext("retryAttempt", attempt).withContext("maxRetryAttempts", maxAttempts);
+ }
+
+ /**
+ * Gets the error context map.
+ *
+ * @return a copy of the error context
+ */
+ public Map getErrorContext() {
+ return new HashMap<>(errorContext);
+ }
+
+ /**
+ * Gets a formatted error message including context information.
+ *
+ * @return formatted error message with context
+ */
+ public String getDetailedMessage() {
+ if (errorContext.isEmpty()) {
+ return getMessage();
+ }
+
+ StringBuilder sb = new StringBuilder(getMessage());
+ sb.append(" [Context: ");
+
+ boolean first = true;
+ for (Map.Entry entry : errorContext.entrySet()) {
+ if (!first) {
+ sb.append(", ");
+ }
+ sb.append(entry.getKey()).append("=").append(entry.getValue());
+ first = false;
+ }
+
+ sb.append("]");
+ return sb.toString();
+ }
+
+ /**
+ * Creates a KeyManagementException for key creation failures.
+ *
+ * @param message Error message
+ * @param cause Root cause
+ * @return KeyManagementException instance
+ */
+ public static KeyManagementException keyCreationFailed(String message, Throwable cause) {
+ return new KeyManagementException(message, KEY_CREATION_FAILED_STATE, cause);
+ }
+
+ /**
+ * Creates a KeyManagementException for key decryption failures.
+ *
+ * @param keyId Key ID
+ * @param masterKeyArn Master key ARN
+ * @param cause Root cause
+ * @return KeyManagementException instance
+ */
+ public static KeyManagementException keyDecryptionFailed(String keyId, String masterKeyArn, Throwable cause) {
+ return new KeyManagementException("Failed to decrypt data key", KEY_DECRYPTION_FAILED_STATE, cause)
+ .withKeyId(keyId)
+ .withMasterKeyArn(masterKeyArn);
+ }
+
+ /**
+ * Creates a KeyManagementException for key storage failures.
+ *
+ * @param message Error message
+ * @param cause Root cause
+ * @return KeyManagementException instance
+ */
+ public static KeyManagementException keyStorageFailed(String message, Throwable cause) {
+ return new KeyManagementException(message, KEY_STORAGE_FAILED_STATE, cause);
+ }
+
+ /**
+ * Creates a KeyManagementException for KMS connection failures.
+ *
+ * @param message Error message
+ * @param cause Root cause
+ * @return KeyManagementException instance
+ */
+ public static KeyManagementException kmsConnectionFailed(String message, Throwable cause) {
+ return new KeyManagementException(message, KMS_CONNECTION_FAILED_STATE, cause);
+ }
+
+ /**
+ * Creates a KeyManagementException for invalid key metadata.
+ *
+ * @param message Error message
+ * @return New KeyManagementException instance
+ */
+ public static KeyManagementException invalidKeyMetadata(String message) {
+ return new KeyManagementException(message, INVALID_KEY_METADATA_STATE, null);
+ }
+
+ // Sanitization methods to prevent sensitive data exposure
+
+ private String sanitizeKeyId(String keyId) {
+ if (keyId == null) return null;
+ // Show only first and last 4 characters of key ID
+ if (keyId.length() > 8) {
+ return keyId.substring(0, 4) + "***" + keyId.substring(keyId.length() - 4);
+ }
+ return "***";
+ }
+
+ private String sanitizeArn(String arn) {
+ if (arn == null) return null;
+ // Keep only the key ID part of the ARN
+ int lastSlash = arn.lastIndexOf('/');
+ if (lastSlash != -1 && lastSlash < arn.length() - 1) {
+ return "arn:aws:kms:***:***:key/" + arn.substring(lastSlash + 1);
+ }
+ return "arn:aws:kms:***:***:key/***";
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementUtility.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementUtility.java
new file mode 100644
index 000000000..6fabec4a8
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManagementUtility.java
@@ -0,0 +1,476 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.key;
+
+import software.amazon.jdbc.plugin.encryption.metadata.MetadataException;
+import software.amazon.jdbc.plugin.encryption.metadata.MetadataManager;
+import software.amazon.jdbc.plugin.encryption.model.ColumnEncryptionConfig;
+import software.amazon.jdbc.plugin.encryption.model.EncryptionConfig;
+import software.amazon.jdbc.plugin.encryption.model.KeyMetadata;
+import java.util.logging.Logger;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.awssdk.services.kms.model.*;
+
+import javax.sql.DataSource;
+import java.sql.*;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Utility class providing administrative functions for key management operations.
+ * This class offers high-level methods for creating master keys, setting up encryption
+ * for tables/columns, rotating keys, and managing the encryption lifecycle.
+ */
+public class KeyManagementUtility {
+
+ private static final Logger LOGGER = Logger.getLogger(KeyManagementUtility.class.getName());
+
+ private final KeyManager keyManager;
+ private final MetadataManager metadataManager;
+ private final DataSource dataSource;
+ private final KmsClient kmsClient;
+ private final EncryptionConfig config;
+
+ public KeyManagementUtility(KeyManager keyManager, MetadataManager metadataManager,
+ DataSource dataSource, KmsClient kmsClient, EncryptionConfig config) {
+ this.keyManager = Objects.requireNonNull(keyManager, "KeyManager cannot be null");
+ this.metadataManager = Objects.requireNonNull(metadataManager, "MetadataManager cannot be null");
+ this.dataSource = Objects.requireNonNull(dataSource, "DataSource cannot be null");
+ this.kmsClient = Objects.requireNonNull(kmsClient, "KmsClient cannot be null");
+ this.config = Objects.requireNonNull(config, "EncryptionConfig cannot be null");
+ }
+
+ private String getInsertEncryptionMetadataSql() {
+ String schema = config.getEncryptionMetadataSchema();
+ return "INSERT INTO " + schema + ".encryption_metadata (table_name, column_name, encryption_algorithm, key_id, created_at, updated_at) " +
+ "VALUES (?, ?, ?, ?, ?, ?) " +
+ "ON CONFLICT (table_name, column_name) DO UPDATE SET " +
+ "encryption_algorithm = EXCLUDED.encryption_algorithm, " +
+ "key_id = EXCLUDED.key_id, " +
+ "updated_at = EXCLUDED.updated_at";
+ }
+
+ private String getUpdateEncryptionMetadataKeySql() {
+ return "UPDATE " + config.getEncryptionMetadataSchema() + ".encryption_metadata SET key_id = ?, updated_at = ? " +
+ "WHERE table_name = ? AND column_name = ?";
+ }
+
+ private String getSelectColumnsWithKeySql() {
+ return "SELECT table_name, column_name FROM " + config.getEncryptionMetadataSchema() + ".encryption_metadata WHERE key_id = ?";
+ }
+
+ private String getDeleteEncryptionMetadataSql() {
+ return "DELETE FROM " + config.getEncryptionMetadataSchema() + ".encryption_metadata WHERE table_name = ? AND column_name = ?";
+ }
+
+ /**
+ * Creates a new KMS master key with proper permissions for encryption operations.
+ *
+ * @param description Description for the master key
+ * @param keyPolicy Optional key policy JSON string. If null, uses default policy
+ * @return The ARN of the created master key
+ * @throws KeyManagementException if key creation fails
+ */
+ public String createMasterKeyWithPermissions(String description, String keyPolicy)
+ throws KeyManagementException {
+ Objects.requireNonNull(description, "Description cannot be null");
+
+ LOGGER.info(()->String.format("Creating KMS master key with permissions: %s", description));
+
+ try {
+ CreateKeyRequest.Builder requestBuilder = CreateKeyRequest.builder()
+ .description(description)
+ .keyUsage(KeyUsageType.ENCRYPT_DECRYPT)
+ .keySpec(KeySpec.SYMMETRIC_DEFAULT);
+
+ // Add key policy if provided
+ if (keyPolicy != null && !keyPolicy.trim().isEmpty()) {
+ requestBuilder.policy(keyPolicy);
+ LOGGER.finest(()->"Using custom key policy for master key creation");
+ }
+
+ CreateKeyResponse response = kmsClient.createKey(requestBuilder.build());
+ String keyArn = response.keyMetadata().arn();
+
+ // Create an alias for easier management
+ String aliasName = "alias/jdbc-encryption-" + System.currentTimeMillis();
+ CreateAliasRequest aliasRequest = CreateAliasRequest.builder()
+ .aliasName(aliasName)
+ .targetKeyId(keyArn)
+ .build();
+
+ kmsClient.createAlias(aliasRequest);
+
+ LOGGER.info(()->String.format("Successfully created KMS master key: %s with alias: %s", keyArn, aliasName));
+ return keyArn;
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to create KMS master key with permissions", e.getMessage()));
+ throw new KeyManagementException("Failed to create KMS master key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Creates a master key with default permissions suitable for JDBC encryption.
+ *
+ * @param description Description for the master key
+ * @return The ARN of the created master key
+ * @throws KeyManagementException if key creation fails
+ */
+ public String createMasterKeyWithPermissions(String description) throws KeyManagementException {
+ return createMasterKeyWithPermissions(description, null);
+ }
+
+ /**
+ * Generates and stores a data key for the specified table and column.
+ * This method creates the complete encryption setup for a column.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @param masterKeyArn ARN of the master key to use
+ * @param algorithm Encryption algorithm (defaults to AES-256-GCM if null)
+ * @return The generated key ID
+ * @throws KeyManagementException if key generation or storage fails
+ */
+ public String generateAndStoreDataKey(String tableName, String columnName,
+ String masterKeyArn, String algorithm)
+ throws KeyManagementException {
+ Objects.requireNonNull(tableName, "Table name cannot be null");
+ Objects.requireNonNull(columnName, "Column name cannot be null");
+ Objects.requireNonNull(masterKeyArn, "Master key ARN cannot be null");
+
+ if (algorithm == null || algorithm.trim().isEmpty()) {
+ algorithm = "AES-256-GCM";
+ }
+
+ LOGGER.info(()->String.format("Generating and storing data key for %s.%s using master key: %s",
+ tableName, columnName, masterKeyArn));
+
+ try {
+
+ // Generate the data key using KMS
+ KeyManager.DataKeyResult dataKeyResult = keyManager.generateDataKey(masterKeyArn);
+
+ try {
+ // Generate a unique key name
+ String keyName = "key-" + tableName + "-" + columnName + "-" + System.currentTimeMillis();
+
+ // Create key metadata
+ KeyMetadata keyMetadata = KeyMetadata.builder()
+ .keyId("dummy") // Not used anymore but required by builder
+ .keyName(keyName)
+ .masterKeyArn(masterKeyArn)
+ .encryptedDataKey(dataKeyResult.getEncryptedKey())
+ .keySpec("AES_256")
+ .createdAt(Instant.now())
+ .lastUsedAt(Instant.now())
+ .build();
+
+ // Store key metadata in database and get the generated integer ID
+ int generatedKeyId = keyManager.storeKeyMetadata(tableName, columnName, keyMetadata);
+
+ // Store encryption metadata using the generated integer key ID
+ storeEncryptionMetadata(tableName, columnName, algorithm, generatedKeyId);
+
+ // Refresh metadata cache
+ metadataManager.refreshMetadata();
+
+ LOGGER.info(()->String.format("Successfully generated and stored data key for %s.%s with key ID: %s",
+ tableName, columnName, generatedKeyId));
+
+ return String.valueOf(generatedKeyId);
+
+ } finally {
+ // Clear sensitive data from memory
+ dataKeyResult.clearPlaintextKey();
+ }
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to generate and store data key for %s.%s", tableName, columnName, e.getMessage()));
+ throw new KeyManagementException("Failed to generate and store data key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Rotates the data key for an existing encrypted column.
+ * This creates a new data key while preserving the existing encryption metadata.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @param newMasterKeyArn Optional new master key ARN. If null, uses existing master key
+ * @return The new key ID
+ * @throws KeyManagementException if key rotation fails
+ */
+ public String rotateDataKey(String tableName, String columnName, String newMasterKeyArn)
+ throws KeyManagementException {
+ Objects.requireNonNull(tableName, "Table name cannot be null");
+ Objects.requireNonNull(columnName, "Column name cannot be null");
+
+ LOGGER.info(()->String.format("Rotating data key for %s.%s", tableName, columnName));
+
+ try {
+ // Get current encryption configuration
+ ColumnEncryptionConfig currentConfig = metadataManager.getColumnConfig(tableName, columnName);
+ if (currentConfig == null) {
+ throw new KeyManagementException("No encryption configuration found for " + tableName + "." + columnName);
+ }
+
+ // Use existing master key if new one not provided
+ String masterKeyArn = newMasterKeyArn != null ? newMasterKeyArn :
+ currentConfig.getKeyMetadata().getMasterKeyArn();
+
+ // Generate new data key
+ String newKeyId = keyManager.generateKeyId();
+ KeyManager.DataKeyResult dataKeyResult = keyManager.generateDataKey(masterKeyArn);
+
+ try {
+ // Create new key metadata
+ KeyMetadata newKeyMetadata = KeyMetadata.builder()
+ .keyId(newKeyId)
+ .masterKeyArn(masterKeyArn)
+ .encryptedDataKey(dataKeyResult.getEncryptedKey())
+ .keySpec("AES_256")
+ .createdAt(Instant.now())
+ .lastUsedAt(Instant.now())
+ .build();
+
+ // Store new key metadata
+ keyManager.storeKeyMetadata(tableName, columnName, newKeyMetadata);
+
+ // Update encryption metadata to use new key
+ updateEncryptionMetadataKey(tableName, columnName, newKeyId);
+
+ // Refresh metadata cache
+ metadataManager.refreshMetadata();
+
+ LOGGER.info(()->String.format("Successfully rotated data key for %s.%s from %s to %s",
+ tableName, columnName, currentConfig.getKeyId(), newKeyId));
+
+ return newKeyId;
+
+ } finally {
+ dataKeyResult.clearPlaintextKey();
+ }
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to rotate data key for %s.%s", tableName, columnName, e.getMessage()));
+ throw new KeyManagementException("Failed to rotate data key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Initializes encryption for a new table and column combination.
+ * This is a convenience method that creates everything needed for encryption.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @param masterKeyArn ARN of the master key to use
+ * @return The generated key ID
+ * @throws KeyManagementException if initialization fails
+ */
+ public String initializeEncryptionForColumn(String tableName, String columnName, String masterKeyArn)
+ throws KeyManagementException {
+ return initializeEncryptionForColumn(tableName, columnName, masterKeyArn, "AES-256-GCM");
+ }
+
+ /**
+ * Initializes encryption for a new table and column combination with specified algorithm.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @param masterKeyArn ARN of the master key to use
+ * @param algorithm Encryption algorithm to use
+ * @return The generated key ID
+ * @throws KeyManagementException if initialization fails
+ */
+ public String initializeEncryptionForColumn(String tableName, String columnName,
+ String masterKeyArn, String algorithm)
+ throws KeyManagementException {
+ LOGGER.info(()->String.format("Initializing encryption for column %s.%s", tableName, columnName));
+
+ // Check if column is already encrypted
+ try {
+ if (metadataManager.isColumnEncrypted(tableName, columnName)) {
+ throw new KeyManagementException("Column " + tableName + "." + columnName + " is already encrypted");
+ }
+ } catch (MetadataException e) {
+ throw new KeyManagementException("Failed to check existing encryption status", e);
+ }
+
+ // Generate and store the data key
+ return generateAndStoreDataKey(tableName, columnName, masterKeyArn, algorithm);
+ }
+
+ /**
+ * Removes encryption configuration for a table and column.
+ * This does not delete the actual key data for security reasons.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @throws KeyManagementException if removal fails
+ */
+ public void removeEncryptionForColumn(String tableName, String columnName)
+ throws KeyManagementException {
+ Objects.requireNonNull(tableName, "Table name cannot be null");
+ Objects.requireNonNull(columnName, "Column name cannot be null");
+
+ LOGGER.info(()->String.format("Removing encryption configuration for %s.%s", tableName, columnName));
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(getDeleteEncryptionMetadataSql())) {
+
+ stmt.setString(1, tableName);
+ stmt.setString(2, columnName);
+
+ int rowsAffected = stmt.executeUpdate();
+ if (rowsAffected == 0) {
+ LOGGER.warning(()->String.format("No encryption configuration found for %s.%s", tableName, columnName));
+ } else {
+ LOGGER.info(()->String.format("Successfully removed encryption configuration for %s.%s", tableName, columnName));
+ }
+
+ // Refresh metadata cache
+ metadataManager.refreshMetadata();
+
+ } catch (MetadataException e) {
+ LOGGER.severe(()->String.format("Failed to refresh metadata after removing encryption configuration", e));
+ throw new KeyManagementException("Failed to refresh metadata: " + e.getMessage(), e);
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Failed to remove encryption configuration for %s.%s", tableName, columnName, e));
+ throw new KeyManagementException("Failed to remove encryption configuration: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Lists all columns that use a specific key ID.
+ * Useful for understanding the impact of key operations.
+ *
+ * @param keyId The key ID to search for
+ * @return List of table.column identifiers using the key
+ * @throws KeyManagementException if query fails
+ */
+ public List getColumnsUsingKey(String keyId) throws KeyManagementException {
+ Objects.requireNonNull(keyId, "Key ID cannot be null");
+
+ LOGGER.finest(()->String.format("Finding columns using key ID: %s", keyId));
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(getSelectColumnsWithKeySql())) {
+
+ stmt.setString(1, keyId);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ List columns = new ArrayList<>();
+ while (rs.next()) {
+ String tableName = rs.getString("table_name");
+ String columnName = rs.getString("column_name");
+ columns.add(tableName + "." + columnName);
+ }
+ return columns;
+ }
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Failed to find columns using key ID: %s", keyId, e.getMessage()));
+ throw new KeyManagementException("Failed to find columns using key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Validates that a master key exists and is accessible.
+ *
+ * @param masterKeyArn ARN of the master key to validate
+ * @return true if key is valid and accessible
+ * @throws KeyManagementException if validation fails
+ */
+ public boolean validateMasterKey(String masterKeyArn) throws KeyManagementException {
+ Objects.requireNonNull(masterKeyArn, "Master key ARN cannot be null");
+
+ LOGGER.finest(()->String.format("Validating master key: %s", masterKeyArn));
+
+ try {
+ DescribeKeyRequest request = DescribeKeyRequest.builder()
+ .keyId(masterKeyArn)
+ .build();
+
+ DescribeKeyResponse response = kmsClient.describeKey(request);
+ software.amazon.awssdk.services.kms.model.KeyMetadata keyMetadata = response.keyMetadata();
+
+ boolean isValid = keyMetadata.enabled() &&
+ keyMetadata.keyState() == KeyState.ENABLED &&
+ keyMetadata.keyUsage() == KeyUsageType.ENCRYPT_DECRYPT;
+
+ LOGGER.finest(()->String.format("Master key %s validation result: %s", masterKeyArn, isValid));
+ return isValid;
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to validate master key: %s", masterKeyArn, e.getMessage()));
+ throw new KeyManagementException("Failed to validate master key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Stores encryption metadata in the database.
+ */
+ private void storeEncryptionMetadata(String tableName, String columnName,
+ String algorithm, int keyId) throws SQLException {
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(getInsertEncryptionMetadataSql())) {
+
+ Timestamp now = Timestamp.from(Instant.now());
+
+ stmt.setString(1, tableName);
+ stmt.setString(2, columnName);
+ stmt.setString(3, algorithm);
+ stmt.setInt(4, keyId);
+ stmt.setTimestamp(5, now);
+ stmt.setTimestamp(6, now);
+
+ int rowsAffected = stmt.executeUpdate();
+ if (rowsAffected == 0) {
+ throw new SQLException("Failed to store encryption metadata - no rows affected");
+ }
+
+ LOGGER.finest(()->String.format("Successfully stored encryption metadata for %s.%s", tableName, columnName));
+ }
+ }
+
+ /**
+ * Updates the key ID for existing encryption metadata.
+ */
+ private void updateEncryptionMetadataKey(String tableName, String columnName, String newKeyId)
+ throws SQLException {
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement stmt = conn.prepareStatement(getUpdateEncryptionMetadataKeySql())) {
+
+ stmt.setString(1, newKeyId);
+ stmt.setTimestamp(2, Timestamp.from(Instant.now()));
+ stmt.setString(3, tableName);
+ stmt.setString(4, columnName);
+
+ int rowsAffected = stmt.executeUpdate();
+ if (rowsAffected == 0) {
+ throw new SQLException("Failed to update encryption metadata key - no rows affected");
+ }
+
+ LOGGER.finest(()->String.format("Successfully updated encryption metadata key for %s.%s to %s",
+ tableName, columnName, newKeyId));
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManager.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManager.java
new file mode 100644
index 000000000..bd7b0cd47
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/key/KeyManager.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.key;
+
+import software.amazon.jdbc.PluginService;
+import software.amazon.jdbc.plugin.encryption.cache.DataKeyCache;
+import software.amazon.jdbc.plugin.encryption.model.EncryptionConfig;
+import software.amazon.jdbc.plugin.encryption.model.KeyMetadata;
+import java.util.logging.Logger;
+import software.amazon.awssdk.core.SdkBytes;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.awssdk.services.kms.model.*;
+
+import java.sql.*;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * Manages KMS operations and data key lifecycle for the encryption plugin.
+ * Handles key creation, data key generation/decryption, and database storage of key metadata.
+ */
+public class KeyManager {
+
+ private static final Logger LOGGER = Logger.getLogger(KeyManager.class.getName());
+
+ private final KmsClient kmsClient;
+ private final PluginService pluginService;
+ private final EncryptionConfig config;
+ private final DataKeyCache dataKeyCache;
+
+ public KeyManager(KmsClient kmsClient, PluginService pluginService, EncryptionConfig config) {
+ this.kmsClient = Objects.requireNonNull(kmsClient, "KmsClient cannot be null");
+ this.pluginService = Objects.requireNonNull(pluginService, "DataSource cannot be null");
+ this.config = Objects.requireNonNull(config, "EncryptionConfig cannot be null");
+ this.dataKeyCache = new DataKeyCache(config);
+ }
+
+ private String getInsertKeyMetadataSql() {
+ String schema = config.getEncryptionMetadataSchema();
+ return "INSERT INTO " + schema + ".key_storage (name, master_key_arn, encrypted_data_key, key_spec, created_at, last_used_at) " +
+ "VALUES (?, ?, ?, ?, ?, ?) " +
+ "RETURNING id";
+ }
+
+ private String getSelectKeyMetadataSql() {
+ return "SELECT id, name, master_key_arn, encrypted_data_key, key_spec, created_at, last_used_at " +
+ "FROM " + config.getEncryptionMetadataSchema() + ".key_storage WHERE id = ?";
+ }
+
+ private String getUpdateLastUsedSql() {
+ return "UPDATE " + config.getEncryptionMetadataSchema() + ".key_storage SET last_used_at = ? WHERE key_id = ?";
+ }
+
+ /**
+ * Creates a new KMS master key with the specified description.
+ *
+ * @param description Description for the master key
+ * @return The ARN of the created master key
+ * @throws KeyManagementException if key creation fails
+ */
+ public String createMasterKey(String description) throws KeyManagementException {
+ Objects.requireNonNull(description, "Description cannot be null");
+
+ LOGGER.info(()->String.format("Creating KMS master key with description: %s", description));
+
+ try {
+ CreateKeyRequest request = CreateKeyRequest.builder()
+ .description(description)
+ .keyUsage(KeyUsageType.ENCRYPT_DECRYPT)
+ .keySpec(KeySpec.SYMMETRIC_DEFAULT)
+ .build();
+
+ CreateKeyResponse response = executeWithRetry(() -> kmsClient.createKey(request));
+ String keyArn = response.keyMetadata().arn();
+
+ LOGGER.info(()->String.format("Successfully created KMS master key: %s", keyArn));
+ return keyArn;
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to create KMS master key", e));
+ throw new KeyManagementException("Failed to create KMS master key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generates a new data key using the specified master key.
+ *
+ * @param masterKeyArn ARN of the master key to use for data key generation
+ * @return DataKeyResult containing both plaintext and encrypted data keys
+ * @throws KeyManagementException if data key generation fails
+ */
+ public DataKeyResult generateDataKey(String masterKeyArn) throws KeyManagementException {
+ Objects.requireNonNull(masterKeyArn, "Master key ARN cannot be null");
+
+ LOGGER.finest(()->String.format("Generating data key using master key: %s", masterKeyArn));
+
+ try {
+ GenerateDataKeyRequest request = GenerateDataKeyRequest.builder()
+ .keyId(masterKeyArn)
+ .keySpec(DataKeySpec.AES_256)
+ .build();
+
+ GenerateDataKeyResponse response = executeWithRetry(() -> kmsClient.generateDataKey(request));
+
+ byte[] plaintextKey = response.plaintext().asByteArray();
+ String encryptedKey = Base64.getEncoder().encodeToString(response.ciphertextBlob().asByteArray());
+
+ LOGGER.finest(()->String.format("Successfully generated data key for master key: %s", masterKeyArn));
+ return new DataKeyResult(plaintextKey, encryptedKey);
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to generate data key for master key: %s", masterKeyArn, e));
+ throw new KeyManagementException("Failed to generate data key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Decrypts an encrypted data key using KMS with caching support.
+ *
+ * @param encryptedDataKey Base64-encoded encrypted data key
+ * @param masterKeyArn ARN of the master key used for encryption
+ * @return Decrypted data key as byte array
+ * @throws KeyManagementException if decryption fails
+ */
+ public byte[] decryptDataKey(String encryptedDataKey, String masterKeyArn) throws KeyManagementException {
+ Objects.requireNonNull(encryptedDataKey, "Encrypted data key cannot be null");
+ Objects.requireNonNull(masterKeyArn, "Master key ARN cannot be null");
+
+ // Create cache key from encrypted data key hash
+ String cacheKey = createCacheKey(encryptedDataKey);
+
+ // Try cache first if enabled
+ if (config.isDataKeyCacheEnabled()) {
+ byte[] cachedKey = dataKeyCache.get(cacheKey);
+ if (cachedKey != null) {
+ LOGGER.finest(()->"Cache hit for data key decryption");
+ return cachedKey;
+ }
+ }
+
+ LOGGER.finest(()->String.format("Decrypting data key using master key: %s", masterKeyArn));
+
+ try {
+ byte[] encryptedKeyBytes = Base64.getDecoder().decode(encryptedDataKey);
+
+ DecryptRequest request = DecryptRequest.builder()
+ .ciphertextBlob(SdkBytes.fromByteArray(encryptedKeyBytes))
+ .keyId(masterKeyArn)
+ .build();
+
+ DecryptResponse response = executeWithRetry(() -> kmsClient.decrypt(request));
+ byte[] plaintextKey = response.plaintext().asByteArray();
+
+ // Cache the decrypted key if caching is enabled
+ if (config.isDataKeyCacheEnabled()) {
+ dataKeyCache.put(cacheKey, plaintextKey);
+ }
+
+ LOGGER.finest(()->String.format("Successfully decrypted data key for master key: %s", masterKeyArn));
+ return plaintextKey;
+
+ } catch (Exception e) {
+ LOGGER.severe(()->String.format("Failed to decrypt data key for master key: %s", masterKeyArn, e));
+ throw new KeyManagementException("Failed to decrypt data key: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Stores key metadata in the database for the specified table and column.
+ *
+ * @param tableName Name of the table
+ * @param columnName Name of the column
+ * @param keyMetadata Key metadata to store
+ * @return the generated integer ID
+ * @throws KeyManagementException if storage fails
+ */
+ public int storeKeyMetadata(String tableName, String columnName, KeyMetadata keyMetadata)
+ throws KeyManagementException {
+ Objects.requireNonNull(tableName, "Table name cannot be null");
+ Objects.requireNonNull(columnName, "Column name cannot be null");
+ Objects.requireNonNull(keyMetadata, "Key metadata cannot be null");
+
+ if (!keyMetadata.isValid()) {
+ throw new KeyManagementException("Invalid key metadata provided");
+ }
+
+ LOGGER.finest(()->String.format("Storing key metadata for %s.%s", tableName, columnName));
+
+ try (Connection conn = pluginService.forceConnect(pluginService.getCurrentHostSpec(), pluginService.getProperties());
+ PreparedStatement stmt = conn.prepareStatement(getInsertKeyMetadataSql())) {
+
+ stmt.setString(1, keyMetadata.getKeyName());
+ stmt.setString(2, keyMetadata.getMasterKeyArn());
+ stmt.setString(3, keyMetadata.getEncryptedDataKey());
+ stmt.setString(4, keyMetadata.getKeySpec());
+ stmt.setTimestamp(5, Timestamp.from(keyMetadata.getCreatedAt()));
+ stmt.setTimestamp(6, Timestamp.from(keyMetadata.getLastUsedAt()));
+
+ ResultSet rs = stmt.executeQuery();
+ if (rs.next()) {
+ int generatedId = rs.getInt(1);
+ LOGGER.finest(()->String.format("Successfully stored key metadata for %s.%s with ID: %s", tableName, columnName, generatedId));
+ return generatedId;
+ } else {
+ throw new KeyManagementException("Failed to get generated key ID");
+ }
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Database error storing key metadata for %s.%s %s", tableName, columnName, e.getMessage()));
+ throw new KeyManagementException("Failed to store key metadata: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Retrieves key metadata from the database for the specified key ID.
+ *
+ * @param keyId Key ID to retrieve metadata for
+ * @return Optional containing key metadata if found
+ * @throws KeyManagementException if retrieval fails
+ */
+ public Optional getKeyMetadata(String keyId) throws KeyManagementException {
+ Objects.requireNonNull(keyId, "Key ID cannot be null");
+
+ LOGGER.finest(()->String.format("Retrieving key metadata for key ID: %s", keyId));
+
+ try (Connection conn = pluginService.forceConnect(pluginService.getCurrentHostSpec(), pluginService.getProperties());
+ PreparedStatement stmt = conn.prepareStatement(getSelectKeyMetadataSql())) {
+
+ stmt.setString(1, keyId);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (rs.next()) {
+ KeyMetadata metadata = KeyMetadata.builder()
+ .keyId(rs.getString("key_id"))
+ .masterKeyArn(rs.getString("master_key_arn"))
+ .encryptedDataKey(rs.getString("encrypted_data_key"))
+ .keySpec(rs.getString("key_spec"))
+ .createdAt(rs.getTimestamp("created_at").toInstant())
+ .lastUsedAt(rs.getTimestamp("last_used_at").toInstant())
+ .build();
+
+ LOGGER.finest(()->String.format("Successfully retrieved key metadata for key ID: %s", keyId));
+ return Optional.of(metadata);
+ } else {
+ LOGGER.finest(()->String.format("No key metadata found for key ID: %s", keyId));
+ return Optional.empty();
+ }
+ }
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Database error retrieving key metadata for key ID: %s", keyId, e));
+ throw new KeyManagementException("Failed to retrieve key metadata: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Updates the last used timestamp for the specified key.
+ *
+ * @param keyId Key ID to update
+ * @throws KeyManagementException if update fails
+ */
+ public void updateLastUsed(String keyId) throws KeyManagementException {
+ Objects.requireNonNull(keyId, "Key ID cannot be null");
+
+ try (Connection conn = pluginService.forceConnect(pluginService.getCurrentHostSpec(), pluginService.getProperties());
+ PreparedStatement stmt = conn.prepareStatement(getUpdateLastUsedSql())) {
+
+ stmt.setTimestamp(1, Timestamp.from(Instant.now()));
+ stmt.setString(2, keyId);
+
+ stmt.executeUpdate();
+
+ } catch (SQLException e) {
+ LOGGER.severe(()->String.format("Database error updating last used timestamp for key ID: %s %s", keyId, e.getMessage()));
+ throw new KeyManagementException("Failed to update last used timestamp: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Generates a unique key ID for new keys to store in the database.
+ *
+ * @return Unique key ID
+ */
+ public String generateKeyId() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * Returns the data key cache for metrics and management.
+ *
+ * @return Data key cache instance
+ */
+ public DataKeyCache getDataKeyCache() {
+ return dataKeyCache;
+ }
+
+ /**
+ * Clears the data key cache.
+ */
+ public void clearCache() {
+ dataKeyCache.clear();
+ LOGGER.info(()->"Data key cache cleared");
+ }
+
+ /**
+ * Shuts down the key manager and cleans up resources.
+ */
+ public void shutdown() {
+ LOGGER.info(()->"Shutting down KeyManager");
+ dataKeyCache.shutdown();
+ }
+
+ /**
+ * Executes a KMS operation with retry logic and exponential backoff.
+ */
+ private T executeWithRetry(KmsOperation operation) throws Exception {
+ Exception lastException = null;
+ int maxRetries = config.getMaxRetries();
+
+ for (int attempt = 0; attempt <= maxRetries; attempt++) {
+ try {
+ return operation.execute();
+ } catch (Exception e) {
+ lastException = e;
+
+ if (attempt == maxRetries) {
+ break;
+ }
+
+ if (isRetryableException(e)) {
+ long backoffMs = calculateBackoff(attempt);
+ int finalAttempt = attempt;
+ LOGGER.warning(()->String.format("KMS operation failed (attempt %s/%s), retrying in %sms: %s",
+ finalAttempt + 1, maxRetries + 1, backoffMs, e.getMessage()));
+
+ try {
+ Thread.sleep(backoffMs);
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ throw new KeyManagementException("Operation interrupted during retry", ie);
+ }
+ } else {
+ // Non-retryable exception, fail immediately
+ break;
+ }
+ }
+ }
+
+ throw lastException;
+ }
+
+ /**
+ * Determines if an exception is retryable.
+ */
+ private boolean isRetryableException(Exception e) {
+ if (e instanceof KmsException) {
+ KmsException kmsException = (KmsException) e;
+ // Retry on throttling, service unavailable, and internal errors
+ boolean isServerError = kmsException.statusCode() >= 500;
+ boolean isThrottling = kmsException.statusCode() == 429;
+
+ // Check error code if available
+ boolean isThrottlingError = false;
+ if (kmsException.awsErrorDetails() != null && kmsException.awsErrorDetails().errorCode() != null) {
+ isThrottlingError = "ThrottlingException".equals(kmsException.awsErrorDetails().errorCode());
+ }
+
+ return isServerError || isThrottling || isThrottlingError;
+ }
+
+ // Retry on general network/connection issues
+ return e instanceof java.net.ConnectException ||
+ e instanceof java.net.SocketTimeoutException ||
+ e instanceof java.io.IOException;
+ }
+
+ /**
+ * Calculates exponential backoff with jitter.
+ */
+ private long calculateBackoff(int attempt) {
+ long baseMs = config.getRetryBackoffBase().toMillis();
+ long exponentialBackoff = baseMs * (1L << attempt);
+
+ // Add jitter (±25% of the calculated backoff)
+ long jitter = (long) (exponentialBackoff * 0.25 * (ThreadLocalRandom.current().nextDouble() - 0.5) * 2);
+
+ return Math.max(baseMs, exponentialBackoff + jitter);
+ }
+
+ /**
+ * Creates a cache key from an encrypted data key.
+ */
+ private String createCacheKey(String encryptedDataKey) {
+ // Use a hash of the encrypted data key as cache key for security
+ return "datakey_" + Math.abs(encryptedDataKey.hashCode());
+ }
+
+ /**
+ * Functional interface for KMS operations that can be retried.
+ */
+ @FunctionalInterface
+ private interface KmsOperation {
+ T execute() throws Exception;
+ }
+
+ /**
+ * Result class for data key generation operations.
+ */
+ public static class DataKeyResult {
+ private final byte[] plaintextKey;
+ private final String encryptedKey;
+
+ public DataKeyResult(byte[] plaintextKey, String encryptedKey) {
+ this.plaintextKey = Objects.requireNonNull(plaintextKey, "Plaintext key cannot be null");
+ this.encryptedKey = Objects.requireNonNull(encryptedKey, "Encrypted key cannot be null");
+ }
+
+ public byte[] getPlaintextKey() {
+ return plaintextKey.clone(); // Return copy for security
+ }
+
+ public String getEncryptedKey() {
+ return encryptedKey;
+ }
+
+ /**
+ * Clears the plaintext key from memory for security.
+ */
+ public void clearPlaintextKey() {
+ if (plaintextKey != null) {
+ java.util.Arrays.fill(plaintextKey, (byte) 0);
+ }
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/logging/AuditLogger.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/logging/AuditLogger.java
new file mode 100644
index 000000000..21f50aa37
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/encryption/logging/AuditLogger.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * 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 software.amazon.jdbc.plugin.encryption.logging;
+
+import java.util.logging.Logger;
+import org.slf4j.MDC;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Audit LOGGER for KMS operations and encryption activities.
+ * Provides structured logging without exposing sensitive data.
+ */
+public class AuditLogger {
+
+ private static final Logger auditLogger = Logger.getLogger(AuditLogger.class.getName());
+
+ // Thread-local context for audit information
+ private static final ThreadLocal