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 | 27 | import org.bouncycastle.openssl.PEMKeyPair; |
28 | 28 | import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
29 | 29 | import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; |
30 | +import org.bouncycastle.util.encoders.Hex; | |
30 | 31 | import org.springframework.util.StringUtils; |
31 | 32 | |
32 | 33 | import javax.crypto.Cipher; |
33 | 34 | import javax.crypto.EncryptedPrivateKeyInfo; |
35 | +import javax.crypto.SecretKey; | |
34 | 36 | import javax.crypto.SecretKeyFactory; |
37 | +import javax.crypto.spec.IvParameterSpec; | |
35 | 38 | import javax.crypto.spec.PBEKeySpec; |
39 | +import javax.crypto.spec.SecretKeySpec; | |
36 | 40 | import javax.net.ssl.KeyManagerFactory; |
37 | 41 | import javax.net.ssl.TrustManagerFactory; |
38 | 42 | import java.io.ByteArrayInputStream; |
39 | 43 | import java.io.InputStream; |
44 | +import java.math.BigInteger; | |
45 | +import java.nio.ByteBuffer; | |
40 | 46 | import java.security.AlgorithmParameters; |
41 | 47 | import java.security.Key; |
42 | 48 | import java.security.KeyFactory; |
43 | 49 | import java.security.KeyPair; |
44 | 50 | import java.security.KeyStore; |
51 | +import java.security.MessageDigest; | |
45 | 52 | import java.security.PrivateKey; |
46 | 53 | import java.security.Security; |
47 | 54 | import java.security.cert.Certificate; |
... | ... | @@ -49,6 +56,9 @@ import java.security.cert.CertificateFactory; |
49 | 56 | import java.security.cert.X509Certificate; |
50 | 57 | import java.security.spec.KeySpec; |
51 | 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 | 63 | @Data |
54 | 64 | @Slf4j |
... | ... | @@ -61,6 +71,15 @@ public class CertPemCredentials implements ClientCredentials { |
61 | 71 | private String privateKey; |
62 | 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 | 83 | @Override |
65 | 84 | public CredentialsType getType() { |
66 | 85 | return CredentialsType.CERT_PEM; |
... | ... | @@ -103,7 +122,7 @@ public class CertPemCredentials implements ClientCredentials { |
103 | 122 | KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject); |
104 | 123 | privateKey = key.getPrivate(); |
105 | 124 | } else if (keyObject instanceof PrivateKey) { |
106 | - privateKey = (PrivateKey)keyObject; | |
125 | + privateKey = (PrivateKey) keyObject; | |
107 | 126 | } else { |
108 | 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 | 171 | private PrivateKey readPrivateKeyFile(String fileContent) throws Exception { |
153 | 172 | PrivateKey privateKey = null; |
154 | 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 | 174 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
160 | - KeySpec keySpec = getKeySpec(decoded); | |
175 | + KeySpec keySpec = getKeySpec(fileContent); | |
161 | 176 | privateKey = keyFactory.generatePrivate(keySpec); |
162 | 177 | } |
163 | 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 | 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 | 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 | } | ... | ... |