|
| 1 | +/* |
| 2 | + * Copyright (c) 2024 Airbyte, Inc., all rights reserved. |
| 3 | + */ |
| 4 | + |
| 5 | +package io.airbyte.cdk.jdbc |
| 6 | + |
| 7 | +import io.github.oshai.kotlinlogging.KotlinLogging |
| 8 | +import java.io.BufferedInputStream |
| 9 | +import java.io.ByteArrayInputStream |
| 10 | +import java.io.FileReader |
| 11 | +import java.io.IOException |
| 12 | +import java.net.URI |
| 13 | +import java.nio.charset.StandardCharsets |
| 14 | +import java.nio.file.FileSystem |
| 15 | +import java.nio.file.FileSystems |
| 16 | +import java.nio.file.Files |
| 17 | +import java.nio.file.Path |
| 18 | +import java.security.KeyFactory |
| 19 | +import java.security.KeyStore |
| 20 | +import java.security.KeyStoreException |
| 21 | +import java.security.NoSuchAlgorithmException |
| 22 | +import java.security.PrivateKey |
| 23 | +import java.security.SecureRandom |
| 24 | +import java.security.Security |
| 25 | +import java.security.cert.Certificate |
| 26 | +import java.security.cert.CertificateException |
| 27 | +import java.security.cert.CertificateFactory |
| 28 | +import java.security.spec.InvalidKeySpecException |
| 29 | +import java.security.spec.PKCS8EncodedKeySpec |
| 30 | +import java.util.* |
| 31 | +import javax.net.ssl.SSLContext |
| 32 | +import kotlin.text.Charsets.UTF_8 |
| 33 | +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo |
| 34 | +import org.bouncycastle.jce.provider.BouncyCastleProvider |
| 35 | +import org.bouncycastle.openssl.PEMEncryptedKeyPair |
| 36 | +import org.bouncycastle.openssl.PEMKeyPair |
| 37 | +import org.bouncycastle.openssl.PEMParser |
| 38 | +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter |
| 39 | +import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder |
| 40 | + |
| 41 | +private val log = KotlinLogging.logger {} |
| 42 | + |
| 43 | +/** |
| 44 | + * General SSL utilities used for certificate and keystore operations related to secured db |
| 45 | + * connections. |
| 46 | + */ |
| 47 | +object SSLCertificateUtils { |
| 48 | + |
| 49 | + private const val PKCS_12 = "PKCS12" |
| 50 | + private const val X509 = "X.509" |
| 51 | + private val RANDOM: Random = SecureRandom() |
| 52 | + |
| 53 | + // #17000: postgres driver is hardcoded to only load an entry alias "user" |
| 54 | + const val KEYSTORE_ENTRY_PREFIX: String = "user" |
| 55 | + const val KEYSTORE_FILE_NAME: String = KEYSTORE_ENTRY_PREFIX + "keystore_" |
| 56 | + const val KEYSTORE_FILE_TYPE: String = ".p12" |
| 57 | + |
| 58 | + private fun saveKeyStoreToFile( |
| 59 | + keyStore: KeyStore, |
| 60 | + keyStorePassword: String, |
| 61 | + filesystem: FileSystem, |
| 62 | + directory: String |
| 63 | + ): URI { |
| 64 | + val pathToStore: Path = filesystem.getPath(directory) |
| 65 | + val pathToFile = |
| 66 | + pathToStore.resolve(KEYSTORE_FILE_NAME + RANDOM.nextInt() + KEYSTORE_FILE_TYPE) |
| 67 | + val os = Files.newOutputStream(pathToFile) |
| 68 | + keyStore.store(os, keyStorePassword.toCharArray()) |
| 69 | + return pathToFile.toUri() |
| 70 | + } |
| 71 | + |
| 72 | + private fun fromPEMString(certString: String): Certificate { |
| 73 | + val cf = CertificateFactory.getInstance(X509) |
| 74 | + val byteArrayInputStream = |
| 75 | + ByteArrayInputStream(certString.toByteArray(StandardCharsets.UTF_8)) |
| 76 | + val bufferedInputStream = BufferedInputStream(byteArrayInputStream) |
| 77 | + return cf.generateCertificate(bufferedInputStream) |
| 78 | + } |
| 79 | + |
| 80 | + fun keyStoreFromCertificate( |
| 81 | + cert: Certificate, |
| 82 | + keyStorePassword: String, |
| 83 | + filesystem: FileSystem, |
| 84 | + directory: String |
| 85 | + ): URI { |
| 86 | + val keyStore = KeyStore.getInstance(PKCS_12) |
| 87 | + keyStore.load(null) |
| 88 | + keyStore.setCertificateEntry(KEYSTORE_ENTRY_PREFIX + "1", cert) |
| 89 | + return saveKeyStoreToFile(keyStore, keyStorePassword, filesystem, directory) |
| 90 | + } |
| 91 | + |
| 92 | + fun keyStoreFromCertificate( |
| 93 | + certString: String, |
| 94 | + keyStorePassword: String, |
| 95 | + filesystem: FileSystem, |
| 96 | + directory: String |
| 97 | + ): URI { |
| 98 | + return keyStoreFromCertificate( |
| 99 | + fromPEMString(certString), |
| 100 | + keyStorePassword, |
| 101 | + filesystem, |
| 102 | + directory, |
| 103 | + ) |
| 104 | + } |
| 105 | + |
| 106 | + fun keyStoreFromCertificate(certString: String, keyStorePassword: String): URI { |
| 107 | + return keyStoreFromCertificate( |
| 108 | + fromPEMString(certString), |
| 109 | + keyStorePassword, |
| 110 | + FileSystems.getDefault(), |
| 111 | + "" |
| 112 | + ) |
| 113 | + } |
| 114 | + |
| 115 | + fun keyStoreFromCertificate( |
| 116 | + certString: String, |
| 117 | + keyStorePassword: String, |
| 118 | + directory: String |
| 119 | + ): URI { |
| 120 | + return keyStoreFromCertificate( |
| 121 | + certString, |
| 122 | + keyStorePassword, |
| 123 | + FileSystems.getDefault(), |
| 124 | + directory, |
| 125 | + ) |
| 126 | + } |
| 127 | + |
| 128 | + fun keyStoreFromClientCertificate( |
| 129 | + cert: Certificate, |
| 130 | + key: PrivateKey, |
| 131 | + keyStorePassword: String, |
| 132 | + filesystem: FileSystem, |
| 133 | + directory: String |
| 134 | + ): URI { |
| 135 | + val keyStore = KeyStore.getInstance(PKCS_12) |
| 136 | + keyStore.load(null) |
| 137 | + keyStore.setKeyEntry( |
| 138 | + KEYSTORE_ENTRY_PREFIX, |
| 139 | + key, |
| 140 | + keyStorePassword.toCharArray(), |
| 141 | + arrayOf(cert), |
| 142 | + ) |
| 143 | + return saveKeyStoreToFile(keyStore, keyStorePassword, filesystem, directory) |
| 144 | + } |
| 145 | + |
| 146 | + // Utility function to detect the key algorithm (RSA, DSA, EC) from the key bytes |
| 147 | + fun detectKeyAlgorithm(keyBytes: ByteArray): KeyFactory { |
| 148 | + return when { |
| 149 | + isRsaKey(keyBytes) -> KeyFactory.getInstance("RSA", "BC") |
| 150 | + isDsaKey(keyBytes) -> KeyFactory.getInstance("DSA", "BC") |
| 151 | + isEcKey(keyBytes) -> KeyFactory.getInstance("EC", "BC") |
| 152 | + else -> throw IllegalArgumentException("Unknown or unsupported key type") |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + // Example heuristics for detecting the key type (you can adjust as needed) |
| 157 | + fun isRsaKey(keyBytes: ByteArray): Boolean { |
| 158 | + return keyBytes.size > 100 && keyBytes[0].toInt() == 0x30 // ASN.1 structure for RSA keys |
| 159 | + } |
| 160 | + |
| 161 | + fun isDsaKey(keyBytes: ByteArray): Boolean { |
| 162 | + return keyBytes.size > 50 && |
| 163 | + keyBytes[0].toInt() == 0x30 // Adjust based on DSA key specifics |
| 164 | + } |
| 165 | + |
| 166 | + fun isEcKey(keyBytes: ByteArray): Boolean { |
| 167 | + return keyBytes.size > 50 && keyBytes[0].toInt() == 0x30 // ASN.1 structure for EC keys |
| 168 | + } |
| 169 | + |
| 170 | + @JvmStatic |
| 171 | + fun convertPKCS1ToPKCS8(pkcs1KeyPath: Path, pkcs8KeyPath: Path, keyStorePassword: String?) { |
| 172 | + Security.addProvider(BouncyCastleProvider()) |
| 173 | + FileReader(pkcs1KeyPath.toFile(), UTF_8).use { reader -> |
| 174 | + val pemParser = PEMParser(reader) |
| 175 | + val pemObject = pemParser.readObject() |
| 176 | + // Convert PEM to a PrivateKey (JcaPEMKeyConverter handles different types like RSA, |
| 177 | + // DSA, EC) |
| 178 | + val converter = JcaPEMKeyConverter().setProvider("BC") |
| 179 | + val privateKey = |
| 180 | + when (pemObject) { |
| 181 | + is PEMEncryptedKeyPair -> { |
| 182 | + // Handle encrypted key (if it was encrypted with a password) |
| 183 | + val decryptorProvider = |
| 184 | + JcePEMDecryptorProviderBuilder().build(keyStorePassword?.toCharArray()) |
| 185 | + val keyPair = pemObject.decryptKeyPair(decryptorProvider) |
| 186 | + converter.getPrivateKey(keyPair.privateKeyInfo) |
| 187 | + } |
| 188 | + is PEMKeyPair -> { |
| 189 | + // Handle non-encrypted key |
| 190 | + converter.getPrivateKey(pemObject.privateKeyInfo) |
| 191 | + } |
| 192 | + else -> throw IllegalArgumentException("Unsupported key format") |
| 193 | + } |
| 194 | + |
| 195 | + // Convert the private key to PKCS#8 format |
| 196 | + val pkcs8EncodedKey = convertToPkcs8(privateKey) |
| 197 | + |
| 198 | + // Write the PKCS#8 encoded key in DER format to the output path |
| 199 | + Files.write(pkcs8KeyPath, pkcs8EncodedKey) |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | + fun convertToPkcs8(privateKey: PrivateKey): ByteArray { |
| 204 | + // Convert the private key to PKCS#8 format using PrivateKeyInfo |
| 205 | + val privateKeyInfo = PrivateKeyInfo.getInstance(privateKey.encoded) |
| 206 | + return privateKeyInfo.encoded |
| 207 | + } |
| 208 | + |
| 209 | + @Throws( |
| 210 | + IOException::class, |
| 211 | + InterruptedException::class, |
| 212 | + NoSuchAlgorithmException::class, |
| 213 | + InvalidKeySpecException::class, |
| 214 | + CertificateException::class, |
| 215 | + KeyStoreException::class, |
| 216 | + ) |
| 217 | + fun keyStoreFromClientCertificate( |
| 218 | + certString: String, |
| 219 | + keyString: String, |
| 220 | + keyStorePassword: String, |
| 221 | + filesystem: FileSystem, |
| 222 | + directory: String |
| 223 | + ): URI { |
| 224 | + // Convert RSA key (PKCS#1) to PKCS#8 key |
| 225 | + // Note: java.security doesn't have a built-in support of PKCS#1 format. Hence we need a |
| 226 | + // conversion using BouncyCastle. |
| 227 | + |
| 228 | + val tmpDir = Files.createTempDirectory(null) |
| 229 | + val pkcs1Key = Files.createTempFile(tmpDir, null, null) |
| 230 | + val pkcs8Key = Files.createTempFile(tmpDir, null, null) |
| 231 | + pkcs1Key.toFile().deleteOnExit() |
| 232 | + pkcs8Key.toFile().deleteOnExit() |
| 233 | + |
| 234 | + Files.write(pkcs1Key, keyString.toByteArray(StandardCharsets.UTF_8)) |
| 235 | + convertPKCS1ToPKCS8(pkcs1Key.toAbsolutePath(), pkcs8Key.toAbsolutePath(), keyStorePassword) |
| 236 | + val spec = PKCS8EncodedKeySpec(Files.readAllBytes(pkcs8Key)) |
| 237 | + var privateKey = |
| 238 | + try { |
| 239 | + KeyFactory.getInstance("RSA").generatePrivate(spec) |
| 240 | + } catch (ex1: InvalidKeySpecException) { |
| 241 | + try { |
| 242 | + KeyFactory.getInstance("DSA").generatePrivate(spec) |
| 243 | + } catch (ex2: InvalidKeySpecException) { |
| 244 | + KeyFactory.getInstance("EC").generatePrivate(spec) |
| 245 | + } |
| 246 | + } |
| 247 | + |
| 248 | + return keyStoreFromClientCertificate( |
| 249 | + fromPEMString(certString), |
| 250 | + privateKey, |
| 251 | + keyStorePassword, |
| 252 | + filesystem, |
| 253 | + directory, |
| 254 | + ) |
| 255 | + } |
| 256 | + |
| 257 | + fun keyStoreFromClientCertificate( |
| 258 | + certString: String, |
| 259 | + keyString: String, |
| 260 | + keyStorePassword: String, |
| 261 | + directory: String |
| 262 | + ): URI { |
| 263 | + return keyStoreFromClientCertificate( |
| 264 | + certString, |
| 265 | + keyString, |
| 266 | + keyStorePassword, |
| 267 | + FileSystems.getDefault(), |
| 268 | + directory, |
| 269 | + ) |
| 270 | + } |
| 271 | + |
| 272 | + fun createContextFromCaCert(caCertificate: String): SSLContext { |
| 273 | + try { |
| 274 | + val factory = CertificateFactory.getInstance(X509) |
| 275 | + val trustedCa = |
| 276 | + factory.generateCertificate( |
| 277 | + ByteArrayInputStream(caCertificate.toByteArray(StandardCharsets.UTF_8)), |
| 278 | + ) |
| 279 | + val trustStore = KeyStore.getInstance(PKCS_12) |
| 280 | + trustStore.load(null, null) |
| 281 | + trustStore.setCertificateEntry("ca", trustedCa) |
| 282 | + val sslContextBuilder = |
| 283 | + org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(trustStore, null) |
| 284 | + return sslContextBuilder.build() |
| 285 | + } catch (e: Exception) { |
| 286 | + throw RuntimeException(e) |
| 287 | + } |
| 288 | + } |
| 289 | +} |
0 commit comments