Skip to content

Commit b4bc696

Browse files
Refactored PKCS8 and PEM key parsing to reduce use of Bouncy Castle (#989)
- Replaced Bouncy Castle PKCS8 parsing with Java Security components and hierynomus ASN.1 - Added PEMKeyReader with separate implementation for historical OpenSSL password-based encryption using Bouncy Castle components - Added class-based detection of support for historical encryption for optional use of Bouncy Castle components Co-authored-by: Jeroen van Erp <[email protected]>
1 parent 27bf52e commit b4bc696

File tree

11 files changed

+725
-406
lines changed

11 files changed

+725
-406
lines changed

src/main/java/com/hierynomus/sshj/common/KeyDecryptionFailedException.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,12 @@ public class KeyDecryptionFailedException extends IOException {
2626

2727
public static final String MESSAGE = "Decryption of the key failed. A supplied passphrase may be incorrect.";
2828

29-
public KeyDecryptionFailedException() {
30-
super(MESSAGE);
29+
public KeyDecryptionFailedException(final String message) {
30+
super(message);
31+
}
32+
33+
public KeyDecryptionFailedException(final String message, final Throwable cause) {
34+
super(message, cause);
3135
}
3236

3337
public KeyDecryptionFailedException(IOException cause) {
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.schmizz.sshj.userauth.keyprovider;
17+
18+
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
19+
import net.schmizz.sshj.userauth.password.PasswordFinder;
20+
import net.schmizz.sshj.userauth.password.PasswordUtils;
21+
import net.schmizz.sshj.userauth.password.Resource;
22+
import org.bouncycastle.openssl.PEMDecryptor;
23+
import org.bouncycastle.openssl.PEMDecryptorProvider;
24+
import org.bouncycastle.openssl.PEMException;
25+
import org.bouncycastle.openssl.bc.BcPEMDecryptorProvider;
26+
import org.bouncycastle.operator.OperatorCreationException;
27+
import org.bouncycastle.util.encoders.Hex;
28+
29+
import java.io.BufferedReader;
30+
import java.io.IOException;
31+
import java.util.List;
32+
import java.util.Objects;
33+
import java.util.regex.Matcher;
34+
import java.util.regex.Pattern;
35+
36+
/**
37+
* PEM Key Reader implementation supporting historical password-based encryption from OpenSSL EVP_BytesToKey
38+
*/
39+
class EncryptedPEMKeyReader extends StandardPEMKeyReader {
40+
private static final String PROC_TYPE_ENCRYPTED_HEADER = "Proc-Type: 4,ENCRYPTED";
41+
42+
private static final Pattern DEK_INFO_PATTERN = Pattern.compile("^DEK-Info: ([A-Z0-9\\-]+),([A-F0-9]{16,32})$");
43+
44+
private static final int DEK_INFO_ALGORITHM_GROUP = 1;
45+
46+
private static final int DEK_INFO_IV_GROUP = 2;
47+
48+
private final PasswordFinder passwordFinder;
49+
50+
private final Resource<?> resource;
51+
52+
EncryptedPEMKeyReader(final PasswordFinder passwordFinder, final Resource<?> resource) {
53+
this.passwordFinder = Objects.requireNonNull(passwordFinder, "Password Finder required");
54+
this.resource = Objects.requireNonNull(resource, "Resource required");
55+
}
56+
57+
@Override
58+
public PEMKey readPemKey(final BufferedReader bufferedReader) throws IOException {
59+
final PEMKey pemKey = super.readPemKey(bufferedReader);
60+
final List<String> headers = pemKey.getHeaders();
61+
62+
final PEMKey processedPemKey;
63+
if (isEncrypted(headers)) {
64+
processedPemKey = readEncryptedPemKey(pemKey);
65+
} else {
66+
processedPemKey = pemKey;
67+
}
68+
69+
return processedPemKey;
70+
}
71+
72+
private boolean isEncrypted(final List<String> headers) {
73+
return headers.contains(PROC_TYPE_ENCRYPTED_HEADER);
74+
}
75+
76+
private PEMKey readEncryptedPemKey(final PEMKey pemKey) throws IOException {
77+
final List<String> headers = pemKey.getHeaders();
78+
final DataEncryptionKeyInfo dataEncryptionKeyInfo = getDataEncryptionKeyInfo(headers);
79+
final byte[] pemKeyBody = pemKey.getBody();
80+
81+
byte[] decryptedPemKeyBody = null;
82+
char[] password = passwordFinder.reqPassword(resource);
83+
while (password != null) {
84+
try {
85+
decryptedPemKeyBody = getDecryptedPemKeyBody(password, pemKeyBody, dataEncryptionKeyInfo);
86+
break;
87+
} catch (final KeyDecryptionFailedException e) {
88+
if (passwordFinder.shouldRetry(resource)) {
89+
password = passwordFinder.reqPassword(resource);
90+
} else {
91+
throw e;
92+
}
93+
}
94+
}
95+
96+
if (decryptedPemKeyBody == null) {
97+
throw new KeyDecryptionFailedException("PEM Key password-based decryption failed");
98+
}
99+
100+
return new PEMKey(pemKey.getPemKeyType(), headers, decryptedPemKeyBody);
101+
}
102+
103+
private byte[] getDecryptedPemKeyBody(final char[] password, final byte[] pemKeyBody, final DataEncryptionKeyInfo dataEncryptionKeyInfo) throws IOException {
104+
final String algorithm = dataEncryptionKeyInfo.algorithm;
105+
try {
106+
final PEMDecryptorProvider pemDecryptorProvider = new BcPEMDecryptorProvider(password);
107+
final PEMDecryptor pemDecryptor = pemDecryptorProvider.get(algorithm);
108+
final byte[] initializationVector = dataEncryptionKeyInfo.initializationVector;
109+
return pemDecryptor.decrypt(pemKeyBody, initializationVector);
110+
} catch (final OperatorCreationException e) {
111+
throw new IOException(String.format("PEM decryption support not found for algorithm [%s]", algorithm), e);
112+
} catch (final PEMException e) {
113+
throw new KeyDecryptionFailedException(String.format("PEM Key decryption failed for algorithm [%s]", algorithm), e);
114+
} finally {
115+
PasswordUtils.blankOut(password);
116+
}
117+
}
118+
119+
private DataEncryptionKeyInfo getDataEncryptionKeyInfo(final List<String> headers) throws IOException {
120+
DataEncryptionKeyInfo dataEncryptionKeyInfo = null;
121+
122+
for (final String header : headers) {
123+
final Matcher matcher = DEK_INFO_PATTERN.matcher(header);
124+
if (matcher.matches()) {
125+
final String algorithm = matcher.group(DEK_INFO_ALGORITHM_GROUP);
126+
final String initializationVectorGroup = matcher.group(DEK_INFO_IV_GROUP);
127+
final byte[] initializationVector = Hex.decode(initializationVectorGroup);
128+
dataEncryptionKeyInfo = new DataEncryptionKeyInfo(algorithm, initializationVector);
129+
}
130+
}
131+
132+
if (dataEncryptionKeyInfo == null) {
133+
throw new IOException("Data Encryption Key Information header [DEK-Info] not found");
134+
}
135+
136+
return dataEncryptionKeyInfo;
137+
}
138+
139+
private static class DataEncryptionKeyInfo {
140+
private final String algorithm;
141+
142+
private final byte[] initializationVector;
143+
144+
private DataEncryptionKeyInfo(final String algorithm, final byte[] initializationVector) {
145+
this.algorithm = algorithm;
146+
this.initializationVector = initializationVector;
147+
}
148+
}
149+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (C)2009 - SSHJ Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.schmizz.sshj.userauth.keyprovider;
17+
18+
import java.util.List;
19+
import java.util.Objects;
20+
21+
/**
22+
* PEM Key container with identified Key Type and decoded body
23+
*/
24+
public class PEMKey {
25+
private final PEMKeyType pemKeyType;
26+
27+
private final List<String> headers;
28+
29+
private final byte[] body;
30+
31+
PEMKey(final PEMKeyType pemKeyType, final List<String> headers, final byte[] body) {
32+
this.pemKeyType = Objects.requireNonNull(pemKeyType, "PEM Key Type required");
33+
this.headers = Objects.requireNonNull(headers, "Headers required");
34+
this.body = Objects.requireNonNull(body, "Body required");
35+
}
36+
37+
PEMKeyType getPemKeyType() {
38+
return pemKeyType;
39+
}
40+
41+
List<String> getHeaders() {
42+
return headers;
43+
}
44+
45+
byte[] getBody() {
46+
return body.clone();
47+
}
48+
49+
public enum PEMKeyType {
50+
/** RFC 3279 Section 2.3.2 */
51+
DSA("-----BEGIN DSA PRIVATE KEY-----"),
52+
53+
/** RFC 5915 Section 3 */
54+
EC("-----BEGIN EC PRIVATE KEY-----"),
55+
56+
/** RFC 8017 Appendix 1.2 */
57+
RSA("-----BEGIN RSA PRIVATE KEY-----"),
58+
59+
/** RFC 5208 Section 5 */
60+
PKCS8("-----BEGIN PRIVATE KEY-----"),
61+
62+
/** RFC 5208 Section 6 */
63+
PKCS8_ENCRYPTED("-----BEGIN ENCRYPTED PRIVATE KEY-----");
64+
65+
private final String header;
66+
67+
PEMKeyType(final String header) {
68+
this.header = header;
69+
}
70+
71+
String getHeader() {
72+
return header;
73+
}
74+
}
75+
}

src/main/java/net/schmizz/sshj/userauth/keyprovider/pkcs/KeyPairConverter.java renamed to src/main/java/net/schmizz/sshj/userauth/keyprovider/PEMKeyReader.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,21 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package net.schmizz.sshj.userauth.keyprovider.pkcs;
17-
18-
import org.bouncycastle.openssl.PEMKeyPair;
16+
package net.schmizz.sshj.userauth.keyprovider;
1917

18+
import java.io.BufferedReader;
2019
import java.io.IOException;
2120

2221
/**
23-
* Converter from typed object to PEM Key Pair
24-
* @param <T> Object Type
22+
* Abstraction for parsing and returning PEM Keys
2523
*/
26-
public interface KeyPairConverter<T> {
24+
interface PEMKeyReader {
2725
/**
28-
* Get PEM Key Pair from typed object
26+
* Read PEM Key from buffered reader
2927
*
30-
* @param object Typed Object
31-
* @return PEM Key Pair
32-
* @throws IOException Thrown on conversion failures
28+
* @param bufferedReader Buffered Reader containing lines from resource reader
29+
* @return PEM Key
30+
* @throws IOException Thrown on failure to read PEM Key from resources
3331
*/
34-
PEMKeyPair getKeyPair(T object) throws IOException;
32+
PEMKey readPemKey(BufferedReader bufferedReader) throws IOException;
3533
}

0 commit comments

Comments
 (0)