summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.gpg.bc
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2021-02-25 10:29:07 +0100
committerMatthias Sohn <matthias.sohn@sap.com>2021-02-28 00:58:04 +0100
commitf6597971991e3350df568b0cde05c014dcd69c47 (patch)
treecb61592af3f53da45174beed517b3284d7bd55c6 /org.eclipse.jgit.gpg.bc
parent286ad23cb56ffeac77d4bfd03be575358fd5217c (diff)
parent789c0479a9294417db0375cce9f1949fe9052d8c (diff)
downloadjgit-f6597971991e3350df568b0cde05c014dcd69c47.tar.gz
jgit-f6597971991e3350df568b0cde05c014dcd69c47.zip
Merge branch 'master' into next
* master: (143 commits) Prepare 5.11.0-SNAPSHOT builds JGit v5.11.0.202102240950-m3 [releng] japicmp: update last release version IgnoreNode: include path to file for invalid .gitignore patterns FastIgnoreRule: include bad pattern in log message init: add config option to set default for the initial branch name init: allow specifying the initial branch name for the new repository Fail clone if initial branch doesn't exist in remote repository GPG: fix reading unprotected old-format secret keys Update Orbit to S20210216215844 Add missing bazel dependency for o.e.j.gpg.bc.test GPG: handle extended private key format dfs: handle short copies [GPG] Provide a factory for the BouncyCastleGpgSigner Fix boxing warnings GPG: compute the keygrip to find a secret key GPG signature verification via BouncyCastle Post commit hook failure should not cause commit failure Allow to define additional Hook classes outside JGit GitHook: use default charset for output and error streams ... Change-Id: I689f4070e79f4a0ac1c02b35698ccaab68ad2f34
Diffstat (limited to 'org.eclipse.jgit.gpg.bc')
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF24
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.gpg.bc/about.html83
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory1
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties27
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java34
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java34
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java224
-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/BouncyCastleGpgSignatureVerifier.java388
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java28
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java62
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java322
-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
17 files changed, 2723 insertions, 166 deletions
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index b59a850ea3..48aad320dc 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -8,22 +8,30 @@ Bundle-Vendor: %Bundle-Vendor
Bundle-Localization: plugin
Bundle-Version: 6.0.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
+Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
+ org.bouncycastle.asn1.cryptlib;version="[1.65.0,2.0.0)",
+ org.bouncycastle.asn1.x9;version="[1.65.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.65.0,2.0.0)",
+ org.bouncycastle.crypto.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg;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)",
org.bouncycastle.openpgp;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="[6.0.0,6.1.0)",
org.eclipse.jgit.api.errors;version="[6.0.0,6.1.0)",
- org.eclipse.jgit.errors;version="[6.0.0,6.1.0)",
- org.eclipse.jgit.lib;version="[6.0.0,6.1.0)",
- org.eclipse.jgit.nls;version="[6.0.0,6.1.0)",
- org.eclipse.jgit.transport;version="[6.0.0,6.1.0)",
- org.eclipse.jgit.util;version="[6.0.0,6.1.0)",
org.slf4j;version="[1.7.0,2.0.0)"
-Export-Package: org.eclipse.jgit.gpg.bc.internal;version="6.0.0";
- x-friends:="org.eclipse.jgit.gpg.bc.test"
+Export-Package: org.eclipse.jgit.gpg.bc;version="6.0.0",
+ org.eclipse.jgit.gpg.bc.internal;version="6.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
+ org.eclipse.jgit.gpg.bc.internal.keys;version="6.0.0";x-friends:="org.eclipse.jgit.gpg.bc.test"
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
index 9e0d921ea2..58bd8ea041 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/SOURCE-MANIFEST.MF
@@ -5,3 +5,4 @@ Bundle-SymbolicName: org.eclipse.jgit.gpg.bc.source
Bundle-Vendor: Eclipse.org - JGit
Bundle-Version: 6.0.0.qualifier
Eclipse-SourceBundle: org.eclipse.jgit.gpg.bc;version="6.0.0.qualifier";roots="."
+
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/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
new file mode 100644
index 0000000000..17ab30fba7
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/resources/META-INF/services/org.eclipse.jgit.lib.GpgSignatureVerifierFactory
@@ -0,0 +1 @@
+org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSignatureVerifierFactory \ No newline at end of file
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 1441c63e8e..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,11 +1,36 @@
+corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
-gpgFailedToParseSecretKey=Failed to parse secret key file in directory: {0}. Is the entered passphrase correct?
+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
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
+signatureNoPublicKey=No public key found to verify the signature
+signatureParseError=Signature cannot be parsed
+signatureVerificationError=Signature verification failed
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
+uncompressed25519Key=Cannot handle ed25519 public key with uncompressed data: {0}
+unknownCurve=Unknown curve {0}
+unknownCurveParameters=Curve {0} does not have a prime field
+unknownKeyType=Unknown key type {0} \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
new file mode 100644
index 0000000000..fdd1a2b11a
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/BouncyCastleGpgSignerFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import org.eclipse.jgit.gpg.bc.internal.BouncyCastleGpgSigner;
+import org.eclipse.jgit.lib.GpgSigner;
+
+/**
+ * Factory for creating a {@link GpgSigner} based on Bouncy Castle.
+ *
+ * @since 5.11
+ */
+public final class BouncyCastleGpgSignerFactory {
+
+ private BouncyCastleGpgSignerFactory() {
+ // No instantiation
+ }
+
+ /**
+ * Creates a new {@link GpgSigner}.
+ *
+ * @return the {@link GpgSigner}
+ */
+ public static GpgSigner create() {
+ return new BouncyCastleGpgSigner();
+ }
+}
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 1a00b0fc79..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
@@ -1,3 +1,12 @@
+/*
+ * Copyright (C) 2018, 2021 Salesforce 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;
import org.eclipse.jgit.nls.NLS;
@@ -18,16 +27,41 @@ 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;
/***/ public String gpgNoKeyring;
/***/ 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;
+ /***/ public String signatureNoPublicKey;
+ /***/ public String signatureParseError;
+ /***/ public String signatureVerificationError;
/***/ public String unableToSignCommitNoSecretKey;
+ /***/ public String uncompressed25519Key;
+ /***/ public String unknownCurve;
+ /***/ public String unknownCurveParameters;
+ /***/ public String unknownKeyType;
}
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 eca45072bf..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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2020 Salesforce and others
+ * Copyright (C) 2018, 2021 Salesforce 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
@@ -14,25 +14,22 @@ import static java.nio.file.Files.newInputStream;
import java.io.BufferedInputStream;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.Iterator;
-import java.util.List;
import java.util.Locale;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.KeyBox;
@@ -50,15 +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;
@@ -78,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$
@@ -155,16 +145,13 @@ public class BouncyCastleGpgKeyLocator {
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
- PBEProtectionRemoverFactory passphraseProvider,
- PGPPublicKey publicKey) {
+ 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);
- } catch (IOException | PGPException | ClassCastException e) {
- if (log.isDebugEnabled())
- log.debug("Ignoring unreadable file '{}': {}", keyFile, //$NON-NLS-1$
- e.getMessage(), e);
- return null;
+ return SecretKeys.readSecretKey(in, calculatorProvider,
+ passphraseSupplier, publicKey);
}
}
@@ -219,33 +206,60 @@ public class BouncyCastleGpgKeyLocator {
int stop = toMatch.indexOf('>');
return begin >= 0 && end > begin + 1 && stop > 0
&& userId.substring(begin + 1, end)
- .equals(toMatch.substring(0, stop));
+ .equalsIgnoreCase(toMatch.substring(0, stop));
}
case '@': {
int begin = userId.indexOf('<');
int end = userId.indexOf('>', begin + 1);
return begin >= 0 && end > begin + 1
- && userId.substring(begin + 1, end).contains(toMatch);
+ && containsIgnoreCase(userId.substring(begin + 1, end),
+ toMatch);
}
default:
if (toMatch.trim().isEmpty()) {
return false;
}
- return userId.toLowerCase(Locale.ROOT)
- .contains(toMatch.toLowerCase(Locale.ROOT));
+ return containsIgnoreCase(userId, toMatch);
+ }
+ }
+
+ private static boolean containsIgnoreCase(String a, String b) {
+ int alength = a.length();
+ int blength = b.length();
+ for (int i = 0; i + blength <= alength; i++) {
+ if (a.regionMatches(true, i, b, 0, blength)) {
+ return true;
+ }
}
+ return false;
}
- private String toFingerprint(String keyId) {
+ private static String toFingerprint(String keyId) {
if (keyId.startsWith("0x")) { //$NON-NLS-1$
return keyId.substring(2);
}
return keyId;
}
- private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob)
+ static PGPPublicKey findPublicKey(String fingerprint, String keySpec)
+ throws IOException, PGPException {
+ PGPPublicKey result = findPublicKeyInPubring(USER_PGP_PUBRING_FILE,
+ fingerprint, keySpec);
+ if (result == null && exists(USER_KEYBOX_PATH)) {
+ try {
+ result = findPublicKeyInKeyBox(USER_KEYBOX_PATH, fingerprint,
+ keySpec);
+ } catch (NoSuchAlgorithmException | NoSuchProviderException
+ | IOException | NoOpenPgpKeyException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ return result;
+ }
+
+ private static PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob,
+ String keyId)
throws IOException {
- String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
if (keyId.isEmpty()) {
return null;
}
@@ -259,10 +273,11 @@ public class BouncyCastleGpgKeyLocator {
return null;
}
- private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob)
+ private static PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob,
+ String keySpec)
throws IOException {
for (UserID userID : keyBlob.getUserIds()) {
- if (containsSigningKey(userID.getUserIDAsString(), signingKey)) {
+ if (containsSigningKey(userID.getUserIDAsString(), keySpec)) {
return getSigningPublicKey(keyBlob);
}
}
@@ -274,6 +289,10 @@ public class BouncyCastleGpgKeyLocator {
*
* @param keyboxFile
* the KeyBox file
+ * @param keyId
+ * to look for, may be null
+ * @param keySpec
+ * to look for
* @return publicKey the public key (maybe <code>null</code>)
* @throws IOException
* in case of problems reading the file
@@ -282,19 +301,22 @@ public class BouncyCastleGpgKeyLocator {
* @throws NoOpenPgpKeyException
* if the file does not contain any OpenPGP key
*/
- private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile)
+ private static PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile,
+ String keyId, String keySpec)
throws IOException, NoSuchAlgorithmException,
NoSuchProviderException, NoOpenPgpKeyException {
KeyBox keyBox = readKeyBoxFile(keyboxFile);
+ String id = keyId != null ? keyId
+ : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
boolean hasOpenPgpKey = false;
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) {
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) {
hasOpenPgpKey = true;
- PGPPublicKey key = findPublicKeyByKeyId(keyBlob);
+ PGPPublicKey key = findPublicKeyByKeyId(keyBlob, id);
if (key != null) {
return key;
}
- key = findPublicKeyByUserId(keyBlob);
+ key = findPublicKeyByUserId(keyBlob, keySpec);
if (key != null) {
return key;
}
@@ -338,7 +360,8 @@ public class BouncyCastleGpgKeyLocator {
// pubring.gpg also try secring.gpg to find the secret key.
if (exists(USER_KEYBOX_PATH)) {
try {
- publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH);
+ publicKey = findPublicKeyInKeyBox(USER_KEYBOX_PATH, null,
+ signingKey);
if (publicKey != null) {
key = findSecretKeyForKeyBoxPublicKey(publicKey,
USER_KEYBOX_PATH);
@@ -361,7 +384,8 @@ public class BouncyCastleGpgKeyLocator {
}
}
if (exists(USER_PGP_PUBRING_FILE)) {
- publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE);
+ publicKey = findPublicKeyInPubring(USER_PGP_PUBRING_FILE, null,
+ signingKey);
if (publicKey != null) {
// GPG < 2.1 may have both; the agent using the directory
// and gpg using secring.gpg. GPG >= 2.1 delegates all
@@ -433,67 +457,59 @@ public class BouncyCastleGpgKeyLocator {
PGPPublicKey publicKey, Path userKeyboxPath)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
- /*
- * this is somewhat brute-force but there doesn't seem to be another
- * way; we have to walk all private key files we find and try to open
- * them
- */
-
- PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
- .build();
-
- try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
- List<Path> allPaths = keyFiles.filter(Files::isRegularFile)
- .collect(Collectors.toCollection(ArrayList::new));
- if (allPaths.isEmpty()) {
- return null;
+ byte[] keyGrip = null;
+ try {
+ keyGrip = KeyGrip.getKeyGrip(publicKey);
+ } catch (PGPException e) {
+ throw new PGPException(
+ MessageFormat.format(BCText.get().gpgNoKeygrip,
+ Hex.toHexString(publicKey.getFingerprint())),
+ e);
+ }
+ String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
+ + ".key"; //$NON-NLS-1$
+ Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
+ if (!Files.exists(keyFile)) {
+ return null;
+ }
+ boolean clearPrompt = false;
+ try {
+ PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
+ .build();
+ clearPrompt = true;
+ PGPSecretKey secretKey = null;
+ try {
+ secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
+ () -> passphrasePrompt.getPassphrase(
+ publicKey.getFingerprint(), userKeyboxPath),
+ publicKey);
+ } catch (PGPException e) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().gpgFailedToParseSecretKey,
+ keyFile.toAbsolutePath()), e);
}
- PBEProtectionRemoverFactory passphraseProvider = p -> {
- throw new EncryptedPgpKeyException();
- };
- for (int attempts = 0; attempts < 2; attempts++) {
- // Second pass will traverse only the encrypted keys with a real
- // passphrase provider.
- Iterator<Path> pathIterator = allPaths.iterator();
- while (pathIterator.hasNext()) {
- Path keyFile = pathIterator.next();
- try {
- PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
- calculatorProvider, passphraseProvider,
- publicKey);
- pathIterator.remove();
- if (secretKey != null) {
- if (!secretKey.isSigningKey()) {
- throw new PGPException(MessageFormat.format(
- BCText.get().gpgNotASigningKey,
- signingKey));
- }
- return new BouncyCastleGpgKey(secretKey,
- userKeyboxPath);
- }
- } catch (EncryptedPgpKeyException e) {
- // Ignore; we'll try again.
- }
- }
- if (attempts > 0 || allPaths.isEmpty()) {
- break;
+ if (secretKey != null) {
+ if (!secretKey.isSigningKey()) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().gpgNotASigningKey, signingKey));
}
- // allPaths contains only the encrypted keys now.
- passphraseProvider = new JcePBEProtectionRemoverFactory(
- passphrasePrompt.getPassphrase(
- publicKey.getFingerprint(), userKeyboxPath));
+ clearPrompt = false;
+ return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
}
-
- passphrasePrompt.clear();
return null;
} catch (RuntimeException e) {
- passphrasePrompt.clear();
throw e;
+ } catch (FileNotFoundException | NoSuchFileException e) {
+ clearPrompt = false;
+ return null;
} catch (IOException e) {
- passphrasePrompt.clear();
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
- USER_SECRET_KEY_DIR.toAbsolutePath()), e);
+ keyFile.toAbsolutePath()), e);
+ } finally {
+ if (clearPrompt) {
+ passphrasePrompt.clear();
+ }
}
}
@@ -551,6 +567,11 @@ public class BouncyCastleGpgKeyLocator {
* Return the first public key matching the key id ({@link #signingKey}.
*
* @param pubringFile
+ * to search
+ * @param keyId
+ * to look for, may be null
+ * @param keySpec
+ * to look for
*
* @return the PGP public key, or {@code null} if none found
* @throws IOException
@@ -558,14 +579,16 @@ public class BouncyCastleGpgKeyLocator {
* @throws PGPException
* on BouncyCastle errors
*/
- private PGPPublicKey findPublicKeyInPubring(Path pubringFile)
+ private static PGPPublicKey findPublicKeyInPubring(Path pubringFile,
+ String keyId, String keySpec)
throws IOException, PGPException {
try (InputStream in = newInputStream(pubringFile)) {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
new BufferedInputStream(in),
new JcaKeyFingerprintCalculator());
- String keyId = toFingerprint(signingKey).toLowerCase(Locale.ROOT);
+ String id = keyId != null ? keyId
+ : toFingerprint(keySpec).toLowerCase(Locale.ROOT);
Iterator<PGPPublicKeyRing> keyrings = pgpPub.getKeyRings();
while (keyrings.hasNext()) {
PGPPublicKeyRing keyRing = keyrings.next();
@@ -575,30 +598,33 @@ public class BouncyCastleGpgKeyLocator {
// try key id
String fingerprint = Hex.toHexString(key.getFingerprint())
.toLowerCase(Locale.ROOT);
- if (fingerprint.endsWith(keyId)) {
+ if (fingerprint.endsWith(id)) {
return key;
}
// try user id
Iterator<String> userIDs = key.getUserIDs();
while (userIDs.hasNext()) {
String userId = userIDs.next();
- if (containsSigningKey(userId, signingKey)) {
+ if (containsSigningKey(userId, keySpec)) {
return key;
}
}
}
}
+ } catch (FileNotFoundException | NoSuchFileException e) {
+ // Ignore and return null
}
return null;
}
- private PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
+ private static PGPPublicKey getPublicKey(KeyBlob blob, byte[] fingerprint)
throws IOException {
return ((PublicKeyRingBlob) blob).getPGPPublicKeyRing()
.getPublicKey(fingerprint);
}
- private PGPPublicKey getSigningPublicKey(KeyBlob blob) throws IOException {
+ private static PGPPublicKey getSigningPublicKey(KeyBlob blob)
+ throws IOException {
PGPPublicKey masterKey = null;
Iterator<PGPPublicKey> keys = ((PublicKeyRingBlob) blob)
.getPGPPublicKeyRing().getPublicKeys();
@@ -618,7 +644,7 @@ public class BouncyCastleGpgKeyLocator {
return masterKey;
}
- private boolean isSigningKey(PGPPublicKey key) {
+ private static boolean isSigningKey(PGPPublicKey key) {
Iterator signatures = key.getSignatures();
while (signatures.hasNext()) {
PGPSignature sig = (PGPSignature) signatures.next();
@@ -630,7 +656,7 @@ public class BouncyCastleGpgKeyLocator {
return false;
}
- private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
+ private static KeyBox readKeyBoxFile(Path keyboxFile) throws IOException,
NoSuchAlgorithmException, NoSuchProviderException,
NoOpenPgpKeyException {
if (keyboxFile.toFile().length() == 0) {
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/BouncyCastleGpgSignatureVerifier.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
new file mode 100644
index 0000000000..7161895a6b
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifier.java
@@ -0,0 +1,388 @@
+/*
+ * 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;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.Security;
+import java.text.MessageFormat;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Locale;
+
+import org.bouncycastle.bcpg.sig.IssuerFingerprint;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.PGPSignatureList;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
+import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.util.LRUMap;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
+ */
+public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier {
+
+ private static void registerBouncyCastleProviderIfNecessary() {
+ if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+ }
+
+ /**
+ * Creates a new instance and registers the BouncyCastle security provider
+ * if needed.
+ */
+ public BouncyCastleGpgSignatureVerifier() {
+ registerBouncyCastleProviderIfNecessary();
+ }
+
+ // To support more efficient signature verification of multiple objects we
+ // cache public keys once found in a LRU cache.
+
+ private static final Object NO_KEY = new Object();
+
+ private LRUMap<String, Object> byFingerprint = new LRUMap<>(16, 200);
+
+ private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200);
+
+ @Override
+ public String getName() {
+ return "bc"; //$NON-NLS-1$
+ }
+
+ @Override
+ @Nullable
+ public SignatureVerification verifySignature(@NonNull RevObject object,
+ @NonNull GpgConfig config) throws IOException {
+ if (object instanceof RevCommit) {
+ RevCommit commit = (RevCommit) object;
+ byte[] signatureData = commit.getRawGpgSignature();
+ if (signatureData == null) {
+ return null;
+ }
+ byte[] raw = commit.getRawBuffer();
+ // Now remove the GPG signature
+ byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
+ int start = RawParseUtils.headerStart(header, raw, 0);
+ if (start < 0) {
+ return null;
+ }
+ int end = RawParseUtils.headerEnd(raw, start);
+ // start is at the beginning of the header's content
+ start -= header.length + 1;
+ // end is on the terminating LF; we need to skip that, too
+ if (end < raw.length) {
+ end++;
+ }
+ byte[] data = new byte[raw.length - (end - start)];
+ System.arraycopy(raw, 0, data, 0, start);
+ System.arraycopy(raw, end, data, start, raw.length - end);
+ return verify(data, signatureData);
+ } else if (object instanceof RevTag) {
+ RevTag tag = (RevTag) object;
+ byte[] signatureData = tag.getRawGpgSignature();
+ if (signatureData == null) {
+ return null;
+ }
+ byte[] raw = tag.getRawBuffer();
+ // The signature is just tacked onto the end of the message, which
+ // is last in the buffer.
+ byte[] data = Arrays.copyOfRange(raw, 0,
+ raw.length - signatureData.length);
+ return verify(data, signatureData);
+ }
+ return null;
+ }
+
+ static PGPSignature parseSignature(InputStream in)
+ throws IOException, PGPException {
+ try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
+ JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
+ Object obj = pgpFactory.nextObject();
+ if (obj instanceof PGPCompressedData) {
+ obj = new JcaPGPObjectFactory(
+ ((PGPCompressedData) obj).getDataStream()).nextObject();
+ }
+ if (obj instanceof PGPSignatureList) {
+ return ((PGPSignatureList) obj).get(0);
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public SignatureVerification verify(byte[] data, byte[] signatureData)
+ throws IOException {
+ PGPSignature signature = null;
+ String fingerprint = null;
+ String signer = null;
+ String keyId = null;
+ try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
+ signature = parseSignature(sigIn);
+ if (signature != null) {
+ // Try to figure out something to find the public key with.
+ if (signature.hasSubpackets()) {
+ PGPSignatureSubpacketVector packets = signature
+ .getHashedSubPackets();
+ IssuerFingerprint fingerprintPacket = packets
+ .getIssuerFingerprint();
+ if (fingerprintPacket != null) {
+ fingerprint = Hex
+ .toHexString(fingerprintPacket.getFingerprint())
+ .toLowerCase(Locale.ROOT);
+ }
+ signer = packets.getSignerUserID();
+ if (signer != null) {
+ signer = BouncyCastleGpgSigner.extractSignerId(signer);
+ }
+ }
+ keyId = Long.toUnsignedString(signature.getKeyID(), 16)
+ .toLowerCase(Locale.ROOT);
+ } else {
+ throw new JGitInternalException(BCText.get().nonSignatureError);
+ }
+ } catch (PGPException e) {
+ throw new JGitInternalException(BCText.get().signatureParseError,
+ e);
+ }
+ Date signatureCreatedAt = signature.getCreationTime();
+ if (fingerprint == null && signer == null && keyId == null) {
+ return new VerificationResult(signatureCreatedAt, null, null, null,
+ false, false, TrustLevel.UNKNOWN,
+ BCText.get().signatureNoKeyInfo);
+ }
+ if (fingerprint != null && keyId != null
+ && !fingerprint.endsWith(keyId)) {
+ return new VerificationResult(signatureCreatedAt, signer, fingerprint,
+ null, false, false, TrustLevel.UNKNOWN,
+ MessageFormat.format(BCText.get().signatureInconsistent,
+ keyId, fingerprint));
+ }
+ if (fingerprint == null && keyId != null) {
+ fingerprint = keyId;
+ }
+ // Try to find the public key
+ String keySpec = '<' + signer + '>';
+ Object cached = null;
+ PGPPublicKey publicKey = null;
+ try {
+ cached = byFingerprint.get(fingerprint);
+ if (cached != null) {
+ if (cached instanceof PGPPublicKey) {
+ publicKey = (PGPPublicKey) cached;
+ }
+ } else if (!StringUtils.isEmptyOrNull(signer)) {
+ cached = bySigner.get(signer);
+ if (cached != null) {
+ if (cached instanceof PGPPublicKey) {
+ publicKey = (PGPPublicKey) cached;
+ }
+ }
+ }
+ if (cached == null) {
+ publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
+ keySpec);
+ }
+ } catch (IOException | PGPException e) {
+ throw new JGitInternalException(
+ BCText.get().signatureKeyLookupError, e);
+ }
+ if (publicKey == null) {
+ if (cached == null) {
+ byFingerprint.put(fingerprint, NO_KEY);
+ byFingerprint.put(keyId, NO_KEY);
+ if (signer != null) {
+ bySigner.put(signer, NO_KEY);
+ }
+ }
+ return new VerificationResult(signatureCreatedAt, signer,
+ fingerprint, null, false, false, TrustLevel.UNKNOWN,
+ BCText.get().signatureNoPublicKey);
+ }
+ if (cached == null) {
+ byFingerprint.put(fingerprint, publicKey);
+ byFingerprint.put(keyId, publicKey);
+ if (signer != null) {
+ bySigner.put(signer, publicKey);
+ }
+ }
+ String user = null;
+ Iterator<String> userIds = publicKey.getUserIDs();
+ if (!StringUtils.isEmptyOrNull(signer)) {
+ while (userIds.hasNext()) {
+ String userId = userIds.next();
+ if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
+ keySpec)) {
+ user = userId;
+ break;
+ }
+ }
+ }
+ if (user == null) {
+ userIds = publicKey.getUserIDs();
+ if (userIds.hasNext()) {
+ user = userIds.next();
+ }
+ }
+ boolean expired = false;
+ long validFor = publicKey.getValidSeconds();
+ if (validFor > 0 && signatureCreatedAt != null) {
+ Instant expiredAt = publicKey.getCreationTime().toInstant()
+ .plusSeconds(validFor);
+ expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
+ }
+ // Trust data is not defined in OpenPGP; the format is implementation
+ // specific. We don't use the GPG trustdb but simply the trust packet
+ // on the public key, if present. Even if present, it may or may not
+ // be set.
+ byte[] trustData = publicKey.getTrustData();
+ TrustLevel trust = parseGpgTrustPacket(trustData);
+ boolean verified = false;
+ try {
+ signature.init(
+ new JcaPGPContentVerifierBuilderProvider()
+ .setProvider(BouncyCastleProvider.PROVIDER_NAME),
+ publicKey);
+ signature.update(data);
+ verified = signature.verify();
+ } catch (PGPException e) {
+ throw new JGitInternalException(
+ BCText.get().signatureVerificationError, e);
+ }
+ return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
+ verified, expired, trust, null);
+ }
+
+ private TrustLevel parseGpgTrustPacket(byte[] packet) {
+ if (packet == null || packet.length < 6) {
+ // A GPG trust packet has at least 6 bytes.
+ return TrustLevel.UNKNOWN;
+ }
+ if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
+ // Not a GPG trust packet
+ return TrustLevel.UNKNOWN;
+ }
+ int trust = packet[0] & 0x0F;
+ switch (trust) {
+ case 0: // No determined/set
+ case 1: // Trust expired; i.e., calculation outdated or key expired
+ case 2: // Undefined: not enough information to set
+ return TrustLevel.UNKNOWN;
+ case 3:
+ return TrustLevel.NEVER;
+ case 4:
+ return TrustLevel.MARGINAL;
+ case 5:
+ return TrustLevel.FULL;
+ case 6:
+ return TrustLevel.ULTIMATE;
+ default:
+ return TrustLevel.UNKNOWN;
+ }
+ }
+
+ @Override
+ public void clear() {
+ byFingerprint.clear();
+ bySigner.clear();
+ }
+
+ private static class VerificationResult implements SignatureVerification {
+
+ private final Date creationDate;
+
+ private final String signer;
+
+ private final String keyUser;
+
+ private final String fingerprint;
+
+ private final boolean verified;
+
+ private final boolean expired;
+
+ private final @NonNull TrustLevel trustLevel;
+
+ private final String message;
+
+ public VerificationResult(Date creationDate, String signer,
+ String fingerprint, String user, boolean verified,
+ boolean expired, @NonNull TrustLevel trust, String message) {
+ this.creationDate = creationDate;
+ this.signer = signer;
+ this.fingerprint = fingerprint;
+ this.keyUser = user;
+ this.verified = verified;
+ this.expired = expired;
+ this.trustLevel = trust;
+ this.message = message;
+ }
+
+ @Override
+ public Date getCreationDate() {
+ return creationDate;
+ }
+
+ @Override
+ public String getSigner() {
+ return signer;
+ }
+
+ @Override
+ public String getKeyUser() {
+ return keyUser;
+ }
+
+ @Override
+ public String getKeyFingerprint() {
+ return fingerprint;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return expired;
+ }
+
+ @Override
+ public TrustLevel getTrustLevel() {
+ return trustLevel;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+
+ @Override
+ public boolean getVerified() {
+ return verified;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
new file mode 100644
index 0000000000..ae82b758a6
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSignatureVerifierFactory.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+import org.eclipse.jgit.lib.GpgSignatureVerifier;
+import org.eclipse.jgit.lib.GpgSignatureVerifierFactory;
+
+/**
+ * A {@link GpgSignatureVerifierFactory} that creates
+ * {@link GpgSignatureVerifier} instances that verify GPG signatures using
+ * BouncyCastle and that do cache public keys.
+ */
+public final class BouncyCastleGpgSignatureVerifierFactory
+ extends GpgSignatureVerifierFactory {
+
+ @Override
+ public GpgSignatureVerifier getVerifier() {
+ return new BouncyCastleGpgSignatureVerifier();
+ }
+
+}
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 ea159c547d..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
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2020, Salesforce and others
+ * Copyright (C) 2018, 2021, Salesforce 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
@@ -34,18 +34,25 @@ import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgObjectSigner;
import org.eclipse.jgit.lib.GpgSignature;
import org.eclipse.jgit.lib.GpgSigner;
+import org.eclipse.jgit.lib.ObjectBuilder;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
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 {
+public class BouncyCastleGpgSigner extends GpgSigner
+ implements GpgObjectSigner {
private static void registerBouncyCastleProviderIfNecessary() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -67,13 +74,32 @@ public class BouncyCastleGpgSigner extends GpgSigner {
public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
PersonIdent committer, CredentialsProvider credentialsProvider)
throws CanceledException {
+ try {
+ return canLocateSigningKey(gpgSigningKey, committer,
+ credentialsProvider, null);
+ } catch (UnsupportedSigningFormatException e) {
+ // Cannot occur with a null config
+ return false;
+ }
+ }
+
+ @Override
+ public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
+ PersonIdent committer, CredentialsProvider credentialsProvider,
+ GpgConfig config)
+ throws CanceledException, UnsupportedSigningFormatException {
+ if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+ throw new UnsupportedSigningFormatException(
+ JGitText.get().onlyOpenPgpSupportedForSigning);
+ }
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
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;
}
}
@@ -98,10 +124,28 @@ public class BouncyCastleGpgSigner extends GpgSigner {
public void sign(@NonNull CommitBuilder commit,
@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
CredentialsProvider credentialsProvider) throws CanceledException {
+ try {
+ signObject(commit, gpgSigningKey, committer, credentialsProvider,
+ null);
+ } catch (UnsupportedSigningFormatException e) {
+ // Cannot occur with a null config
+ }
+ }
+
+ @Override
+ public void signObject(@NonNull ObjectBuilder object,
+ @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
+ CredentialsProvider credentialsProvider, GpgConfig config)
+ throws CanceledException, UnsupportedSigningFormatException {
+ if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+ throw new UnsupportedSigningFormatException(
+ JGitText.get().onlyOpenPgpSupportedForSigning);
+ }
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
- committer, passphrasePrompt);
+ committer,
+ passphrasePrompt);
PGPSecretKey secretKey = gpgKey.getSecretKey();
if (secretKey == null) {
throw new JGitInternalException(
@@ -158,17 +202,17 @@ public class BouncyCastleGpgSigner extends GpgSigner {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (BCPGOutputStream out = new BCPGOutputStream(
new ArmoredOutputStream(buffer))) {
- signatureGenerator.update(commit.build());
+ signatureGenerator.update(object.build());
signatureGenerator.generate().encode(out);
}
- commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
+ object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
} catch (PGPException | IOException | NoSuchAlgorithmException
| NoSuchProviderException | URISyntaxException e) {
throw new JGitInternalException(e.getMessage(), e);
}
}
- private String extractSignerId(String pgpUserId) {
+ static String extractSignerId(String pgpUserId) {
int from = pgpUserId.indexOf('<');
if (from >= 0) {
int to = pgpUserId.indexOf('>', from + 1);
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
new file mode 100644
index 0000000000..b1d4446010
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
@@ -0,0 +1,322 @@
+/*
+ * 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.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers;
+import org.bouncycastle.asn1.x9.ECNamedCurveTable;
+import org.bouncycastle.asn1.x9.X9ECParameters;
+import org.bouncycastle.bcpg.DSAPublicBCPGKey;
+import org.bouncycastle.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.bcpg.RSAPublicBCPGKey;
+import org.bouncycastle.crypto.ec.CustomNamedCurves;
+import org.bouncycastle.math.ec.ECAlgorithms;
+import org.bouncycastle.math.field.FiniteField;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.util.encoders.Hex;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.gpg.bc.internal.BCText;
+import org.eclipse.jgit.util.sha1.SHA1;
+
+/**
+ * Utilities to compute the <em>keygrip</em> of a key. A keygrip is a SHA1 hash
+ * over the public key parameters and is used internally by the gpg-agent to
+ * find the secret key belonging to a public key: the secret key is stored in a
+ * file under ~/.gnupg/private-keys-v1.d/ with a name "&lt;keygrip>.key". While
+ * this storage organization is an implementation detail of GPG, the way
+ * keygrips are computed is not; they are computed by libgcrypt and their
+ * definition is stable.
+ */
+public final class KeyGrip {
+
+ // Some OIDs apparently unknown to BouncyCastle.
+
+ private static String OID_OPENPGP_ED25519 = "1.3.6.1.4.1.11591.15.1"; //$NON-NLS-1$
+
+ private static String OID_RFC8410_CURVE25519 = "1.3.101.110"; //$NON-NLS-1$
+
+ private static String OID_RFC8410_ED25519 = "1.3.101.112"; //$NON-NLS-1$
+
+ private KeyGrip() {
+ // No instantiation
+ }
+
+ /**
+ * Computes the keygrip for a {@link PGPPublicKey}.
+ *
+ * @param publicKey
+ * to get the keygrip of
+ * @return the keygrip
+ * @throws PGPException
+ * if an unknown key type is encountered.
+ */
+ @NonNull
+ public static byte[] getKeyGrip(PGPPublicKey publicKey)
+ throws PGPException {
+ SHA1 grip = SHA1.newInstance();
+ grip.setDetectCollision(false);
+
+ switch (publicKey.getAlgorithm()) {
+ case PublicKeyAlgorithmTags.RSA_GENERAL:
+ case PublicKeyAlgorithmTags.RSA_ENCRYPT:
+ case PublicKeyAlgorithmTags.RSA_SIGN:
+ BigInteger modulus = ((RSAPublicBCPGKey) publicKey
+ .getPublicKeyPacket().getKey()).getModulus();
+ hash(grip, modulus.toByteArray());
+ break;
+ case PublicKeyAlgorithmTags.DSA:
+ DSAPublicBCPGKey dsa = (DSAPublicBCPGKey) publicKey
+ .getPublicKeyPacket().getKey();
+ hash(grip, dsa.getP().toByteArray(), 'p', true);
+ hash(grip, dsa.getQ().toByteArray(), 'q', true);
+ hash(grip, dsa.getG().toByteArray(), 'g', true);
+ hash(grip, dsa.getY().toByteArray(), 'y', true);
+ break;
+ case PublicKeyAlgorithmTags.ELGAMAL_GENERAL:
+ case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
+ ElGamalPublicBCPGKey eg = (ElGamalPublicBCPGKey) publicKey
+ .getPublicKeyPacket().getKey();
+ hash(grip, eg.getP().toByteArray(), 'p', true);
+ hash(grip, eg.getG().toByteArray(), 'g', true);
+ hash(grip, eg.getY().toByteArray(), 'y', true);
+ break;
+ case PublicKeyAlgorithmTags.ECDH:
+ case PublicKeyAlgorithmTags.ECDSA:
+ case PublicKeyAlgorithmTags.EDDSA:
+ ECPublicBCPGKey ec = (ECPublicBCPGKey) publicKey
+ .getPublicKeyPacket().getKey();
+ ASN1ObjectIdentifier curveOID = ec.getCurveOID();
+ // BC doesn't know these OIDs.
+ if (OID_OPENPGP_ED25519.equals(curveOID.getId())
+ || OID_RFC8410_ED25519.equals(curveOID.getId())) {
+ return hashEd25519(grip, ec.getEncodedPoint());
+ } else if (CryptlibObjectIdentifiers.curvey25519.equals(curveOID)
+ || OID_RFC8410_CURVE25519.equals(curveOID.getId())) {
+ // curvey25519 actually is the OpenPGP OID for Curve25519 and is
+ // known to BC, but the parameters are for the short Weierstrass
+ // form. See https://github.com/bcgit/bc-java/issues/399 .
+ // libgcrypt uses Montgomery form.
+ return hashCurve25519(grip, ec.getEncodedPoint());
+ }
+ X9ECParameters params = getX9Parameters(curveOID);
+ if (params == null) {
+ throw new PGPException(MessageFormat
+ .format(BCText.get().unknownCurve, curveOID.getId()));
+ }
+ // Need to write p, a, b, g, n, q
+ BigInteger q = ec.getEncodedPoint();
+ byte[] g = params.getG().getEncoded(false);
+ BigInteger a = params.getCurve().getA().toBigInteger();
+ BigInteger b = params.getCurve().getB().toBigInteger();
+ BigInteger n = params.getN();
+ BigInteger p = null;
+ FiniteField field = params.getCurve().getField();
+ if (ECAlgorithms.isFpField(field)) {
+ p = field.getCharacteristic();
+ }
+ if (p == null) {
+ // Don't know...
+ throw new PGPException(MessageFormat.format(
+ BCText.get().unknownCurveParameters, curveOID.getId()));
+ }
+ hash(grip, p.toByteArray(), 'p', false);
+ hash(grip, a.toByteArray(), 'a', false);
+ hash(grip, b.toByteArray(), 'b', false);
+ hash(grip, g, 'g', false);
+ hash(grip, n.toByteArray(), 'n', false);
+ if (publicKey.getAlgorithm() == PublicKeyAlgorithmTags.EDDSA) {
+ hashQ25519(grip, q);
+ } else {
+ hash(grip, q.toByteArray(), 'q', false);
+ }
+ break;
+ default:
+ throw new PGPException(
+ MessageFormat.format(BCText.get().unknownKeyType,
+ Integer.toString(publicKey.getAlgorithm())));
+ }
+ return grip.digest();
+ }
+
+ private static void hash(SHA1 grip, byte[] data) {
+ // Need to skip leading zero bytes
+ int i = 0;
+ while (i < data.length && data[i] == 0) {
+ i++;
+ }
+ int length = data.length - i;
+ if (i < data.length) {
+ if ((data[i] & 0x80) != 0) {
+ grip.update((byte) 0);
+ }
+ grip.update(data, i, length);
+ }
+ }
+
+ private static void hash(SHA1 grip, byte[] data, char id, boolean zeroPad) {
+ // Need to skip leading zero bytes
+ int i = 0;
+ while (i < data.length && data[i] == 0) {
+ i++;
+ }
+ int length = data.length - i;
+ boolean addZero = false;
+ if (i < data.length && zeroPad && (data[i] & 0x80) != 0) {
+ addZero = true;
+ }
+ // libgcrypt includes an SExp in the hash
+ String prefix = "(1:" + id + (addZero ? length + 1 : length) + ':'; //$NON-NLS-1$
+ grip.update(prefix.getBytes(StandardCharsets.US_ASCII));
+ // For some items, gcrypt prepends a zero byte if the high bit is set
+ if (addZero) {
+ grip.update((byte) 0);
+ }
+ if (i < data.length) {
+ grip.update(data, i, length);
+ }
+ grip.update((byte) ')');
+ }
+
+ private static void hashQ25519(SHA1 grip, BigInteger q)
+ throws PGPException {
+ byte[] data = q.toByteArray();
+ switch (data[0]) {
+ case 0x04:
+ if (data.length != 65) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().corrupt25519Key, Hex.toHexString(data)));
+ }
+ // Uncompressed: should not occur with ed25519 or curve25519
+ throw new PGPException(MessageFormat.format(
+ BCText.get().uncompressed25519Key, Hex.toHexString(data)));
+ case 0x40:
+ if (data.length != 33) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().corrupt25519Key, Hex.toHexString(data)));
+ }
+ // Compressed; normal case. Skip prefix.
+ hash(grip, Arrays.copyOfRange(data, 1, data.length), 'q', false);
+ break;
+ default:
+ if (data.length != 32) {
+ throw new PGPException(MessageFormat.format(
+ BCText.get().corrupt25519Key, Hex.toHexString(data)));
+ }
+ // Compressed format without prefix. Should not occur?
+ hash(grip, data, 'q', false);
+ break;
+ }
+ }
+
+ /**
+ * Computes the keygrip for an ed25519 public key.
+ * <p>
+ * Package-visible for tests only.
+ * </p>
+ *
+ * @param grip
+ * initialized {@link SHA1}
+ * @param q
+ * the public key's EC point
+ * @return the keygrip
+ * @throws PGPException
+ * if q indicates uncompressed format
+ */
+ @SuppressWarnings("nls")
+ static byte[] hashEd25519(SHA1 grip, BigInteger q) throws PGPException {
+ // For the values, see RFC 7748: https://tools.ietf.org/html/rfc7748
+ // p = 2^255 - 19
+ hash(grip, Hex.decodeStrict(
+ "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
+ 'p', false);
+ // Field: a = 1
+ hash(grip, new byte[] { 0x01 }, 'a', false);
+ // Field: b = 121665/121666 (mod p)
+ // See Berstein et.al., "Twisted Edwards Curves",
+ // https://doi.org/10.1007/978-3-540-68164-9_26
+ hash(grip, Hex.decodeStrict(
+ "2DFC9311D490018C7338BF8688861767FF8FF5B2BEBE27548A14B235ECA6874A"),
+ 'b', false);
+ // Generator point with affine X,Y
+ // @formatter:off
+ // X(P) = 15112221349535400772501151409588531511454012693041857206046113283949847762202
+ // Y(P) = 46316835694926478169428394003475163141307993866256225615783033603165251855960
+ // the "04" signifies uncompressed format.
+ // @formatter:on
+ hash(grip, Hex.decodeStrict("04"
+ + "216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A"
+ + "6666666666666666666666666666666666666666666666666666666666666658"),
+ 'g', false);
+ // order = 2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed
+ hash(grip, Hex.decodeStrict(
+ "1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
+ 'n', false);
+ hashQ25519(grip, q);
+ return grip.digest();
+ }
+
+ /**
+ * Computes the keygrip for a curve25519 public key.
+ * <p>
+ * Package-visible for tests only.
+ * </p>
+ *
+ * @param grip
+ * initialized {@link SHA1}
+ * @param q
+ * the public key's EC point
+ * @return the keygrip
+ * @throws PGPException
+ * if q indicates uncompressed format
+ */
+ @SuppressWarnings("nls")
+ static byte[] hashCurve25519(SHA1 grip, BigInteger q) throws PGPException {
+ hash(grip, Hex.decodeStrict(
+ "7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
+ 'p', false);
+ // Unclear: RFC 7748 says A = 486662. This value here is (A-2)/4 =
+ // 121665. Compare ecc-curves.c in libgcrypt:
+ // https://github.com/gpg/libgcrypt/blob/361a058/cipher/ecc-curves.c#L146
+ hash(grip, new byte[] { 0x01, (byte) 0xDB, 0x41 }, 'a', false);
+ hash(grip, new byte[] { 0x01 }, 'b', false);
+ // libgcrypt uses the old g.y value before the erratum to RFC 7748 for
+ // the keygrip. The new value would be
+ // 5F51E65E475F794B1FE122D388B72EB36DC2B28192839E4DD6163A5D81312C14. See
+ // https://www.rfc-editor.org/errata/eid4730 and
+ // https://github.com/gpg/libgcrypt/commit/f67b6492e0b0
+ hash(grip, Hex.decodeStrict("04"
+ + "0000000000000000000000000000000000000000000000000000000000000009"
+ + "20AE19A1B8A086B4E01EDD2C7748D14C923D4D7E6D7C61B229E9C5A27ECED3D9"),
+ 'g', false);
+ hash(grip, Hex.decodeStrict(
+ "1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
+ 'n', false);
+ hashQ25519(grip, q);
+ return grip.digest();
+ }
+
+ private static X9ECParameters getX9Parameters(
+ ASN1ObjectIdentifier curveOID) {
+ X9ECParameters params = CustomNamedCurves.getByOID(curveOID);
+ if (params == null) {
+ params = ECNamedCurveTable.getByOID(curveOID);
+ }
+ return params;
+ }
+
+}
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..269a1ba0f6
--- /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.
+ PBEProtectionRemoverFactory decryptor = null;
+ if (matches(data, 4, PROTECTED_KEY)) {
+ // AES/CBC encrypted.
+ 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;
+ }
+}