From fadb0c656364be8c2bbd026991db9f0fdc87fbed Mon Sep 17 00:00:00 2001 From: Mouhsin Elmajdouby Date: Mon, 16 Dec 2024 16:25:26 +0100 Subject: [PATCH] Implement Wallet Expiration Parsing and Resource Caching Enhancement (#130) * Implement Wallet Expiration Parsing and Resource Caching Enhancement * Remove unused imports * Remove unused imports * test parse Expirating Date * test parse Expirating Date * test parse Expirating Date * test parse Expirating Date * test parse Expirating Date * Fix Date Parsing for Wallet Expiration Date and Handle Time Zone Consistency * Address comment review * Optimize Wallet expiration date handling --- .../oracle/jdbc/provider/util/Wallet.java | 124 +++++++++++++++++- .../provider/oci/database/WalletFactory.java | 13 +- 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/ojdbc-provider-common/src/main/java/oracle/jdbc/provider/util/Wallet.java b/ojdbc-provider-common/src/main/java/oracle/jdbc/provider/util/Wallet.java index 6c0ba3d7..4744994c 100644 --- a/ojdbc-provider-common/src/main/java/oracle/jdbc/provider/util/Wallet.java +++ b/ojdbc-provider-common/src/main/java/oracle/jdbc/provider/util/Wallet.java @@ -39,11 +39,24 @@ package oracle.jdbc.provider.util; import javax.net.ssl.SSLContext; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.security.KeyStore; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import static java.nio.charset.StandardCharsets.UTF_8; + /** *

* Represents a wallet zip generated by the ADB service. The contents of the zip @@ -59,6 +72,11 @@ * JDBC requires a dependency on the Oracle PKI security provider. The JKS file * is chosen over the SSO or PKCS12 files in order to avoid this dependency. *

+ *

+ * It also extracts the expiration date of the wallet's certificates from + * the {@code README} file included in the wallet zip. The expiration date is + * used to determine when the wallet should be refreshed. + *

*/ public final class Wallet { @@ -74,18 +92,42 @@ public final class Wallet { /** Type of the keystore file in a wallet zip */ private static final String KEY_STORE_TYPE = "JKS"; + /** Name of the README file in a wallet zip */ + private static final String README_FILE = "README"; + + /** Pattern to match the expiration date line in the README file */ + private static final Pattern EXPIRY_PATTERN = Pattern.compile( + "The SSL certificates provided in this wallet will expire on ([\\d\\-\\.: ]+ UTC)\\."); + /** Contents of the tnsnames.ora file */ private final TNSNames tnsNames; + /** Expiration date of the wallet's certificates, extracted from the + * README file */ + private final OffsetDateTime expirationDate; + /** * An {@code SSLContext} initialized with key and trust material of the * keystore.jks and truststore.jks files. */ private final SSLContext sslContext; - private Wallet(TNSNames tnsNames, SSLContext sslContext) { + /** + * A static DateTimeFormatter used to parse expiration dates in the README. + */ + private static final DateTimeFormatter EXPIRATION_DATE_FORMATTER = + new DateTimeFormatterBuilder() + .appendPattern("yyyy-MM-dd HH:mm:ss") + .optionalStart() + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .optionalEnd() + .appendPattern("X") // Accept 'Z' as offset + .toFormatter(Locale.ENGLISH); + + private Wallet(TNSNames tnsNames, SSLContext sslContext, OffsetDateTime expirationDate) { this.tnsNames = tnsNames; this.sslContext = sslContext; + this.expirationDate = expirationDate; } /** @@ -96,6 +138,13 @@ public SSLContext getSSLContext() { return sslContext; } + /** + * @return The expiration date of the wallet's certificates, or null if not available. + */ + public OffsetDateTime getExpirationDate() { + return expirationDate; + } + /** * @return The connection string for high consumer group. Not null. * @throws IllegalStateException If no connection string is defined for the @@ -162,6 +211,9 @@ private String getConnectionString(TNSNames.ConsumerGroup consumerGroup) { /** * Unzips the stream of a wallet directory, returning a {@code Wallet} that * retains the contents of any files relevant to Oracle JDBC. + * @param zipStream The input stream of the wallet zip file. + * @param password The password used to decrypt the keystore. + * @return A new {@code Wallet} instance containing the parsed wallet data. * @throws IllegalStateException If the files are not found or can not be * decoded. */ @@ -170,6 +222,7 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) { TNSNames tnsNames = null; KeyStore keyStore = null; KeyStore trustStore = null; + OffsetDateTime expirationDate = null; try { for (ZipEntry entry = zipStream.getNextEntry(); entry != null; entry = zipStream.getNextEntry()) { @@ -185,6 +238,9 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) { trustStore = TlsUtils.loadKeyStore(zipStream, null, KEY_STORE_TYPE, null); break; + case README_FILE: + expirationDate = parseExpirationDateFromReadme(zipStream); + break; default: // Ignore other files } @@ -205,7 +261,8 @@ public static Wallet unzip(ZipInputStream zipStream, char[] password) { return new Wallet( tnsNames, - TlsUtils.createSSLContext(keyStore, trustStore, password)); + TlsUtils.createSSLContext(keyStore, trustStore, password), + expirationDate); } /** Returns an exception for a missing file in the wallet ZIP */ @@ -213,4 +270,67 @@ private static IllegalStateException missingFile(String fileName) { return new IllegalStateException("Wallet ZIP did not contain: " + fileName); } + /** + * Reads an {@code InputStream} line by line to find and extract the expiration + * date of the wallet's certificates using a predefined pattern. + * Stops reading as soon as the expiration date is found. + * + * @param inputStream The input stream to read from. Not null. + * @return The extracted expiration date string, or {@code null} + * if no match is found. + * @throws IOException If an I/O error occurs while reading the stream. + */ + private static String findExpirationDateInStream(InputStream inputStream) + throws IOException { + BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = EXPIRY_PATTERN.matcher(line); + if (matcher.find()) { + return matcher.group(1).trim().replace(" UTC", "Z"); + } + } + return null; + } + + /** + * Parses the expiration date of the wallet's certificates from the + * content of a {@code README} file provided as an {@code InputStream}. + * The method stops reading the stream as soon as the expiration date + * is found and attempts to parse it into an {@code OffsetDateTime}. + * + * @param inputStream The input stream of the {@code README} file. + * @return The parsed expiration date as an {@code OffsetDateTime}, + * or {@code null} if no expiration date is found or if parsing fails. + * @throws IOException If an I/O error occurs while reading the stream. + */ + public static OffsetDateTime parseExpirationDateFromReadme(InputStream inputStream) + throws IOException { + if (inputStream == null) { + return null; + } + String expiryDateString = findExpirationDateInStream(inputStream); + if (expiryDateString == null) { + return null; + } + return parseOffsetDateTime(expiryDateString); + } + + /** + * Parses a date-time string into an OffsetDateTime. + * The method uses the predefined {@code EXPIRATION_DATE_FORMATTER} to parse + * the given string. If parsing fails due to an invalid format or other reasons, + * the method returns {@code null}. + * + * @param dateTimeString The date-time string to parse. + * @return The parsed OffsetDateTime, or null if parsing fails. + */ + private static OffsetDateTime parseOffsetDateTime(String dateTimeString) { + try { + return OffsetDateTime.parse(dateTimeString, EXPIRATION_DATE_FORMATTER); + } catch (DateTimeParseException e) { + return null; + } + } } diff --git a/ojdbc-provider-oci/src/main/java/oracle/jdbc/provider/oci/database/WalletFactory.java b/ojdbc-provider-oci/src/main/java/oracle/jdbc/provider/oci/database/WalletFactory.java index e52be064..837f3b44 100644 --- a/ojdbc-provider-oci/src/main/java/oracle/jdbc/provider/oci/database/WalletFactory.java +++ b/ojdbc-provider-oci/src/main/java/oracle/jdbc/provider/oci/database/WalletFactory.java @@ -53,6 +53,7 @@ import java.io.IOException; import java.io.InputStream; +import java.time.OffsetDateTime; import java.util.Arrays; import java.util.zip.ZipInputStream; @@ -75,8 +76,6 @@ private WalletFactory() { } /** * A cache of wallet configurations requested from the OCI database service. - * TODO: The cache should invalidate entries after TLS certificates have - * expired. */ private static final ResourceFactory INSTANCE = CachedResourceFactory.create(new WalletFactory()); @@ -132,9 +131,13 @@ protected Resource request( "Failed to close ZIP stream", ioException); } - // TODO: It may be possible to parse an expiration time from the wallet - // README, and return an expiring resource here. - return Resource.createPermanentResource(wallet, false); + OffsetDateTime expiry = wallet.getExpirationDate(); + if (expiry == null) { + // If expiry could not be determined, treat as permanent + return Resource.createPermanentResource(wallet, false); + } else { + return Resource.createExpiringResource(wallet, expiry, false); + } } finally { Arrays.fill(password, (char)0);