summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.gpg.bc
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.gpg.bc')
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF8
-rw-r--r--org.eclipse.jgit.gpg.bc/about.html83
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties12
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java12
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java51
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java7
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java10
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java121
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java826
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java110
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java597
11 files changed, 1744 insertions, 93 deletions
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index 040ed0818c..afb0ee151f 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -18,6 +18,7 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.interfaces;version="[1.65.0,2.0.0)",
+ org.bouncycastle.jcajce.util;version="[1.65.0,2.0.0)",
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.math.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.math.field;version="[1.65.0,2.0.0)",
@@ -25,14 +26,11 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
+ org.bouncycastle.util;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
+ org.bouncycastle.util.io;version="[1.65.0,2.0.0)",
org.eclipse.jgit.annotations;version="[5.11.0,5.12.0)",
org.eclipse.jgit.api.errors;version="[5.11.0,5.12.0)",
- org.eclipse.jgit.errors;version="[5.11.0,5.12.0)",
- org.eclipse.jgit.lib;version="[5.11.0,5.12.0)",
- org.eclipse.jgit.nls;version="[5.11.0,5.12.0)",
- org.eclipse.jgit.transport;version="[5.11.0,5.12.0)",
- org.eclipse.jgit.util;version="[5.11.0,5.12.0)",
org.slf4j;version="[1.7.0,2.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc;version="5.11.0",
org.eclipse.jgit.gpg.bc.internal;version="5.11.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
diff --git a/org.eclipse.jgit.gpg.bc/about.html b/org.eclipse.jgit.gpg.bc/about.html
index f971af18d0..fc527d5a3a 100644
--- a/org.eclipse.jgit.gpg.bc/about.html
+++ b/org.eclipse.jgit.gpg.bc/about.html
@@ -11,7 +11,7 @@
margin: 0.25in 0.5in 0.25in 0.5in;
tab-interval: 0.5in;
}
- p {
+ p {
margin-left: auto;
margin-top: 0.5em;
margin-bottom: 0.5em;
@@ -36,60 +36,53 @@
<p>Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. </p>
<p>All rights reserved.</p>
-<p>Redistribution and use in source and binary forms, with or without modification,
+<p>Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
-<ul><li>Redistributions of source code must retain the above copyright notice,
+<ul><li>Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer. </li>
-<li>Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
+<li>Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. </li>
-<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its
- contributors may be used to endorse or promote products derived from
+<li>Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ contributors may be used to endorse or promote products derived from
this software without specific prior written permission. </li></ul>
</p>
-<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
-IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
-NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
-WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.</p>
<hr>
-<p><b>SHA-1 UbcCheck - MIT</b></p>
+<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>
-<p>Copyright (c) 2017:</p>
-<div class="ubc-name">
-Marc Stevens
-Cryptology Group
-Centrum Wiskunde & Informatica
-P.O. Box 94079, 1090 GB Amsterdam, Netherlands
-marc@marc-stevens.nl
-</div>
-<div class="ubc-name">
-Dan Shumow
-Microsoft Research
-danshu@microsoft.com
-</div>
-<p>Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+<p>Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc.
+(<a href="https://www.bouncycastle.org">https://www.bouncycastle.org</a>)</p>
+
+<p>
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+and associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+</p>
+<p>
+The above copyright notice and this permission notice shall be included in all copies or substantial
+portions of the Software.
+</p>
+<p>
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
</p>
-<ul><li>The above copyright notice and this permission notice shall be included
-in all copies or substantial portions of the Software.</li></ul>
-<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.</p>
</body>
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index f2aa014d6b..e4b1baba1f 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -1,5 +1,7 @@
corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
+cryptCipherError=Cannot create cipher to decrypt: {0}
+cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
gpgNoCredentialsProvider=missing credentials provider
gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
@@ -7,10 +9,20 @@ gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
+gpgNoSuchAlgorithm=Cannot decrypt encrypted secret key: encryption algorithm {0} is not available
gpgNotASigningKey=Secret key ({0}) is not suitable for signing
gpgKeyInfo=GPG Key (fingerprint {0})
gpgSigningCancelled=Signing was cancelled
nonSignatureError=Signature does not decode into a signature object
+secretKeyTooShort=Secret key file corrupt; only {0} bytes read
+sexprHexNotClosed=Hex number in s-expression not closed
+sexprHexOdd=Hex number in s-expression has an odd number of digits
+sexprStringInvalidEscape=Invalid escape {0} in s-expression
+sexprStringInvalidEscapeAtEnd=Invalid s-expression: quoted string ends with escape character
+sexprStringInvalidHexEscape=Invalid hex escape in s-expression
+sexprStringInvalidOctalEscape=Invalid octal escape in s-expression
+sexprStringNotClosed=String in s-expression not closed
+sexprUnhandled=Unhandled token {0} in s-expression
signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
signatureKeyLookupError=Error occurred while looking for public key
signatureNoKeyInfo=No way to determine a public key from the signature
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 4753ac138d..aedf8a5be5 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -29,6 +29,8 @@ public final class BCText extends TranslationBundle {
// @formatter:off
/***/ public String corrupt25519Key;
/***/ public String credentialPassphrase;
+ /***/ public String cryptCipherError;
+ /***/ public String cryptWrongDecryptedLength;
/***/ public String gpgFailedToParseSecretKey;
/***/ public String gpgNoCredentialsProvider;
/***/ public String gpgNoKeygrip;
@@ -36,10 +38,20 @@ public final class BCText extends TranslationBundle {
/***/ public String gpgNoKeyInLegacySecring;
/***/ public String gpgNoPublicKeyFound;
/***/ public String gpgNoSecretKeyForPublicKey;
+ /***/ public String gpgNoSuchAlgorithm;
/***/ public String gpgNotASigningKey;
/***/ public String gpgKeyInfo;
/***/ public String gpgSigningCancelled;
/***/ public String nonSignatureError;
+ /***/ public String secretKeyTooShort;
+ /***/ public String sexprHexNotClosed;
+ /***/ public String sexprHexOdd;
+ /***/ public String sexprStringInvalidEscape;
+ /***/ public String sexprStringInvalidEscapeAtEnd;
+ /***/ public String sexprStringInvalidHexEscape;
+ /***/ public String sexprStringInvalidOctalEscape;
+ /***/ public String sexprStringNotClosed;
+ /***/ public String sexprUnhandled;
/***/ public String signatureInconsistent;
/***/ public String signatureKeyLookupError;
/***/ public String signatureNoKeyInfo;
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
index 7f0f32a2a7..cf4d3d2340 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
@@ -30,7 +30,6 @@ import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Locale;
-import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.KeyBox;
@@ -48,16 +47,15 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPUtil;
-import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
-import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
+import org.eclipse.jgit.gpg.bc.internal.keys.SecretKeys;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.SystemReader;
@@ -77,17 +75,10 @@ public class BouncyCastleGpgKeyLocator {
}
- /** Thrown if we try to read an encrypted private key without password. */
- private static class EncryptedPgpKeyException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- }
-
private static final Logger log = LoggerFactory
.getLogger(BouncyCastleGpgKeyLocator.class);
- private static final Path GPG_DIRECTORY = findGpgDirectory();
+ static final Path GPG_DIRECTORY = findGpgDirectory();
private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
.resolve("pubring.kbx"); //$NON-NLS-1$
@@ -154,11 +145,13 @@ public class BouncyCastleGpgKeyLocator {
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
- PBEProtectionRemoverFactory passphraseProvider,
- PGPPublicKey publicKey) throws IOException, PGPException {
+ SecretKeys.PassphraseSupplier passphraseSupplier,
+ PGPPublicKey publicKey)
+ throws IOException, PGPException, CanceledException,
+ UnsupportedCredentialItem, URISyntaxException {
try (InputStream in = newInputStream(keyFile)) {
- return new SExprParser(calculatorProvider).parseSecretKey(
- new BufferedInputStream(in), passphraseProvider, publicKey);
+ return SecretKeys.readSecretKey(in, calculatorProvider,
+ passphraseSupplier, publicKey);
}
}
@@ -483,29 +476,17 @@ public class BouncyCastleGpgKeyLocator {
try {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
- PBEProtectionRemoverFactory passphraseProvider = p -> {
- throw new EncryptedPgpKeyException();
- };
+ clearPrompt = true;
PGPSecretKey secretKey = null;
try {
- // Try without passphrase
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
- passphraseProvider, publicKey);
- } catch (EncryptedPgpKeyException e) {
- // Let's try again with a passphrase
- passphraseProvider = new JcePBEProtectionRemoverFactory(
- passphrasePrompt.getPassphrase(
- publicKey.getFingerprint(), userKeyboxPath));
- clearPrompt = true;
- try {
- secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
- passphraseProvider, publicKey);
- } catch (PGPException e1) {
- throw new PGPException(MessageFormat.format(
- BCText.get().gpgFailedToParseSecretKey,
- keyFile.toAbsolutePath()), e);
-
- }
+ () -> passphrasePrompt.getPassphrase(
+ publicKey.getFingerprint(), userKeyboxPath),
+ publicKey);
+ } catch (PGPException e) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().gpgFailedToParseSecretKey,
+ keyFile.toAbsolutePath()), e);
}
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
index e47f64f1a6..6144195983 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyPassphrasePrompt.java
@@ -17,8 +17,8 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.transport.CredentialItem.CharArrayType;
import org.eclipse.jgit.transport.CredentialItem.InformationalMessage;
+import org.eclipse.jgit.transport.CredentialItem.Password;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
@@ -31,7 +31,7 @@ import org.eclipse.jgit.transport.URIish;
*/
class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
- private CharArrayType passphrase;
+ private Password passphrase;
private CredentialsProvider credentialsProvider;
@@ -78,8 +78,7 @@ class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
if (passphrase == null) {
- passphrase = new CharArrayType(BCText.get().credentialPassphrase,
- true);
+ passphrase = new Password(BCText.get().credentialPassphrase);
}
if (credentialsProvider == null) {
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index 9f48e5431e..211bd7bd20 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -49,7 +49,7 @@ import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.util.StringUtils;
/**
- * GPG Signer using BouncyCastle library
+ * GPG Signer using the BouncyCastle library.
*/
public class BouncyCastleGpgSigner extends GpgSigner
implements GpgObjectSigner {
@@ -97,8 +97,9 @@ public class BouncyCastleGpgSigner extends GpgSigner
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
return gpgKey != null;
- } catch (PGPException | IOException | NoSuchAlgorithmException
- | NoSuchProviderException | URISyntaxException e) {
+ } catch (CanceledException e) {
+ throw e;
+ } catch (Exception e) {
return false;
}
}
@@ -143,7 +144,8 @@ public class BouncyCastleGpgSigner extends GpgSigner
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
- committer, passphrasePrompt);
+ committer,
+ passphrasePrompt);
PGPSecretKey secretKey = gpgKey.getSecretKey();
if (secretKey == null) {
throw new JGitInternalException(
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
new file mode 100644
index 0000000000..68f8a45555
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.security.NoSuchAlgorithmException;
+import java.text.MessageFormat;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.util.Arrays;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+
+/**
+ * A {@link PBEProtectionRemoverFactory} using AES/OCB/NoPadding for decryption.
+ * It accepts an AAD in the factory's constructor, so the factory can be used to
+ * create a {@link PBESecretKeyDecryptor} only for a particular input.
+ * <p>
+ * For JGit's needs, this is sufficient, but for a general upstream
+ * implementation that limitation might not be acceptable.
+ * </p>
+ */
+class OCBPBEProtectionRemoverFactory
+ implements PBEProtectionRemoverFactory {
+
+ private final PGPDigestCalculatorProvider calculatorProvider;
+
+ private final char[] passphrase;
+
+ private final byte[] aad;
+
+ /**
+ * Creates a new factory instance with the given parameters.
+ * <p>
+ * Because the AAD is given at factory level, the {@link PBESecretKeyDecryptor}s
+ * created by the factory can be used to decrypt only a particular input
+ * matching this AAD.
+ * </p>
+ *
+ * @param passphrase to use for secret key derivation
+ * @param calculatorProvider for computing digests
+ * @param aad for the OCB decryption
+ */
+ OCBPBEProtectionRemoverFactory(char[] passphrase,
+ PGPDigestCalculatorProvider calculatorProvider, byte[] aad) {
+ this.calculatorProvider = calculatorProvider;
+ this.passphrase = passphrase;
+ this.aad = aad;
+ }
+
+ @Override
+ public PBESecretKeyDecryptor createDecryptor(String protection)
+ throws PGPException {
+ return new PBESecretKeyDecryptor(passphrase, calculatorProvider) {
+
+ @Override
+ public byte[] recoverKeyData(int encAlgorithm, byte[] key,
+ byte[] iv, byte[] encrypted, int encryptedOffset,
+ int encryptedLength) throws PGPException {
+ String algorithmName = PGPUtil
+ .getSymmetricCipherName(encAlgorithm);
+ byte[] decrypted = null;
+ try {
+ Cipher c = Cipher
+ .getInstance(algorithmName + "/OCB/NoPadding"); //$NON-NLS-1$
+ SecretKey secretKey = new SecretKeySpec(key, algorithmName);
+ c.init(Cipher.DECRYPT_MODE, secretKey,
+ new IvParameterSpec(iv));
+ c.updateAAD(aad);
+ decrypted = new byte[c.getOutputSize(encryptedLength)];
+ int decryptedLength = c.update(encrypted, encryptedOffset,
+ encryptedLength, decrypted);
+ // doFinal() for OCB will check the MAC and throw an
+ // exception if it doesn't match
+ decryptedLength += c.doFinal(decrypted, decryptedLength);
+ if (decryptedLength != decrypted.length) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().cryptWrongDecryptedLength,
+ Integer.valueOf(decryptedLength),
+ Integer.valueOf(decrypted.length)));
+ }
+ byte[] result = decrypted;
+ decrypted = null; // Don't clear in finally
+ return result;
+ } catch (NoClassDefFoundError e) {
+ String msg = MessageFormat.format(
+ BCText.get().gpgNoSuchAlgorithm,
+ algorithmName + "/OCB"); //$NON-NLS-1$
+ throw new PGPException(msg,
+ new NoSuchAlgorithmException(msg, e));
+ } catch (PGPException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new PGPException(
+ MessageFormat.format(BCText.get().cryptCipherError,
+ e.getLocalizedMessage()),
+ e);
+ } finally {
+ if (decrypted != null) {
+ // Prevent halfway decrypted data leaking.
+ Arrays.fill(decrypted, (byte) 0);
+ }
+ }
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
new file mode 100644
index 0000000000..a9bb22c780
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SExprParser.java
@@ -0,0 +1,826 @@
+/*
+ * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+ * <p>
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * </p>
+ * <p>
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ * </p>
+ * <p>
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </p>
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.util.Date;
+
+import org.bouncycastle.asn1.x9.ECNamedCurveTable;
+import org.bouncycastle.bcpg.DSAPublicBCPGKey;
+import org.bouncycastle.bcpg.DSASecretBCPGKey;
+import org.bouncycastle.bcpg.ECDSAPublicBCPGKey;
+import org.bouncycastle.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.ECSecretBCPGKey;
+import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
+import org.bouncycastle.bcpg.ElGamalSecretBCPGKey;
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.bcpg.PublicKeyPacket;
+import org.bouncycastle.bcpg.RSAPublicBCPGKey;
+import org.bouncycastle.bcpg.RSASecretBCPGKey;
+import org.bouncycastle.bcpg.S2K;
+import org.bouncycastle.bcpg.SecretKeyPacket;
+import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.util.Arrays;
+import org.bouncycastle.util.Strings;
+
+/**
+ * A parser for secret keys stored in s-expressions. Original BouncyCastle code
+ * modified by the JGit team to:
+ * <ul>
+ * <li>handle unencrypted DSA, EC, and ElGamal keys (upstream only handles
+ * unencrypted RSA), and</li>
+ * <li>handle secret keys using AES/OCB as encryption (those don't have a
+ * hash).</li>
+ * </ul>
+ */
+@SuppressWarnings("nls")
+public class SExprParser {
+ private final PGPDigestCalculatorProvider digestProvider;
+
+ /**
+ * Base constructor.
+ *
+ * @param digestProvider
+ * a provider for digest calculations. Used to confirm key
+ * protection hashes.
+ */
+ public SExprParser(PGPDigestCalculatorProvider digestProvider) {
+ this.digestProvider = digestProvider;
+ }
+
+ /**
+ * Parse a secret key from one of the GPG S expression keys associating it
+ * with the passed in public key.
+ *
+ * @param inputStream
+ * to read from
+ * @param keyProtectionRemoverFactory
+ * for decrypting encrypted keys
+ * @param pubKey
+ * the private key should belong to
+ *
+ * @return a secret key object.
+ * @throws IOException
+ * @throws PGPException
+ */
+ public PGPSecretKey parseSecretKey(InputStream inputStream,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory,
+ PGPPublicKey pubKey) throws IOException, PGPException {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String type;
+
+ type = SXprUtils.readString(inputStream, inputStream.read());
+ if (type.equals("protected-private-key")
+ || type.equals("private-key")) {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String keyType = SXprUtils.readString(inputStream,
+ inputStream.read());
+ if (keyType.equals("ecc")) {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String curveID = SXprUtils.readString(inputStream,
+ inputStream.read());
+ String curveName = SXprUtils.readString(inputStream,
+ inputStream.read());
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ byte[] qVal;
+
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ type = SXprUtils.readString(inputStream, inputStream.read());
+ if (type.equals("q")) {
+ qVal = SXprUtils.readBytes(inputStream, inputStream.read());
+ } else {
+ throw new PGPException("no q value found");
+ }
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ BigInteger d = processECSecretKey(inputStream, curveID,
+ curveName, qVal, keyProtectionRemoverFactory);
+
+ if (curveName.startsWith("NIST ")) {
+ curveName = curveName.substring("NIST ".length());
+ }
+
+ ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
+ ECNamedCurveTable.getOID(curveName),
+ new BigInteger(1, qVal));
+ ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
+ .getPublicKeyPacket().getKey();
+ if (!basePubKey.getCurveOID().equals(assocPubKey.getCurveOID())
+ || !basePubKey.getEncodedPoint()
+ .equals(assocPubKey.getEncodedPoint())) {
+ throw new PGPException(
+ "passed in public key does not match secret key");
+ }
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new ECSecretBCPGKey(d).getEncoded()),
+ pubKey);
+ } else if (keyType.equals("dsa")) {
+ BigInteger p = readBigInteger("p", inputStream);
+ BigInteger q = readBigInteger("q", inputStream);
+ BigInteger g = readBigInteger("g", inputStream);
+
+ BigInteger y = readBigInteger("y", inputStream);
+
+ BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
+ keyProtectionRemoverFactory);
+
+ DSAPublicBCPGKey basePubKey = new DSAPublicBCPGKey(p, q, g, y);
+ DSAPublicBCPGKey assocPubKey = (DSAPublicBCPGKey) pubKey
+ .getPublicKeyPacket().getKey();
+ if (!basePubKey.getP().equals(assocPubKey.getP())
+ || !basePubKey.getQ().equals(assocPubKey.getQ())
+ || !basePubKey.getG().equals(assocPubKey.getG())
+ || !basePubKey.getY().equals(assocPubKey.getY())) {
+ throw new PGPException(
+ "passed in public key does not match secret key");
+ }
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new DSASecretBCPGKey(x).getEncoded()),
+ pubKey);
+ } else if (keyType.equals("elg")) {
+ BigInteger p = readBigInteger("p", inputStream);
+ BigInteger g = readBigInteger("g", inputStream);
+
+ BigInteger y = readBigInteger("y", inputStream);
+
+ BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
+ keyProtectionRemoverFactory);
+
+ ElGamalPublicBCPGKey basePubKey = new ElGamalPublicBCPGKey(p, g,
+ y);
+ ElGamalPublicBCPGKey assocPubKey = (ElGamalPublicBCPGKey) pubKey
+ .getPublicKeyPacket().getKey();
+ if (!basePubKey.getP().equals(assocPubKey.getP())
+ || !basePubKey.getG().equals(assocPubKey.getG())
+ || !basePubKey.getY().equals(assocPubKey.getY())) {
+ throw new PGPException(
+ "passed in public key does not match secret key");
+ }
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubKey.getPublicKeyPacket(),
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new ElGamalSecretBCPGKey(x).getEncoded()),
+ pubKey);
+ } else if (keyType.equals("rsa")) {
+ BigInteger n = readBigInteger("n", inputStream);
+ BigInteger e = readBigInteger("e", inputStream);
+
+ BigInteger[] values = processRSASecretKey(inputStream, n, e,
+ keyProtectionRemoverFactory);
+
+ // TODO: type of RSA key?
+ RSAPublicBCPGKey basePubKey = new RSAPublicBCPGKey(n, e);
+ RSAPublicBCPGKey assocPubKey = (RSAPublicBCPGKey) pubKey
+ .getPublicKeyPacket().getKey();
+ if (!basePubKey.getModulus().equals(assocPubKey.getModulus())
+ || !basePubKey.getPublicExponent()
+ .equals(assocPubKey.getPublicExponent())) {
+ throw new PGPException(
+ "passed in public key does not match secret key");
+ }
+
+ return new PGPSecretKey(new SecretKeyPacket(
+ pubKey.getPublicKeyPacket(),
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new RSASecretBCPGKey(values[0], values[1], values[2])
+ .getEncoded()),
+ pubKey);
+ } else {
+ throw new PGPException("unknown key type: " + keyType);
+ }
+ }
+
+ throw new PGPException("unknown key type found");
+ }
+
+ /**
+ * Parse a secret key from one of the GPG S expression keys.
+ *
+ * @param inputStream
+ * to read from
+ * @param keyProtectionRemoverFactory
+ * for decrypting encrypted keys
+ * @param fingerPrintCalculator
+ * for calculating key fingerprints
+ *
+ * @return a secret key object.
+ * @throws IOException
+ * @throws PGPException
+ */
+ public PGPSecretKey parseSecretKey(InputStream inputStream,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory,
+ KeyFingerPrintCalculator fingerPrintCalculator)
+ throws IOException, PGPException {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String type;
+
+ type = SXprUtils.readString(inputStream, inputStream.read());
+ if (type.equals("protected-private-key")
+ || type.equals("private-key")) {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String keyType = SXprUtils.readString(inputStream,
+ inputStream.read());
+ if (keyType.equals("ecc")) {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String curveID = SXprUtils.readString(inputStream,
+ inputStream.read());
+ String curveName = SXprUtils.readString(inputStream,
+ inputStream.read());
+
+ if (curveName.startsWith("NIST ")) {
+ curveName = curveName.substring("NIST ".length());
+ }
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ byte[] qVal;
+
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ type = SXprUtils.readString(inputStream, inputStream.read());
+ if (type.equals("q")) {
+ qVal = SXprUtils.readBytes(inputStream, inputStream.read());
+ } else {
+ throw new PGPException("no q value found");
+ }
+
+ PublicKeyPacket pubPacket = new PublicKeyPacket(
+ PublicKeyAlgorithmTags.ECDSA, new Date(),
+ new ECDSAPublicBCPGKey(
+ ECNamedCurveTable.getOID(curveName),
+ new BigInteger(1, qVal)));
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ BigInteger d = processECSecretKey(inputStream, curveID,
+ curveName, qVal, keyProtectionRemoverFactory);
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubPacket,
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new ECSecretBCPGKey(d).getEncoded()),
+ new PGPPublicKey(pubPacket, fingerPrintCalculator));
+ } else if (keyType.equals("dsa")) {
+ BigInteger p = readBigInteger("p", inputStream);
+ BigInteger q = readBigInteger("q", inputStream);
+ BigInteger g = readBigInteger("g", inputStream);
+
+ BigInteger y = readBigInteger("y", inputStream);
+
+ BigInteger x = processDSASecretKey(inputStream, p, q, g, y,
+ keyProtectionRemoverFactory);
+
+ PublicKeyPacket pubPacket = new PublicKeyPacket(
+ PublicKeyAlgorithmTags.DSA, new Date(),
+ new DSAPublicBCPGKey(p, q, g, y));
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubPacket,
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new DSASecretBCPGKey(x).getEncoded()),
+ new PGPPublicKey(pubPacket, fingerPrintCalculator));
+ } else if (keyType.equals("elg")) {
+ BigInteger p = readBigInteger("p", inputStream);
+ BigInteger g = readBigInteger("g", inputStream);
+
+ BigInteger y = readBigInteger("y", inputStream);
+
+ BigInteger x = processElGamalSecretKey(inputStream, p, g, y,
+ keyProtectionRemoverFactory);
+
+ PublicKeyPacket pubPacket = new PublicKeyPacket(
+ PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, new Date(),
+ new ElGamalPublicBCPGKey(p, g, y));
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubPacket,
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new ElGamalSecretBCPGKey(x).getEncoded()),
+ new PGPPublicKey(pubPacket, fingerPrintCalculator));
+ } else if (keyType.equals("rsa")) {
+ BigInteger n = readBigInteger("n", inputStream);
+ BigInteger e = readBigInteger("e", inputStream);
+
+ BigInteger[] values = processRSASecretKey(inputStream, n, e,
+ keyProtectionRemoverFactory);
+
+ // TODO: type of RSA key?
+ PublicKeyPacket pubPacket = new PublicKeyPacket(
+ PublicKeyAlgorithmTags.RSA_GENERAL, new Date(),
+ new RSAPublicBCPGKey(n, e));
+
+ return new PGPSecretKey(
+ new SecretKeyPacket(pubPacket,
+ SymmetricKeyAlgorithmTags.NULL, null, null,
+ new RSASecretBCPGKey(values[0], values[1],
+ values[2]).getEncoded()),
+ new PGPPublicKey(pubPacket, fingerPrintCalculator));
+ } else {
+ throw new PGPException("unknown key type: " + keyType);
+ }
+ }
+
+ throw new PGPException("unknown key type found");
+ }
+
+ private BigInteger readBigInteger(String expectedType,
+ InputStream inputStream) throws IOException, PGPException {
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String type = SXprUtils.readString(inputStream, inputStream.read());
+ if (!type.equals(expectedType)) {
+ throw new PGPException(expectedType + " value expected");
+ }
+
+ byte[] nBytes = SXprUtils.readBytes(inputStream, inputStream.read());
+ BigInteger v = new BigInteger(1, nBytes);
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ return v;
+ }
+
+ private static byte[][] extractData(InputStream inputStream,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+ throws PGPException, IOException {
+ byte[] data;
+ byte[] protectedAt = null;
+
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ String type = SXprUtils.readString(inputStream, inputStream.read());
+ if (type.equals("protected")) {
+ String protection = SXprUtils.readString(inputStream,
+ inputStream.read());
+
+ SXprUtils.skipOpenParenthesis(inputStream);
+
+ S2K s2k = SXprUtils.parseS2K(inputStream);
+
+ byte[] iv = SXprUtils.readBytes(inputStream, inputStream.read());
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ byte[] secKeyData = SXprUtils.readBytes(inputStream,
+ inputStream.read());
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ PBESecretKeyDecryptor keyDecryptor = keyProtectionRemoverFactory
+ .createDecryptor(protection);
+
+ // TODO: recognise other algorithms
+ byte[] key = keyDecryptor.makeKeyFromPassPhrase(
+ SymmetricKeyAlgorithmTags.AES_128, s2k);
+
+ data = keyDecryptor.recoverKeyData(
+ SymmetricKeyAlgorithmTags.AES_128, key, iv, secKeyData, 0,
+ secKeyData.length);
+
+ // check if protected at is present
+ if (inputStream.read() == '(') {
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream();
+
+ bOut.write('(');
+ int ch;
+ while ((ch = inputStream.read()) >= 0 && ch != ')') {
+ bOut.write(ch);
+ }
+
+ if (ch != ')') {
+ throw new IOException("unexpected end to SExpr");
+ }
+
+ bOut.write(')');
+
+ protectedAt = bOut.toByteArray();
+ }
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+ SXprUtils.skipCloseParenthesis(inputStream);
+ } else if (type.equals("d") || type.equals("x")) {
+ // JGit modification: unencrypted DSA or ECC keys can have an "x"
+ // here
+ return null;
+ } else {
+ throw new PGPException("protected block not found");
+ }
+
+ return new byte[][] { data, protectedAt };
+ }
+
+ private BigInteger processDSASecretKey(InputStream inputStream,
+ BigInteger p, BigInteger q, BigInteger g, BigInteger y,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+ throws IOException, PGPException {
+ String type;
+ byte[][] basicData = extractData(inputStream,
+ keyProtectionRemoverFactory);
+
+ // JGit modification: handle unencrypted DSA keys
+ if (basicData == null) {
+ byte[] nBytes = SXprUtils.readBytes(inputStream,
+ inputStream.read());
+ BigInteger x = new BigInteger(1, nBytes);
+ SXprUtils.skipCloseParenthesis(inputStream);
+ return x;
+ }
+
+ byte[] keyData = basicData[0];
+ byte[] protectedAt = basicData[1];
+
+ //
+ // parse the secret key S-expr
+ //
+ InputStream keyIn = new ByteArrayInputStream(keyData);
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ SXprUtils.skipOpenParenthesis(keyIn);
+
+ BigInteger x = readBigInteger("x", keyIn);
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ // JGit modification: OCB-encrypted keys don't have and don't need a
+ // hash
+ if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+ return x;
+ }
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("hash")) {
+ throw new PGPException("hash keyword expected");
+ }
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("sha1")) {
+ throw new PGPException("hash keyword expected");
+ }
+
+ byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ if (digestProvider != null) {
+ PGPDigestCalculator digestCalculator = digestProvider
+ .get(HashAlgorithmTags.SHA1);
+
+ OutputStream dOut = digestCalculator.getOutputStream();
+
+ dOut.write(Strings.toByteArray("(3:dsa"));
+ writeCanonical(dOut, "p", p);
+ writeCanonical(dOut, "q", q);
+ writeCanonical(dOut, "g", g);
+ writeCanonical(dOut, "y", y);
+ writeCanonical(dOut, "x", x);
+
+ // check protected-at
+ if (protectedAt != null) {
+ dOut.write(protectedAt);
+ }
+
+ dOut.write(Strings.toByteArray(")"));
+
+ byte[] check = digestCalculator.getDigest();
+ if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+ throw new PGPException(
+ "checksum on protected data failed in SExpr");
+ }
+ }
+
+ return x;
+ }
+
+ private BigInteger processElGamalSecretKey(InputStream inputStream,
+ BigInteger p, BigInteger g, BigInteger y,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+ throws IOException, PGPException {
+ String type;
+ byte[][] basicData = extractData(inputStream,
+ keyProtectionRemoverFactory);
+
+ // JGit modification: handle unencrypted EC keys
+ if (basicData == null) {
+ byte[] nBytes = SXprUtils.readBytes(inputStream,
+ inputStream.read());
+ BigInteger x = new BigInteger(1, nBytes);
+ SXprUtils.skipCloseParenthesis(inputStream);
+ return x;
+ }
+
+ byte[] keyData = basicData[0];
+ byte[] protectedAt = basicData[1];
+
+ //
+ // parse the secret key S-expr
+ //
+ InputStream keyIn = new ByteArrayInputStream(keyData);
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ SXprUtils.skipOpenParenthesis(keyIn);
+
+ BigInteger x = readBigInteger("x", keyIn);
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ // JGit modification: OCB-encrypted keys don't have and don't need a
+ // hash
+ if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+ return x;
+ }
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("hash")) {
+ throw new PGPException("hash keyword expected");
+ }
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("sha1")) {
+ throw new PGPException("hash keyword expected");
+ }
+
+ byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ if (digestProvider != null) {
+ PGPDigestCalculator digestCalculator = digestProvider
+ .get(HashAlgorithmTags.SHA1);
+
+ OutputStream dOut = digestCalculator.getOutputStream();
+
+ dOut.write(Strings.toByteArray("(3:elg"));
+ writeCanonical(dOut, "p", p);
+ writeCanonical(dOut, "g", g);
+ writeCanonical(dOut, "y", y);
+ writeCanonical(dOut, "x", x);
+
+ // check protected-at
+ if (protectedAt != null) {
+ dOut.write(protectedAt);
+ }
+
+ dOut.write(Strings.toByteArray(")"));
+
+ byte[] check = digestCalculator.getDigest();
+ if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+ throw new PGPException(
+ "checksum on protected data failed in SExpr");
+ }
+ }
+
+ return x;
+ }
+
+ private BigInteger processECSecretKey(InputStream inputStream,
+ String curveID, String curveName, byte[] qVal,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+ throws IOException, PGPException {
+ String type;
+
+ byte[][] basicData = extractData(inputStream,
+ keyProtectionRemoverFactory);
+
+ // JGit modification: handle unencrypted EC keys
+ if (basicData == null) {
+ byte[] nBytes = SXprUtils.readBytes(inputStream,
+ inputStream.read());
+ BigInteger d = new BigInteger(1, nBytes);
+ SXprUtils.skipCloseParenthesis(inputStream);
+ return d;
+ }
+
+ byte[] keyData = basicData[0];
+ byte[] protectedAt = basicData[1];
+
+ //
+ // parse the secret key S-expr
+ //
+ InputStream keyIn = new ByteArrayInputStream(keyData);
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ SXprUtils.skipOpenParenthesis(keyIn);
+ BigInteger d = readBigInteger("d", keyIn);
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ // JGit modification: OCB-encrypted keys don't have and don't need a
+ // hash
+ if (keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+ return d;
+ }
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("hash")) {
+ throw new PGPException("hash keyword expected");
+ }
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("sha1")) {
+ throw new PGPException("hash keyword expected");
+ }
+
+ byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ if (digestProvider != null) {
+ PGPDigestCalculator digestCalculator = digestProvider
+ .get(HashAlgorithmTags.SHA1);
+
+ OutputStream dOut = digestCalculator.getOutputStream();
+
+ dOut.write(Strings.toByteArray("(3:ecc"));
+
+ dOut.write(Strings.toByteArray("(" + curveID.length() + ":"
+ + curveID + curveName.length() + ":" + curveName + ")"));
+
+ writeCanonical(dOut, "q", qVal);
+ writeCanonical(dOut, "d", d);
+
+ // check protected-at
+ if (protectedAt != null) {
+ dOut.write(protectedAt);
+ }
+
+ dOut.write(Strings.toByteArray(")"));
+
+ byte[] check = digestCalculator.getDigest();
+
+ if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+ throw new PGPException(
+ "checksum on protected data failed in SExpr");
+ }
+ }
+
+ return d;
+ }
+
+ private BigInteger[] processRSASecretKey(InputStream inputStream,
+ BigInteger n, BigInteger e,
+ PBEProtectionRemoverFactory keyProtectionRemoverFactory)
+ throws IOException, PGPException {
+ String type;
+ byte[][] basicData = extractData(inputStream,
+ keyProtectionRemoverFactory);
+
+ byte[] keyData;
+ byte[] protectedAt = null;
+
+ InputStream keyIn;
+ BigInteger d;
+
+ if (basicData == null) {
+ keyIn = inputStream;
+ byte[] nBytes = SXprUtils.readBytes(inputStream,
+ inputStream.read());
+ d = new BigInteger(1, nBytes);
+
+ SXprUtils.skipCloseParenthesis(inputStream);
+
+ } else {
+ keyData = basicData[0];
+ protectedAt = basicData[1];
+
+ keyIn = new ByteArrayInputStream(keyData);
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ SXprUtils.skipOpenParenthesis(keyIn);
+ d = readBigInteger("d", keyIn);
+ }
+
+ //
+ // parse the secret key S-expr
+ //
+
+ BigInteger p = readBigInteger("p", keyIn);
+ BigInteger q = readBigInteger("q", keyIn);
+ BigInteger u = readBigInteger("u", keyIn);
+
+ // JGit modification: OCB-encrypted keys don't have and don't need a
+ // hash
+ if (basicData == null
+ || keyProtectionRemoverFactory instanceof OCBPBEProtectionRemoverFactory) {
+ return new BigInteger[] { d, p, q, u };
+ }
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ SXprUtils.skipOpenParenthesis(keyIn);
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("hash")) {
+ throw new PGPException("hash keyword expected");
+ }
+ type = SXprUtils.readString(keyIn, keyIn.read());
+
+ if (!type.equals("sha1")) {
+ throw new PGPException("hash keyword expected");
+ }
+
+ byte[] hashBytes = SXprUtils.readBytes(keyIn, keyIn.read());
+
+ SXprUtils.skipCloseParenthesis(keyIn);
+
+ if (digestProvider != null) {
+ PGPDigestCalculator digestCalculator = digestProvider
+ .get(HashAlgorithmTags.SHA1);
+
+ OutputStream dOut = digestCalculator.getOutputStream();
+
+ dOut.write(Strings.toByteArray("(3:rsa"));
+
+ writeCanonical(dOut, "n", n);
+ writeCanonical(dOut, "e", e);
+ writeCanonical(dOut, "d", d);
+ writeCanonical(dOut, "p", p);
+ writeCanonical(dOut, "q", q);
+ writeCanonical(dOut, "u", u);
+
+ // check protected-at
+ if (protectedAt != null) {
+ dOut.write(protectedAt);
+ }
+
+ dOut.write(Strings.toByteArray(")"));
+
+ byte[] check = digestCalculator.getDigest();
+
+ if (!Arrays.constantTimeAreEqual(check, hashBytes)) {
+ throw new PGPException(
+ "checksum on protected data failed in SExpr");
+ }
+ }
+
+ return new BigInteger[] { d, p, q, u };
+ }
+
+ private void writeCanonical(OutputStream dOut, String label, BigInteger i)
+ throws IOException {
+ writeCanonical(dOut, label, i.toByteArray());
+ }
+
+ private void writeCanonical(OutputStream dOut, String label, byte[] data)
+ throws IOException {
+ dOut.write(Strings.toByteArray(
+ "(" + label.length() + ":" + label + data.length + ":"));
+ dOut.write(data);
+ dOut.write(Strings.toByteArray(")"));
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
new file mode 100644
index 0000000000..220aa285ff
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SXprUtils.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
+ * <p>
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without restriction,
+ *including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ * </p>
+ * <p>
+ * The above copyright notice and this permission notice shall be included in all copies or substantial
+ * portions of the Software.
+ * </p>
+ * <p>
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+ * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ * PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ * </p>
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+// This class is an unmodified copy from Bouncy Castle; needed because it's package-visible only and used by SExprParser.
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.bouncycastle.bcpg.HashAlgorithmTags;
+import org.bouncycastle.bcpg.S2K;
+import org.bouncycastle.util.io.Streams;
+
+/**
+ * Utility functions for looking a S-expression keys. This class will move when
+ * it finds a better home!
+ * <p>
+ * Format documented here:
+ * http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=agent/keyformat.txt;h=42c4b1f06faf1bbe71ffadc2fee0fad6bec91a97;hb=refs/heads/master
+ * </p>
+ */
+class SXprUtils {
+ private static int readLength(InputStream in, int ch) throws IOException {
+ int len = ch - '0';
+
+ while ((ch = in.read()) >= 0 && ch != ':') {
+ len = len * 10 + ch - '0';
+ }
+
+ return len;
+ }
+
+ static String readString(InputStream in, int ch) throws IOException {
+ int len = readLength(in, ch);
+
+ char[] chars = new char[len];
+
+ for (int i = 0; i != chars.length; i++) {
+ chars[i] = (char) in.read();
+ }
+
+ return new String(chars);
+ }
+
+ static byte[] readBytes(InputStream in, int ch) throws IOException {
+ int len = readLength(in, ch);
+
+ byte[] data = new byte[len];
+
+ Streams.readFully(in, data);
+
+ return data;
+ }
+
+ static S2K parseS2K(InputStream in) throws IOException {
+ skipOpenParenthesis(in);
+
+ // Algorithm is hard-coded to SHA1 below anyway.
+ readString(in, in.read());
+ byte[] iv = readBytes(in, in.read());
+ final long iterationCount = Long.parseLong(readString(in, in.read()));
+
+ skipCloseParenthesis(in);
+
+ // we have to return the actual iteration count provided.
+ S2K s2k = new S2K(HashAlgorithmTags.SHA1, iv, (int) iterationCount) {
+ @Override
+ public long getIterationCount() {
+ return iterationCount;
+ }
+ };
+
+ return s2k;
+ }
+
+ static void skipOpenParenthesis(InputStream in) throws IOException {
+ int ch = in.read();
+ if (ch != '(') {
+ throw new IOException(
+ "unknown character encountered: " + (char) ch); //$NON-NLS-1$
+ }
+ }
+
+ static void skipCloseParenthesis(InputStream in) throws IOException {
+ int ch = in.read();
+ if (ch != ')') {
+ throw new IOException("unknown character encountered"); //$NON-NLS-1$
+ }
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
new file mode 100644
index 0000000000..1542b8cbcc
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/SecretKeys.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.gpg.bc.internal.keys;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StreamCorruptedException;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
+import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
+import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
+import org.bouncycastle.util.io.Streams;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Utilities for reading GPG secret keys from a gpg-agent key file.
+ */
+public final class SecretKeys {
+
+ private SecretKeys() {
+ // No instantiation.
+ }
+
+ /**
+ * Something that can supply a passphrase to decrypt an encrypted secret
+ * key.
+ */
+ public interface PassphraseSupplier {
+
+ /**
+ * Supplies a passphrase.
+ *
+ * @return the passphrase
+ * @throws PGPException
+ * if no passphrase can be obtained
+ * @throws CanceledException
+ * if the user canceled passphrase entry
+ * @throws UnsupportedCredentialItem
+ * if an internal error occurred
+ * @throws URISyntaxException
+ * if an internal error occurred
+ */
+ char[] getPassphrase() throws PGPException, CanceledException,
+ UnsupportedCredentialItem, URISyntaxException;
+ }
+
+ private static final byte[] PROTECTED_KEY = "protected-private-key" //$NON-NLS-1$
+ .getBytes(StandardCharsets.US_ASCII);
+
+ private static final byte[] OCB_PROTECTED = "openpgp-s2k3-ocb-aes" //$NON-NLS-1$
+ .getBytes(StandardCharsets.US_ASCII);
+
+ /**
+ * Reads a GPG secret key from the given stream.
+ *
+ * @param in
+ * {@link InputStream} to read from, doesn't need to be buffered
+ * @param calculatorProvider
+ * for checking digests
+ * @param passphraseSupplier
+ * for decrypting encrypted keys
+ * @param publicKey
+ * the secret key should be for
+ * @return the secret key
+ * @throws IOException
+ * if the stream cannot be parsed
+ * @throws PGPException
+ * if thrown by the underlying S-Expression parser, for instance
+ * when the passphrase is wrong
+ * @throws CanceledException
+ * if thrown by the {@code passphraseSupplier}
+ * @throws UnsupportedCredentialItem
+ * if thrown by the {@code passphraseSupplier}
+ * @throws URISyntaxException
+ * if thrown by the {@code passphraseSupplier}
+ */
+ public static PGPSecretKey readSecretKey(InputStream in,
+ PGPDigestCalculatorProvider calculatorProvider,
+ PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey)
+ throws IOException, PGPException, CanceledException,
+ UnsupportedCredentialItem, URISyntaxException {
+ byte[] data = Streams.readAll(in);
+ if (data.length == 0) {
+ throw new EOFException();
+ } else if (data.length < 4 + PROTECTED_KEY.length) {
+ // +4 for "(21:" for a binary protected key
+ throw new IOException(
+ MessageFormat.format(BCText.get().secretKeyTooShort,
+ Integer.toUnsignedString(data.length)));
+ }
+ SExprParser parser = new SExprParser(calculatorProvider);
+ byte firstChar = data[0];
+ try {
+ if (firstChar == '(') {
+ // Binary format.
+ if (!matches(data, 4, PROTECTED_KEY)) {
+ // Not encrypted binary format.
+ return parser.parseSecretKey(in, null, publicKey);
+ }
+ // AES/CBC encrypted.
+ PBEProtectionRemoverFactory decryptor = new JcePBEProtectionRemoverFactory(
+ passphraseSupplier.getPassphrase(), calculatorProvider);
+ try (InputStream sIn = new ByteArrayInputStream(data)) {
+ return parser.parseSecretKey(sIn, decryptor, publicKey);
+ }
+ }
+ // Assume it's the new key-value format.
+ try (ByteArrayInputStream keyIn = new ByteArrayInputStream(data)) {
+ byte[] rawData = keyFromNameValueFormat(keyIn);
+ if (!matches(rawData, 1, PROTECTED_KEY)) {
+ // Not encrypted human-readable format.
+ try (InputStream sIn = new ByteArrayInputStream(
+ convertSexpression(rawData))) {
+ return parser.parseSecretKey(sIn, null, publicKey);
+ }
+ }
+ // An encrypted key from a key-value file. Most likely AES/OCB
+ // encrypted.
+ boolean isOCB[] = { false };
+ byte[] sExp = convertSexpression(rawData, isOCB);
+ PBEProtectionRemoverFactory decryptor;
+ if (isOCB[0]) {
+ decryptor = new OCBPBEProtectionRemoverFactory(
+ passphraseSupplier.getPassphrase(),
+ calculatorProvider, getAad(sExp));
+ } else {
+ decryptor = new JcePBEProtectionRemoverFactory(
+ passphraseSupplier.getPassphrase(),
+ calculatorProvider);
+ }
+ try (InputStream sIn = new ByteArrayInputStream(sExp)) {
+ return parser.parseSecretKey(sIn, decryptor, publicKey);
+ }
+ }
+ } catch (IOException e) {
+ throw new PGPException(e.getLocalizedMessage(), e);
+ }
+ }
+
+ /**
+ * Extract the AAD for the OCB decryption from an s-expression.
+ *
+ * @param sExp
+ * buffer containing a valid binary s-expression
+ * @return the AAD
+ */
+ private static byte[] getAad(byte[] sExp) {
+ // Given a key
+ // @formatter:off
+ // (protected-private-key (rsa ... (protected openpgp-s2k3-ocb-aes ... )(protected-at ...)))
+ // A B C D
+ // The AAD is [A..B)[C..D). (From the binary serialized form.)
+ // @formatter:on
+ int i = 1; // Skip initial '('
+ while (sExp[i] != '(') {
+ i++;
+ }
+ int aadStart = i++;
+ int aadEnd = skip(sExp, aadStart);
+ byte[] protectedPrefix = "(9:protected" //$NON-NLS-1$
+ .getBytes(StandardCharsets.US_ASCII);
+ while (!matches(sExp, i, protectedPrefix)) {
+ i++;
+ }
+ int protectedStart = i;
+ int protectedEnd = skip(sExp, protectedStart);
+ byte[] aadData = new byte[aadEnd - aadStart
+ - (protectedEnd - protectedStart)];
+ System.arraycopy(sExp, aadStart, aadData, 0, protectedStart - aadStart);
+ System.arraycopy(sExp, protectedEnd, aadData, protectedStart - aadStart,
+ aadEnd - protectedEnd);
+ return aadData;
+ }
+
+ /**
+ * Skips a list including nested lists.
+ *
+ * @param sExp
+ * buffer containing valid binary s-expression data
+ * @param start
+ * index of the opening '(' of the list to skip
+ * @return the index after the closing ')' of the skipped list
+ */
+ private static int skip(byte[] sExp, int start) {
+ int i = start + 1;
+ int depth = 1;
+ while (depth > 0) {
+ switch (sExp[i]) {
+ case '(':
+ depth++;
+ break;
+ case ')':
+ depth--;
+ break;
+ default:
+ // We must be on a length
+ int j = i;
+ while (sExp[j] >= '0' && sExp[j] <= '9') {
+ j++;
+ }
+ // j is on the colon
+ int length = Integer.parseInt(
+ new String(sExp, i, j - i, StandardCharsets.US_ASCII));
+ i = j + length;
+ }
+ i++;
+ }
+ return i;
+ }
+
+ /**
+ * Checks whether the {@code needle} matches {@code src} at offset
+ * {@code from}.
+ *
+ * @param src
+ * to match against {@code needle}
+ * @param from
+ * position in {@code src} to start matching
+ * @param needle
+ * to match against
+ * @return {@code true} if {@code src} contains {@code needle} at position
+ * {@code from}, {@code false} otherwise
+ */
+ private static boolean matches(byte[] src, int from, byte[] needle) {
+ if (from < 0 || from + needle.length > src.length) {
+ return false;
+ }
+ return org.bouncycastle.util.Arrays.constantTimeAreEqual(needle.length,
+ src, from, needle, 0);
+ }
+
+ /**
+ * Converts a human-readable serialized s-expression into a binary
+ * serialized s-expression.
+ *
+ * @param humanForm
+ * to convert
+ * @return the converted s-expression
+ * @throws IOException
+ * if the conversion fails
+ */
+ private static byte[] convertSexpression(byte[] humanForm)
+ throws IOException {
+ boolean[] isOCB = { false };
+ return convertSexpression(humanForm, isOCB);
+ }
+
+ /**
+ * Converts a human-readable serialized s-expression into a binary
+ * serialized s-expression.
+ *
+ * @param humanForm
+ * to convert
+ * @param isOCB
+ * returns whether the s-expression specified AES/OCB encryption
+ * @return the converted s-expression
+ * @throws IOException
+ * if the conversion fails
+ */
+ private static byte[] convertSexpression(byte[] humanForm, boolean[] isOCB)
+ throws IOException {
+ int pos = 0;
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream(
+ humanForm.length)) {
+ while (pos < humanForm.length) {
+ byte b = humanForm[pos];
+ if (b == '(' || b == ')') {
+ out.write(b);
+ pos++;
+ } else if (isGpgSpace(b)) {
+ pos++;
+ } else if (b == '#') {
+ // Hex value follows up to the next #
+ int i = ++pos;
+ while (i < humanForm.length && isHex(humanForm[i])) {
+ i++;
+ }
+ if (i == pos || humanForm[i] != '#') {
+ throw new StreamCorruptedException(
+ BCText.get().sexprHexNotClosed);
+ }
+ if ((i - pos) % 2 != 0) {
+ throw new StreamCorruptedException(
+ BCText.get().sexprHexOdd);
+ }
+ int l = (i - pos) / 2;
+ out.write(Integer.toString(l)
+ .getBytes(StandardCharsets.US_ASCII));
+ out.write(':');
+ while (pos < i) {
+ int x = (nibble(humanForm[pos]) << 4)
+ | nibble(humanForm[pos + 1]);
+ pos += 2;
+ out.write(x);
+ }
+ pos = i + 1;
+ } else if (isTokenChar(b)) {
+ // Scan the token
+ int start = pos++;
+ while (pos < humanForm.length
+ && isTokenChar(humanForm[pos])) {
+ pos++;
+ }
+ int l = pos - start;
+ if (pos - start == OCB_PROTECTED.length
+ && matches(humanForm, start, OCB_PROTECTED)) {
+ isOCB[0] = true;
+ }
+ out.write(Integer.toString(l)
+ .getBytes(StandardCharsets.US_ASCII));
+ out.write(':');
+ out.write(humanForm, start, pos - start);
+ } else if (b == '"') {
+ // Potentially quoted string.
+ int start = ++pos;
+ boolean escaped = false;
+ while (pos < humanForm.length
+ && (escaped || humanForm[pos] != '"')) {
+ int ch = humanForm[pos++];
+ escaped = !escaped && ch == '\\';
+ }
+ if (pos >= humanForm.length) {
+ throw new StreamCorruptedException(
+ BCText.get().sexprStringNotClosed);
+ }
+ // start is on the first character of the string, pos on the
+ // closing quote.
+ byte[] dq = dequote(humanForm, start, pos);
+ out.write(Integer.toString(dq.length)
+ .getBytes(StandardCharsets.US_ASCII));
+ out.write(':');
+ out.write(dq);
+ pos++;
+ } else {
+ throw new StreamCorruptedException(
+ MessageFormat.format(BCText.get().sexprUnhandled,
+ Integer.toHexString(b & 0xFF)));
+ }
+ }
+ return out.toByteArray();
+ }
+ }
+
+ /**
+ * GPG-style string de-quoting, which is basically C-style, with some
+ * literal CR/LF escaping.
+ *
+ * @param in
+ * buffer containing the quoted string
+ * @param from
+ * index after the opening quote in {@code in}
+ * @param to
+ * index of the closing quote in {@code in}
+ * @return the dequoted raw string value
+ * @throws StreamCorruptedException
+ */
+ private static byte[] dequote(byte[] in, int from, int to)
+ throws StreamCorruptedException {
+ // Result must be shorter or have the same length
+ byte[] out = new byte[to - from];
+ int j = 0;
+ int i = from;
+ while (i < to) {
+ byte b = in[i++];
+ if (b != '\\') {
+ out[j++] = b;
+ continue;
+ }
+ if (i == to) {
+ throw new StreamCorruptedException(
+ BCText.get().sexprStringInvalidEscapeAtEnd);
+ }
+ b = in[i++];
+ switch (b) {
+ case 'b':
+ out[j++] = '\b';
+ break;
+ case 'f':
+ out[j++] = '\f';
+ break;
+ case 'n':
+ out[j++] = '\n';
+ break;
+ case 'r':
+ out[j++] = '\r';
+ break;
+ case 't':
+ out[j++] = '\t';
+ break;
+ case 'v':
+ out[j++] = 0x0B;
+ break;
+ case '"':
+ case '\'':
+ case '\\':
+ out[j++] = b;
+ break;
+ case '\r':
+ // Escaped literal line end. If an LF is following, skip that,
+ // too.
+ if (i < to && in[i] == '\n') {
+ i++;
+ }
+ break;
+ case '\n':
+ // Same for LF possibly followed by CR.
+ if (i < to && in[i] == '\r') {
+ i++;
+ }
+ break;
+ case 'x':
+ if (i + 1 >= to || !isHex(in[i]) || !isHex(in[i + 1])) {
+ throw new StreamCorruptedException(
+ BCText.get().sexprStringInvalidHexEscape);
+ }
+ out[j++] = (byte) ((nibble(in[i]) << 4) | nibble(in[i + 1]));
+ i += 2;
+ break;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ if (i + 2 >= to || !isOctal(in[i]) || !isOctal(in[i + 1])
+ || !isOctal(in[i + 2])) {
+ throw new StreamCorruptedException(
+ BCText.get().sexprStringInvalidOctalEscape);
+ }
+ out[j++] = (byte) (((((in[i] - '0') << 3)
+ | (in[i + 1] - '0')) << 3) | (in[i + 2] - '0'));
+ i += 3;
+ break;
+ default:
+ throw new StreamCorruptedException(MessageFormat.format(
+ BCText.get().sexprStringInvalidEscape,
+ Integer.toHexString(b & 0xFF)));
+ }
+ }
+ return Arrays.copyOf(out, j);
+ }
+
+ /**
+ * Extracts the key from a GPG name-value-pair key file.
+ * <p>
+ * Package-visible for tests only.
+ * </p>
+ *
+ * @param in
+ * {@link InputStream} to read from; should be buffered
+ * @return the raw key data as extracted from the file
+ * @throws IOException
+ * if the {@code in} stream cannot be read or does not contain a
+ * key
+ */
+ static byte[] keyFromNameValueFormat(InputStream in) throws IOException {
+ // It would be nice if we could use RawParseUtils here, but GPG compares
+ // names case-insensitively. We're only interested in the "Key:"
+ // name-value pair.
+ int[] nameLow = { 'k', 'e', 'y', ':' };
+ int[] nameCap = { 'K', 'E', 'Y', ':' };
+ int nameIdx = 0;
+ for (;;) {
+ int next = in.read();
+ if (next < 0) {
+ throw new EOFException();
+ }
+ if (next == '\n') {
+ nameIdx = 0;
+ } else if (nameIdx >= 0) {
+ if (nameLow[nameIdx] == next || nameCap[nameIdx] == next) {
+ nameIdx++;
+ if (nameIdx == nameLow.length) {
+ break;
+ }
+ } else {
+ nameIdx = -1;
+ }
+ }
+ }
+ // We're after "Key:". Read the value as continuation lines.
+ int last = ':';
+ byte[] rawData;
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream(8192)) {
+ for (;;) {
+ int next = in.read();
+ if (next < 0) {
+ break;
+ }
+ if (last == '\n') {
+ if (next == ' ' || next == '\t') {
+ // Continuation line; skip this whitespace
+ last = next;
+ continue;
+ }
+ break; // Not a continuation line
+ }
+ out.write(next);
+ last = next;
+ }
+ rawData = out.toByteArray();
+ }
+ // GPG trims off trailing whitespace, and a line having only whitespace
+ // is a single LF.
+ try (ByteArrayOutputStream out = new ByteArrayOutputStream(
+ rawData.length)) {
+ int lineStart = 0;
+ boolean trimLeading = true;
+ while (lineStart < rawData.length) {
+ int nextLineStart = RawParseUtils.nextLF(rawData, lineStart);
+ if (trimLeading) {
+ while (lineStart < nextLineStart
+ && isGpgSpace(rawData[lineStart])) {
+ lineStart++;
+ }
+ }
+ // Trim trailing
+ int i = nextLineStart - 1;
+ while (lineStart < i && isGpgSpace(rawData[i])) {
+ i--;
+ }
+ if (i <= lineStart) {
+ // Empty line signifies LF
+ out.write('\n');
+ trimLeading = true;
+ } else {
+ out.write(rawData, lineStart, i - lineStart + 1);
+ trimLeading = false;
+ }
+ lineStart = nextLineStart;
+ }
+ return out.toByteArray();
+ }
+ }
+
+ private static boolean isGpgSpace(int ch) {
+ return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
+ }
+
+ private static boolean isTokenChar(int ch) {
+ switch (ch) {
+ case '-':
+ case '.':
+ case '/':
+ case '_':
+ case ':':
+ case '*':
+ case '+':
+ case '=':
+ return true;
+ default:
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
+ || (ch >= '0' && ch <= '9')) {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private static boolean isHex(int ch) {
+ return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
+ || (ch >= 'a' && ch <= 'f');
+ }
+
+ private static boolean isOctal(int ch) {
+ return (ch >= '0' && ch <= '7');
+ }
+
+ private static int nibble(int ch) {
+ if (ch >= '0' && ch <= '9') {
+ return ch - '0';
+ } else if (ch >= 'A' && ch <= 'F') {
+ return ch - 'A' + 10;
+ } else if (ch >= 'a' && ch <= 'f') {
+ return ch - 'a' + 10;
+ }
+ return -1;
+ }
+}