Skip to content

Commit 6469111

Browse files
authored
[source-mysql-v2] cdk and spec change for ssl (#45351)
1 parent 962826d commit 6469111

File tree

10 files changed

+849
-37
lines changed

10 files changed

+849
-37
lines changed

airbyte-cdk/bulk/toolkits/extract-jdbc/build.gradle

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
dependencies {
22
implementation project(':airbyte-cdk:bulk:core:bulk-cdk-core-base')
33
implementation project(':airbyte-cdk:bulk:core:bulk-cdk-core-extract')
4+
api 'org.bouncycastle:bcpkix-jdk18on:1.77'
5+
api 'org.bouncycastle:bcprov-jdk18on:1.77'
6+
api 'org.bouncycastle:bctls-jdk18on:1.77'
7+
api 'org.bouncycastle:bcpkix-jdk18on:1.77'
8+
api 'org.apache.httpcomponents:httpcore:4.4'
49

510
testFixturesApi testFixtures(project(':airbyte-cdk:bulk:core:bulk-cdk-core-base'))
611
testFixturesApi testFixtures(project(':airbyte-cdk:bulk:core:bulk-cdk-core-extract'))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)