Commit 593f63818eab3461427882fe262081ba63ed6768
Committed by
GitHub
1 parent
1d2918f0
Added support for RSA encrypted keys in PEM client credentials (#4230)
* Added support for RSA encrypted keys in PEM client credentials * Refactoring * Refactoring
Showing
1 changed file
with
148 additions
and
19 deletions
@@ -27,21 +27,28 @@ import org.bouncycastle.openssl.PEMEncryptedKeyPair; | @@ -27,21 +27,28 @@ import org.bouncycastle.openssl.PEMEncryptedKeyPair; | ||
27 | import org.bouncycastle.openssl.PEMKeyPair; | 27 | import org.bouncycastle.openssl.PEMKeyPair; |
28 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; | 28 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
29 | import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; | 29 | import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; |
30 | +import org.bouncycastle.util.encoders.Hex; | ||
30 | import org.springframework.util.StringUtils; | 31 | import org.springframework.util.StringUtils; |
31 | 32 | ||
32 | import javax.crypto.Cipher; | 33 | import javax.crypto.Cipher; |
33 | import javax.crypto.EncryptedPrivateKeyInfo; | 34 | import javax.crypto.EncryptedPrivateKeyInfo; |
35 | +import javax.crypto.SecretKey; | ||
34 | import javax.crypto.SecretKeyFactory; | 36 | import javax.crypto.SecretKeyFactory; |
37 | +import javax.crypto.spec.IvParameterSpec; | ||
35 | import javax.crypto.spec.PBEKeySpec; | 38 | import javax.crypto.spec.PBEKeySpec; |
39 | +import javax.crypto.spec.SecretKeySpec; | ||
36 | import javax.net.ssl.KeyManagerFactory; | 40 | import javax.net.ssl.KeyManagerFactory; |
37 | import javax.net.ssl.TrustManagerFactory; | 41 | import javax.net.ssl.TrustManagerFactory; |
38 | import java.io.ByteArrayInputStream; | 42 | import java.io.ByteArrayInputStream; |
39 | import java.io.InputStream; | 43 | import java.io.InputStream; |
44 | +import java.math.BigInteger; | ||
45 | +import java.nio.ByteBuffer; | ||
40 | import java.security.AlgorithmParameters; | 46 | import java.security.AlgorithmParameters; |
41 | import java.security.Key; | 47 | import java.security.Key; |
42 | import java.security.KeyFactory; | 48 | import java.security.KeyFactory; |
43 | import java.security.KeyPair; | 49 | import java.security.KeyPair; |
44 | import java.security.KeyStore; | 50 | import java.security.KeyStore; |
51 | +import java.security.MessageDigest; | ||
45 | import java.security.PrivateKey; | 52 | import java.security.PrivateKey; |
46 | import java.security.Security; | 53 | import java.security.Security; |
47 | import java.security.cert.Certificate; | 54 | import java.security.cert.Certificate; |
@@ -49,6 +56,9 @@ import java.security.cert.CertificateFactory; | @@ -49,6 +56,9 @@ import java.security.cert.CertificateFactory; | ||
49 | import java.security.cert.X509Certificate; | 56 | import java.security.cert.X509Certificate; |
50 | import java.security.spec.KeySpec; | 57 | import java.security.spec.KeySpec; |
51 | import java.security.spec.PKCS8EncodedKeySpec; | 58 | import java.security.spec.PKCS8EncodedKeySpec; |
59 | +import java.security.spec.RSAPrivateCrtKeySpec; | ||
60 | +import java.util.regex.Matcher; | ||
61 | +import java.util.regex.Pattern; | ||
52 | 62 | ||
53 | @Data | 63 | @Data |
54 | @Slf4j | 64 | @Slf4j |
@@ -61,6 +71,15 @@ public class CertPemCredentials implements ClientCredentials { | @@ -61,6 +71,15 @@ public class CertPemCredentials implements ClientCredentials { | ||
61 | private String privateKey; | 71 | private String privateKey; |
62 | private String password; | 72 | private String password; |
63 | 73 | ||
74 | + static final String OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX = "\\s*" | ||
75 | + + "-----BEGIN RSA PRIVATE KEY-----" + "\\s*" | ||
76 | + + "Proc-Type: 4,ENCRYPTED" + "\\s*" | ||
77 | + + "DEK-Info:" + "\\s*([^\\s]+)" + "\\s+" | ||
78 | + + "([\\s\\S]*)" | ||
79 | + + "-----END RSA PRIVATE KEY-----" + "\\s*"; | ||
80 | + | ||
81 | + static final Pattern OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN = Pattern.compile(OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX); | ||
82 | + | ||
64 | @Override | 83 | @Override |
65 | public CredentialsType getType() { | 84 | public CredentialsType getType() { |
66 | return CredentialsType.CERT_PEM; | 85 | return CredentialsType.CERT_PEM; |
@@ -103,7 +122,7 @@ public class CertPemCredentials implements ClientCredentials { | @@ -103,7 +122,7 @@ public class CertPemCredentials implements ClientCredentials { | ||
103 | KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject); | 122 | KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject); |
104 | privateKey = key.getPrivate(); | 123 | privateKey = key.getPrivate(); |
105 | } else if (keyObject instanceof PrivateKey) { | 124 | } else if (keyObject instanceof PrivateKey) { |
106 | - privateKey = (PrivateKey)keyObject; | 125 | + privateKey = (PrivateKey) keyObject; |
107 | } else { | 126 | } else { |
108 | throw new RuntimeException("Unable to get private key from object: " + keyObject.getClass()); | 127 | throw new RuntimeException("Unable to get private key from object: " + keyObject.getClass()); |
109 | } | 128 | } |
@@ -152,34 +171,144 @@ public class CertPemCredentials implements ClientCredentials { | @@ -152,34 +171,144 @@ public class CertPemCredentials implements ClientCredentials { | ||
152 | private PrivateKey readPrivateKeyFile(String fileContent) throws Exception { | 171 | private PrivateKey readPrivateKeyFile(String fileContent) throws Exception { |
153 | PrivateKey privateKey = null; | 172 | PrivateKey privateKey = null; |
154 | if (fileContent != null && !fileContent.isEmpty()) { | 173 | if (fileContent != null && !fileContent.isEmpty()) { |
155 | - fileContent = fileContent.replaceAll(".*BEGIN.*PRIVATE KEY.*", "") | ||
156 | - .replaceAll(".*END.*PRIVATE KEY.*", "") | ||
157 | - .replaceAll("\\s", ""); | ||
158 | - byte[] decoded = Base64.decodeBase64(fileContent); | ||
159 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | 174 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
160 | - KeySpec keySpec = getKeySpec(decoded); | 175 | + KeySpec keySpec = getKeySpec(fileContent); |
161 | privateKey = keyFactory.generatePrivate(keySpec); | 176 | privateKey = keyFactory.generatePrivate(keySpec); |
162 | } | 177 | } |
163 | return privateKey; | 178 | return privateKey; |
164 | } | 179 | } |
165 | 180 | ||
166 | - private KeySpec getKeySpec(byte[] encodedKey) throws Exception { | ||
167 | - KeySpec keySpec; | ||
168 | - if (password == null || password.isEmpty()) { | ||
169 | - keySpec = new PKCS8EncodedKeySpec(encodedKey); | 181 | + private KeySpec getKeySpec(String encodedKey) throws Exception { |
182 | + KeySpec keySpec = null; | ||
183 | + Matcher matcher = OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN.matcher(encodedKey); | ||
184 | + if (matcher.matches()) { | ||
185 | + String encryptionDetails = matcher.group(1).trim(); | ||
186 | + String encryptedKey = matcher.group(2).replaceAll("\\s", ""); | ||
187 | + byte[] encryptedBinaryKey = java.util.Base64.getDecoder().decode(encryptedKey); | ||
188 | + String[] encryptionDetailsParts = encryptionDetails.split(","); | ||
189 | + if (encryptionDetailsParts.length == 2) { | ||
190 | + String encryptionAlgorithm = encryptionDetailsParts[0]; | ||
191 | + String encryptedAlgorithmParams = encryptionDetailsParts[1]; | ||
192 | + byte[] pw = password.getBytes(); | ||
193 | + byte[] iv = Hex.decode(encryptedAlgorithmParams); | ||
194 | + | ||
195 | + MessageDigest digest = MessageDigest.getInstance("MD5"); | ||
196 | + digest.update(pw); | ||
197 | + digest.update(iv, 0, 8); | ||
198 | + | ||
199 | + byte[] round1Digest = digest.digest(); | ||
200 | + digest.update(round1Digest); | ||
201 | + digest.update(pw); | ||
202 | + digest.update(iv, 0, 8); | ||
203 | + | ||
204 | + byte[] round2Digest = digest.digest(); | ||
205 | + Cipher cipher = null; | ||
206 | + SecretKey secretKey = null; | ||
207 | + byte[] key = null; | ||
208 | + | ||
209 | + switch(encryptionAlgorithm) { | ||
210 | + case "AES-256-CBC": | ||
211 | + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | ||
212 | + key = new byte[32]; | ||
213 | + System.arraycopy(round1Digest, 0, key, 0, 16); | ||
214 | + System.arraycopy(round2Digest, 0, key, 16, 16); | ||
215 | + secretKey = new SecretKeySpec(key, "AES"); | ||
216 | + break; | ||
217 | + case "AES-192-CBC": | ||
218 | + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | ||
219 | + key = new byte[24]; | ||
220 | + System.arraycopy(round1Digest, 0, key, 0, 16); | ||
221 | + System.arraycopy(round2Digest, 0, key, 16, 8); | ||
222 | + secretKey = new SecretKeySpec(key, "AES"); | ||
223 | + break; | ||
224 | + case "AES-128-CBC": | ||
225 | + cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); | ||
226 | + key = new byte[16]; | ||
227 | + System.arraycopy(round1Digest, 0, key, 0, 16); | ||
228 | + secretKey = new SecretKeySpec(key, "AES"); | ||
229 | + break; | ||
230 | + case "DES-EDE3-CBC": | ||
231 | + cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding"); | ||
232 | + key = new byte[24]; | ||
233 | + System.arraycopy(round1Digest, 0, key, 0, 16); | ||
234 | + System.arraycopy(round2Digest, 0, key, 16, 8); | ||
235 | + secretKey = new SecretKeySpec(key, "DESede"); | ||
236 | + break; | ||
237 | + case "DES-CBC": | ||
238 | + cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); | ||
239 | + key = new byte[8]; | ||
240 | + System.arraycopy(round1Digest, 0, key, 0, 8); | ||
241 | + secretKey = new SecretKeySpec(key, "DES"); | ||
242 | + break; | ||
243 | + } | ||
244 | + if (cipher != null) { | ||
245 | + cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); | ||
246 | + byte[] pkcs1 = cipher.doFinal(encryptedBinaryKey); | ||
247 | + keySpec = decodeRSAPrivatePKCS1(pkcs1); | ||
248 | + } else { | ||
249 | + throw new RuntimeException("Unknown Encryption algorithm!"); | ||
250 | + } | ||
251 | + } else { | ||
252 | + throw new RuntimeException("Wrong encryption details!"); | ||
253 | + } | ||
170 | } else { | 254 | } else { |
171 | - PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); | 255 | + encodedKey = encodedKey.replaceAll(".*BEGIN.*PRIVATE KEY.*", "") |
256 | + .replaceAll(".*END.*PRIVATE KEY.*", "") | ||
257 | + .replaceAll("\\s", ""); | ||
258 | + byte[] decoded = Base64.decodeBase64(encodedKey); | ||
259 | + if (password == null || password.isEmpty()) { | ||
260 | + keySpec = new PKCS8EncodedKeySpec(decoded); | ||
261 | + } else { | ||
262 | + PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); | ||
172 | 263 | ||
173 | - EncryptedPrivateKeyInfo privateKeyInfo = new EncryptedPrivateKeyInfo(encodedKey); | ||
174 | - String algorithmName = privateKeyInfo.getAlgName(); | ||
175 | - Cipher cipher = Cipher.getInstance(algorithmName); | ||
176 | - SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithmName); | 264 | + EncryptedPrivateKeyInfo privateKeyInfo = new EncryptedPrivateKeyInfo(decoded); |
265 | + String algorithmName = privateKeyInfo.getAlgName(); | ||
266 | + Cipher cipher = Cipher.getInstance(algorithmName); | ||
267 | + SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithmName); | ||
177 | 268 | ||
178 | - Key pbeKey = secretKeyFactory.generateSecret(pbeKeySpec); | ||
179 | - AlgorithmParameters algParams = privateKeyInfo.getAlgParameters(); | ||
180 | - cipher.init(Cipher.DECRYPT_MODE, pbeKey, algParams); | ||
181 | - keySpec = privateKeyInfo.getKeySpec(cipher); | 269 | + Key pbeKey = secretKeyFactory.generateSecret(pbeKeySpec); |
270 | + AlgorithmParameters algParams = privateKeyInfo.getAlgParameters(); | ||
271 | + cipher.init(Cipher.DECRYPT_MODE, pbeKey, algParams); | ||
272 | + keySpec = privateKeyInfo.getKeySpec(cipher); | ||
273 | + } | ||
182 | } | 274 | } |
183 | return keySpec; | 275 | return keySpec; |
184 | } | 276 | } |
277 | + | ||
278 | + private static BigInteger derint(ByteBuffer input) { | ||
279 | + int len = der(input, 0x02); | ||
280 | + byte[] value = new byte[len]; | ||
281 | + input.get(value); | ||
282 | + return new BigInteger(+1, value); | ||
283 | + } | ||
284 | + | ||
285 | + private static int der(ByteBuffer input, int exp) { | ||
286 | + int tag = input.get() & 0xFF; | ||
287 | + if (tag != exp) throw new IllegalArgumentException("Unexpected tag"); | ||
288 | + int n = input.get() & 0xFF; | ||
289 | + if (n < 128) return n; | ||
290 | + n &= 0x7F; | ||
291 | + if ((n < 1) || (n > 2)) throw new IllegalArgumentException("Invalid length"); | ||
292 | + int len = 0; | ||
293 | + while (n-- > 0) { | ||
294 | + len <<= 8; | ||
295 | + len |= input.get() & 0xFF; | ||
296 | + } | ||
297 | + return len; | ||
298 | + } | ||
299 | + | ||
300 | + static RSAPrivateCrtKeySpec decodeRSAPrivatePKCS1(byte[] encoded) { | ||
301 | + ByteBuffer input = ByteBuffer.wrap(encoded); | ||
302 | + if (der(input, 0x30) != input.remaining()) throw new IllegalArgumentException("Excess data"); | ||
303 | + if (!BigInteger.ZERO.equals(derint(input))) throw new IllegalArgumentException("Unsupported version"); | ||
304 | + BigInteger n = derint(input); | ||
305 | + BigInteger e = derint(input); | ||
306 | + BigInteger d = derint(input); | ||
307 | + BigInteger p = derint(input); | ||
308 | + BigInteger q = derint(input); | ||
309 | + BigInteger ep = derint(input); | ||
310 | + BigInteger eq = derint(input); | ||
311 | + BigInteger c = derint(input); | ||
312 | + return new RSAPrivateCrtKeySpec(n, e, d, p, q, ep, eq, c); | ||
313 | + } | ||
185 | } | 314 | } |