Skip to content

Commit 2899691

Browse files
committed
Merge branch 'pkcs1.native.#2'
* pkcs1.native.#2: 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)
2 parents 8d4a9a4 + 3965aa9 commit 2899691

File tree

4 files changed

+73
-23
lines changed

4 files changed

+73
-23
lines changed

README.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ class Taskwarrior {
8989
Used `taskwarrior.properties` can be created by copying and adjusting
9090
[`src/main/resources/taskwarrior.properties.template`](https://github.com/aaschmid/taskwarrior-java-client/tree/master/src/main/resources/taskwarrior.properties.template).
9191

92-
9392
Testing
9493
-------
9594

@@ -99,18 +98,29 @@ To run tests manually you will need to build and run taskwarrior server containe
9998
Keys formats
10099
------------
101100

102-
Unfortunately Java only has an encoded key spec for a private key in [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8)
103-
format. However, [taskd](https://taskwarrior.org/docs/taskserver/setup.html) generates the private key in
104-
[PKCS#1](https://en.wikipedia.org/wiki/PKCS_1) format if you follow the
105-
[documentation](https://taskwarrior.org/docs/taskserver/user.html). The transformation command below also converts the
106-
key from [PEM](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail) to
107-
[DER](https://en.wikipedia.org/wiki/X.690#DER_encoding) format which does not need any further transformation as
108-
handling of the base64 encoded PEM keys.
101+
| Key specification | [PEM]() format¹ | [DER]() format |
102+
| ----------------- |:---------------:|:--------------:|
103+
| [PKCS#1]() | yes | |
104+
| [PKCS#8]() | yes | yes |
105+
106+
¹: The kind of format is currently detected by file extentions.
107+
108+
Note: Keys can be transformed using `openssl`, e.g. from [PKCS#8]() in [PEM]() format to [PKCS#1]() in [DER]() format:
109109

110110
```sh
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+
118+
[PKCS#1]: https://en.wikipedia.org/wiki/PKCS_1
119+
[PKCS#8]: https://en.wikipedia.org/wiki/PKCS_8
120+
[PEM]: https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail
121+
[DER]: https://en.wikipedia.org/wiki/X.690#DER_encoding
122+
123+
114124
Release notes
115125
-------------
116126

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ dependencies {
5353
tasks {
5454
withType<JavaCompile> {
5555
options.encoding = "UTF-8"
56-
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror", "-Xlint:-processing"))
56+
options.compilerArgs.addAll(listOf("-Xlint:all", "-Werror", "-Xlint:-processing", "-XDenableSunApiLintControl"))
5757
}
5858

5959
withType<Jar> {

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

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.io.File;
55
import java.io.FileInputStream;
66
import java.io.IOException;
7+
import java.math.BigInteger;
8+
import java.nio.charset.StandardCharsets;
79
import java.nio.file.Files;
810
import java.security.KeyFactory;
911
import java.security.KeyStore;
@@ -19,9 +21,13 @@
1921
import java.security.spec.InvalidKeySpecException;
2022
import java.security.spec.KeySpec;
2123
import java.security.spec.PKCS8EncodedKeySpec;
24+
import java.security.spec.RSAPrivateCrtKeySpec;
2225
import java.util.ArrayList;
26+
import java.util.Base64;
2327
import java.util.List;
2428
import java.util.concurrent.atomic.AtomicInteger;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
2531

2632
import static java.util.Objects.requireNonNull;
2733

@@ -30,6 +36,9 @@ class KeyStoreBuilder {
3036
private static final String TYPE_CERTIFICATE = "X.509";
3137
private static final String ALGORITHM_PRIVATE_KEY = "RSA";
3238

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-----");
41+
3342
private ProtectionParameter keyStoreProtection;
3443
private File caCertFile;
3544
private File privateKeyCertFile;
@@ -62,17 +71,6 @@ KeyStoreBuilder withPrivateKeyCertFile(File privateKeyCertFile) {
6271
return this;
6372
}
6473

65-
/**
66-
* Provide the private key file to use. It must be in {@code *.DER} format. If you have a *.PME private key, you can create it using
67-
* {@code openssl}.
68-
* <p>
69-
* The required command looks like following:<br>
70-
* <code>$ openssl pkcs8 -topk8 -nocrypt -in key.pem -inform PEM -out key.der -outform DER</code>
71-
* </p>
72-
*
73-
* @param privateKeyFile private key file in {@code *.DER} format (must not be {@code null}
74-
* @return the {@link KeyStore} builder itself
75-
*/
7674
KeyStoreBuilder withPrivateKeyFile(File privateKeyFile) {
7775
requireNonNull(privateKeyFile, "'privateKeyFile' must not be null.");
7876
if (!privateKeyFile.exists()) {
@@ -129,12 +127,56 @@ private List<Certificate> createCertificatesFor(File certFile) {
129127
private PrivateKey createPrivateKeyFor(File privateKeyFile) {
130128
try {
131129
byte[] bytes = Files.readAllBytes(privateKeyFile.toPath());
130+
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));
136+
}
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);
144+
}
132145
return createPrivateKeyFromPkcs8Der(bytes);
133146
} catch (IOException e) {
134147
throw new TaskwarriorKeyStoreException(e, "Could not read private key of '%s' via input stream.", privateKeyFile);
135148
}
136149
}
137150

151+
@SuppressWarnings("sunapi")
152+
private PrivateKey createPrivateKeyFromPemPkcs1(String privateKeyContent) throws IOException {
153+
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);
172+
}
173+
}
174+
175+
private PrivateKey createPrivateKeyFromPemPkcs8(String privateKeyContent) {
176+
byte[] bytes = Base64.getDecoder().decode(privateKeyContent);
177+
return createPrivateKey(privateKeyFile, new PKCS8EncodedKeySpec(bytes));
178+
}
179+
138180
private PrivateKey createPrivateKeyFromPkcs8Der(byte[] privateKeyBytes) {
139181
return createPrivateKey(privateKeyFile, new PKCS8EncodedKeySpec(privateKeyBytes));
140182
}

src/test/java/de/aaschmid/taskwarrior/TaskwarriorClientIntegrationTest.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import de.aaschmid.taskwarrior.message.TaskwarriorMessage;
88
import de.aaschmid.taskwarrior.message.TaskwarriorRequestHeader;
99
import de.aaschmid.taskwarrior.test.IntegrationTest;
10-
import org.junit.jupiter.api.Test;
1110
import org.junit.jupiter.params.ParameterizedTest;
1211
import org.junit.jupiter.params.provider.Arguments;
1312
import org.junit.jupiter.params.provider.MethodSource;
@@ -24,8 +23,7 @@ class TaskwarriorClientIntegrationTest {
2423
private static final String SYNC_KEY = "f92d5c8d-4cf9-4cf5-b72f-1f4a70cf9b20";
2524

2625
static Stream<Arguments> configs() {
27-
return Stream.of("pkcs8-der")
28-
// return Stream.of("pkcs1", "pkcs8", "pkcs8-der") // TODO use to test all key type to be supported
26+
return Stream.of("pkcs1", "pkcs8", "pkcs8-der")
2927
.map(keyType -> format("/taskwarrior.%s.properties", keyType))
3028
.map(TaskwarriorClientIntegrationTest.class::getResource)
3129
.map(TaskwarriorConfiguration::taskwarriorPropertiesConfiguration)

0 commit comments

Comments
 (0)