Skip to content

Commit 83eb75e

Browse files
committed
Merge branch 'pkcs1.and.pem.bc.#6'
* pkcs1.and.pem.bc.#6: refactor if-else-cascade to switch and improve method names and error messages package and publish dependencies (esp. bouncycastle) using shadow jar replace hard-coded key type with constants inline method and improve error message for PKCS#8 key generation replace properitary `sun.securtiy.*` API with bouncycastle code reuse already existing private key generation from bytes for PKSC#8 standard incorporate requested review changes from #6 document the new key probabilities in README.md use newly introduced PKCS1 and PKCS8 support for pem files (#2) add PKCS1 and PKCS8 support for pem files (#2) enhance integration tests to be able to cover all kinds of key types easily (#2) cleanup add support for PKCS#1 keys
2 parents 2899691 + 5edbf3d commit 83eb75e

File tree

4 files changed

+70
-67
lines changed

4 files changed

+70
-67
lines changed

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ Note: Keys can be transformed using `openssl`, e.g. from [PKCS#8]() in [PEM]() f
111111
openssl pkcs8 -topk8 -nocrypt -in $TASKD_GENERATED_KEY.key.pem -inform PEM -out $KEY_NAME.key.pkcs8.der -outform DER
112112
```
113113

114-
**Word of warning**: Current key parsing algorithm for [PKCS#1]() [PEM](() key uses
115-
`sun.security.util.DerInputStream` and `sun.security.util.DerValue` which are not part of the public interface, see
116-
https://www.oracle.com/java/technologies/faq-sun-packages.html for further explainations.
117-
118114
[PKCS#1]: https://en.wikipedia.org/wiki/PKCS_1
119115
[PKCS#8]: https://en.wikipedia.org/wiki/PKCS_8
120116
[PEM]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail

build.gradle.kts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import java.time.format.DateTimeFormatter
44

55
plugins {
66
`java-library`
7+
id("com.github.johnrengelman.shadow") version "5.2.0"
78
jacoco
89

910
id("com.github.spotbugs") version "3.0.0"
@@ -29,7 +30,7 @@ java {
2930
sourceCompatibility = JavaVersion.VERSION_1_8
3031
targetCompatibility = JavaVersion.VERSION_1_8
3132

32-
withSourcesJar()
33+
withJavadocJar()
3334
withSourcesJar()
3435
}
3536

@@ -45,6 +46,8 @@ dependencies {
4546
testCompileOnly(it)
4647
}
4748

49+
implementation("org.bouncycastle:bcpkix-jdk15on:1.64")
50+
4851
testImplementation("org.junit.jupiter:junit-jupiter:5.5.2")
4952
testImplementation("org.assertj:assertj-core:3.14.0")
5053
testImplementation("org.mockito:mockito-junit-jupiter:3.2.4")
@@ -53,7 +56,7 @@ dependencies {
5356
tasks {
5457
withType<JavaCompile> {
5558
options.encoding = "UTF-8"
56-
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror", "-Xlint:-processing", "-XDenableSunApiLintControl"))
59+
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror", "-Xlint:-processing"))
5760
}
5861

5962
withType<Jar> {
@@ -64,6 +67,7 @@ tasks {
6467
}
6568

6669
jar {
70+
enabled = false
6771
manifest {
6872
val now = LocalDate.now()
6973

@@ -92,6 +96,15 @@ tasks {
9296
}
9397
}
9498

99+
shadowJar {
100+
archiveClassifier.set("")
101+
minimize()
102+
relocate("org.bouncycastle", "de.aaschmid.taskwarrior.thirdparty.org.bouncycastle")
103+
}
104+
assemble {
105+
dependsOn(shadowJar)
106+
}
107+
95108
test {
96109
useJUnitPlatform()
97110
}
@@ -172,7 +185,11 @@ tasks.withType<GenerateModuleMetadata> {
172185
publishing {
173186
publications {
174187
register<MavenPublication>("mavenJava") {
175-
from(components["java"])
188+
// Not using `from(components["java"])` prevents from adding any shadowed dependency to resulting pom.xml
189+
shadow.component(this)
190+
artifact(tasks.get("javadocJar"))
191+
artifact(tasks.get("sourcesJar"))
192+
176193
pom {
177194
packaging = "jar"
178195

src/main/java/de/aaschmid/taskwarrior/client/KeyStoreBuilder.java

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package de.aaschmid.taskwarrior.client;
22

33
import java.io.BufferedInputStream;
4+
import java.io.ByteArrayInputStream;
45
import java.io.File;
56
import java.io.FileInputStream;
67
import java.io.IOException;
7-
import java.math.BigInteger;
8+
import java.io.InputStreamReader;
89
import java.nio.charset.StandardCharsets;
910
import java.nio.file.Files;
1011
import java.security.KeyFactory;
@@ -19,25 +20,26 @@
1920
import java.security.cert.CertificateException;
2021
import java.security.cert.CertificateFactory;
2122
import java.security.spec.InvalidKeySpecException;
22-
import java.security.spec.KeySpec;
2323
import java.security.spec.PKCS8EncodedKeySpec;
24-
import java.security.spec.RSAPrivateCrtKeySpec;
2524
import java.util.ArrayList;
26-
import java.util.Base64;
2725
import java.util.List;
2826
import java.util.concurrent.atomic.AtomicInteger;
29-
import java.util.regex.Matcher;
30-
import java.util.regex.Pattern;
27+
28+
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
29+
import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters;
30+
import org.bouncycastle.crypto.util.PrivateKeyInfoFactory;
31+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
32+
import org.bouncycastle.util.io.pem.PemObject;
33+
import org.bouncycastle.util.io.pem.PemReader;
3134

3235
import static java.util.Objects.requireNonNull;
3336

3437
class KeyStoreBuilder {
3538

36-
private static final String TYPE_CERTIFICATE = "X.509";
37-
private static final String ALGORITHM_PRIVATE_KEY = "RSA";
38-
39-
private static final Pattern PATTERN_PKCS1_PEM = Pattern.compile("-----BEGIN RSA PRIVATE KEY-----(.*)-----END RSA PRIVATE KEY-----");
40-
private static final Pattern PATTERN_PKCS8_PEM = Pattern.compile("-----BEGIN PRIVATE KEY-----(.*)-----END PRIVATE KEY-----");
39+
private static final String CERTIFICATE_TYPE = "X.509";
40+
private static final String KEY_ALGORITHM_RSA = "RSA";
41+
private static final String PEM_TYPE_PKCS1 = "RSA PRIVATE KEY";
42+
private static final String PEM_TYPE_PKCS8 = "PRIVATE KEY";
4143

4244
private ProtectionParameter keyStoreProtection;
4345
private File caCertFile;
@@ -71,6 +73,10 @@ KeyStoreBuilder withPrivateKeyCertFile(File privateKeyCertFile) {
7173
return this;
7274
}
7375

76+
/**
77+
* Provide the non-null private key file to use. Supported are PKCS#1 and PKCS#8 keys in {@code *.PEM} format as well as PKCS#8 keys in
78+
* {@code *.DER} format.
79+
*/
7480
KeyStoreBuilder withPrivateKeyFile(File privateKeyFile) {
7581
requireNonNull(privateKeyFile, "'privateKeyFile' must not be null.");
7682
if (!privateKeyFile.exists()) {
@@ -112,7 +118,7 @@ KeyStore build() {
112118
private List<Certificate> createCertificatesFor(File certFile) {
113119
List<Certificate> result = new ArrayList<>();
114120
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(certFile))) {
115-
CertificateFactory cf = CertificateFactory.getInstance(TYPE_CERTIFICATE);
121+
CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE);
116122
while (bis.available() > 0) {
117123
result.add(cf.generateCertificate(bis));
118124
}
@@ -128,67 +134,51 @@ private PrivateKey createPrivateKeyFor(File privateKeyFile) {
128134
try {
129135
byte[] bytes = Files.readAllBytes(privateKeyFile.toPath());
130136
if (privateKeyFile.getName().endsWith("pem")) {
131-
String content = new String(bytes, StandardCharsets.UTF_8).replaceAll("\\n", "");
132-
133-
Matcher pkcs1Matcher = PATTERN_PKCS1_PEM.matcher(content);
134-
if (pkcs1Matcher.find()) {
135-
return createPrivateKeyFromPemPkcs1(pkcs1Matcher.group(1));
137+
PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8));
138+
PemObject privateKeyObject = pemReader.readPemObject();
139+
140+
switch (privateKeyObject.getType()) {
141+
case PEM_TYPE_PKCS1:
142+
return createPrivateKeyForPkcs1(privateKeyObject.getContent());
143+
case PEM_TYPE_PKCS8:
144+
return createPrivateKeyForPkcs8(privateKeyObject.getContent());
145+
default:
146+
throw new TaskwarriorKeyStoreException("Unsupported key algorithm '%s'.", privateKeyObject.getType());
136147
}
137-
138-
Matcher pkcs8Matcher = PATTERN_PKCS8_PEM.matcher(content);
139-
if (pkcs8Matcher.find()) {
140-
return createPrivateKeyFromPemPkcs8(pkcs8Matcher.group(1));
141-
}
142-
143-
throw new TaskwarriorKeyStoreException("Could not detect key algorithm for '%s'.", privateKeyFile);
144148
}
145-
return createPrivateKeyFromPkcs8Der(bytes);
149+
return createPrivateKeyForPkcs8(bytes);
146150
} catch (IOException e) {
147151
throw new TaskwarriorKeyStoreException(e, "Could not read private key of '%s' via input stream.", privateKeyFile);
148152
}
149153
}
150154

151-
@SuppressWarnings("sunapi")
152-
private PrivateKey createPrivateKeyFromPemPkcs1(String privateKeyContent) throws IOException {
155+
private PrivateKey createPrivateKeyForPkcs1(byte[] privateKeyBytes) {
156+
RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyBytes);
157+
RSAPrivateCrtKeyParameters keyParameters = new RSAPrivateCrtKeyParameters(
158+
rsa.getModulus(),
159+
rsa.getPublicExponent(),
160+
rsa.getPrivateExponent(),
161+
rsa.getPrime1(),
162+
rsa.getPrime2(),
163+
rsa.getExponent1(),
164+
rsa.getExponent2(),
165+
rsa.getCoefficient());
166+
153167
try {
154-
byte[] bytes = Base64.getDecoder().decode(privateKeyContent);
155-
156-
sun.security.util.DerInputStream derReader = new sun.security.util.DerInputStream(bytes);
157-
sun.security.util.DerValue[] seq = derReader.getSequence(0);
158-
// skip version seq[0];
159-
BigInteger modulus = seq[1].getBigInteger();
160-
BigInteger publicExp = seq[2].getBigInteger();
161-
BigInteger privateExp = seq[3].getBigInteger();
162-
BigInteger prime1 = seq[4].getBigInteger();
163-
BigInteger prime2 = seq[5].getBigInteger();
164-
BigInteger exp1 = seq[6].getBigInteger();
165-
BigInteger exp2 = seq[7].getBigInteger();
166-
BigInteger crtCoef = seq[8].getBigInteger();
167-
168-
RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef);
169-
return createPrivateKey(privateKeyFile, keySpec);
170-
} catch (Error | Exception e) {
171-
throw new TaskwarriorKeyStoreException("Could not use required but proprietary 'sun.security.util' package on this platform.", e);
168+
return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(keyParameters));
169+
} catch (IOException e) {
170+
throw new TaskwarriorKeyStoreException(e, "Failed to encode PKCS#1 private key of '%s'.", privateKeyFile);
172171
}
173172
}
174173

175-
private PrivateKey createPrivateKeyFromPemPkcs8(String privateKeyContent) {
176-
byte[] bytes = Base64.getDecoder().decode(privateKeyContent);
177-
return createPrivateKey(privateKeyFile, new PKCS8EncodedKeySpec(bytes));
178-
}
179-
180-
private PrivateKey createPrivateKeyFromPkcs8Der(byte[] privateKeyBytes) {
181-
return createPrivateKey(privateKeyFile, new PKCS8EncodedKeySpec(privateKeyBytes));
182-
}
183-
184-
private PrivateKey createPrivateKey(File privateKeyFile, KeySpec keySpec) {
174+
private PrivateKey createPrivateKeyForPkcs8(byte[] privateKeyBytes) {
185175
try {
186-
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_PRIVATE_KEY);
187-
return keyFactory.generatePrivate(keySpec);
176+
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM_RSA);
177+
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
188178
} catch (NoSuchAlgorithmException e) {
189-
throw new TaskwarriorKeyStoreException(e, "Key factory could not be initialized for algorithm '%s'.", ALGORITHM_PRIVATE_KEY);
179+
throw new TaskwarriorKeyStoreException(e, "Key factory could not be initialized for algorithm '%s'.", KEY_ALGORITHM_RSA);
190180
} catch (InvalidKeySpecException e) {
191-
throw new TaskwarriorKeyStoreException(e, "Could not generate private key for '%s'.", privateKeyFile);
181+
throw new TaskwarriorKeyStoreException(e, "Invalid key spec for %s private key in '%s'.", KEY_ALGORITHM_RSA, privateKeyFile);
192182
}
193183
}
194184
}

src/test/java/de/aaschmid/taskwarrior/client/KeyStoreBuilderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ void build_shouldTaskwarriorKeyStoreExceptionIfPrivateKeyIsInvalid() throws IOEx
148148

149149
assertThatThrownBy(builder::build)
150150
.isInstanceOf(TaskwarriorKeyStoreException.class)
151-
.hasMessage("Could not generate private key for '" + privateKeyFile.toAbsolutePath() + "'.")
151+
.hasMessage("Invalid key spec for RSA private key in '" + privateKeyFile.toAbsolutePath() + "'.")
152152
.hasCauseInstanceOf(InvalidKeySpecException.class);
153153
}
154154

0 commit comments

Comments
 (0)