Browse Source
This also includes a change to generating the jgit CLI jar. Shading is no longer possible because it breaks the signature of BouncyCastle. Instead, the Spring Boot Loader Maven plug-in is now used to generate an executable jar. Bug: 382212 Change-Id: I35ee3d4b06d9d479475ab2e51b29bed49661bbdc Also-by: Gunnar Wagenknecht <gunnar@wagenknecht.org> Signed-off-by: Gunnar Wagenknecht <gunnar@wagenknecht.org> Signed-off-by: Medha Bhargav Prabhala <mprabhala@salesforce.com> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>stable-5.3
Medha Bhargav Prabhala
6 years ago
committed by
Gunnar Wagenknecht
28 changed files with 951 additions and 46 deletions
@ -0,0 +1,71 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Salesforce. |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.lib.internal; |
||||||
|
|
||||||
|
import java.nio.file.Path; |
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKey; |
||||||
|
|
||||||
|
/** |
||||||
|
* Container which holds a {@link #getSecretKey()} together with the |
||||||
|
* {@link #getOrigin() path it was loaded from}. |
||||||
|
*/ |
||||||
|
class BouncyCastleGpgKey { |
||||||
|
|
||||||
|
private PGPSecretKey secretKey; |
||||||
|
|
||||||
|
private Path origin; |
||||||
|
|
||||||
|
public BouncyCastleGpgKey(PGPSecretKey secretKey, Path origin) { |
||||||
|
this.secretKey = secretKey; |
||||||
|
this.origin = origin; |
||||||
|
} |
||||||
|
|
||||||
|
public PGPSecretKey getSecretKey() { |
||||||
|
return secretKey; |
||||||
|
} |
||||||
|
|
||||||
|
public Path getOrigin() { |
||||||
|
return origin; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,340 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Salesforce. |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.lib.internal; |
||||||
|
|
||||||
|
import static java.nio.file.Files.exists; |
||||||
|
import static java.nio.file.Files.newInputStream; |
||||||
|
|
||||||
|
import java.io.BufferedInputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.nio.file.Files; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.nio.file.Paths; |
||||||
|
import java.text.MessageFormat; |
||||||
|
import java.util.Iterator; |
||||||
|
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; |
||||||
|
import org.bouncycastle.gpg.keybox.KeyInformation; |
||||||
|
import org.bouncycastle.gpg.keybox.PublicKeyRingBlob; |
||||||
|
import org.bouncycastle.gpg.keybox.UserID; |
||||||
|
import org.bouncycastle.openpgp.PGPException; |
||||||
|
import org.bouncycastle.openpgp.PGPPublicKey; |
||||||
|
import org.bouncycastle.openpgp.PGPSecretKey; |
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing; |
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; |
||||||
|
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.internal.JGitText; |
||||||
|
|
||||||
|
/** |
||||||
|
* Locates GPG keys from either <code>~/.gnupg/private-keys-v1.d</code> or |
||||||
|
* <code>~/.gnupg/secring.gpg</code> |
||||||
|
*/ |
||||||
|
class BouncyCastleGpgKeyLocator { |
||||||
|
|
||||||
|
private static final Path USER_KEYBOX_PATH = Paths |
||||||
|
.get(System.getProperty("user.home"), ".gnupg", "pubring.kbx"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
|
||||||
|
|
||||||
|
private static final Path USER_SECRET_KEY_DIR = Paths.get( |
||||||
|
System.getProperty("user.home"), ".gnupg", "private-keys-v1.d"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
|
||||||
|
|
||||||
|
private static final Path USER_PGP_LEGACY_SECRING_FILE = Paths |
||||||
|
.get(System.getProperty("user.home"), ".gnupg", "secring.gpg"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
|
||||||
|
|
||||||
|
private final String signingKey; |
||||||
|
|
||||||
|
private BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt; |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new key locator for the specified signing key. |
||||||
|
* <p> |
||||||
|
* The signing key must either be a hex representation of a specific key or |
||||||
|
* a user identity substring (eg., email address). All keys in the KeyBox |
||||||
|
* will be looked up in the order as returned by the KeyBox. A key id will |
||||||
|
* be searched before attempting to find a key by user id. |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param signingKey |
||||||
|
* the signing key to search for |
||||||
|
* @param passphrasePrompt |
||||||
|
* the provider to use when asking for key passphrase |
||||||
|
*/ |
||||||
|
public BouncyCastleGpgKeyLocator(String signingKey, |
||||||
|
@NonNull BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt) { |
||||||
|
this.signingKey = signingKey; |
||||||
|
this.passphrasePrompt = passphrasePrompt; |
||||||
|
} |
||||||
|
|
||||||
|
private PGPSecretKey attemptParseSecretKey(Path keyFile, |
||||||
|
PGPDigestCalculatorProvider calculatorProvider, |
||||||
|
PBEProtectionRemoverFactory passphraseProvider, |
||||||
|
PGPPublicKey publicKey) throws IOException { |
||||||
|
try (InputStream in = newInputStream(keyFile)) { |
||||||
|
return new SExprParser(calculatorProvider).parseSecretKey( |
||||||
|
new BufferedInputStream(in), passphraseProvider, publicKey); |
||||||
|
} catch (PGPException | ClassCastException e) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private boolean containsSigningKey(String userId) { |
||||||
|
return userId.toLowerCase(Locale.ROOT) |
||||||
|
.contains(signingKey.toLowerCase(Locale.ROOT)); |
||||||
|
} |
||||||
|
|
||||||
|
private PGPPublicKey findPublicKeyByKeyId(KeyBlob keyBlob) |
||||||
|
throws IOException { |
||||||
|
for (KeyInformation keyInfo : keyBlob.getKeyInformation()) { |
||||||
|
if (signingKey.toLowerCase(Locale.ROOT) |
||||||
|
.equals(Hex.toHexString(keyInfo.getKeyID()) |
||||||
|
.toLowerCase(Locale.ROOT))) { |
||||||
|
return getFirstPublicKey(keyBlob); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private PGPPublicKey findPublicKeyByUserId(KeyBlob keyBlob) |
||||||
|
throws IOException { |
||||||
|
for (UserID userID : keyBlob.getUserIds()) { |
||||||
|
if (containsSigningKey(userID.getUserIDAsString())) { |
||||||
|
return getFirstPublicKey(keyBlob); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Finds a public key associated with the signing key. |
||||||
|
* |
||||||
|
* @param keyboxFile |
||||||
|
* the KeyBox file |
||||||
|
* @return publicKey the public key (maybe <code>null</code>) |
||||||
|
* @throws IOException |
||||||
|
* in case of problems reading the file |
||||||
|
*/ |
||||||
|
private PGPPublicKey findPublicKeyInKeyBox(Path keyboxFile) |
||||||
|
throws IOException { |
||||||
|
KeyBox keyBox = readKeyBoxFile(keyboxFile); |
||||||
|
for (KeyBlob keyBlob : keyBox.getKeyBlobs()) { |
||||||
|
if (keyBlob.getType() == BlobType.OPEN_PGP_BLOB) { |
||||||
|
PGPPublicKey key = findPublicKeyByKeyId(keyBlob); |
||||||
|
if (key != null) { |
||||||
|
return key; |
||||||
|
} |
||||||
|
key = findPublicKeyByUserId(keyBlob); |
||||||
|
if (key != null) { |
||||||
|
return key; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Use pubring.kbx when available, if not fallback to secring.gpg or secret |
||||||
|
* key path provided to parse and return secret key |
||||||
|
* |
||||||
|
* @return the secret key |
||||||
|
* @throws IOException |
||||||
|
* in case of issues reading key files |
||||||
|
* @throws PGPException |
||||||
|
* in case of issues finding a key |
||||||
|
* @throws CanceledException |
||||||
|
* @throws URISyntaxException |
||||||
|
* @throws UnsupportedCredentialItem |
||||||
|
*/ |
||||||
|
public BouncyCastleGpgKey findSecretKey() |
||||||
|
throws IOException, PGPException, CanceledException, |
||||||
|
UnsupportedCredentialItem, URISyntaxException { |
||||||
|
if (exists(USER_KEYBOX_PATH)) { |
||||||
|
PGPPublicKey publicKey = //
|
||||||
|
findPublicKeyInKeyBox(USER_KEYBOX_PATH); |
||||||
|
|
||||||
|
if (publicKey != null) { |
||||||
|
return findSecretKeyForKeyBoxPublicKey(publicKey, |
||||||
|
USER_KEYBOX_PATH); |
||||||
|
} |
||||||
|
|
||||||
|
throw new PGPException(MessageFormat |
||||||
|
.format(JGitText.get().gpgNoPublicKeyFound, signingKey)); |
||||||
|
} else if (exists(USER_PGP_LEGACY_SECRING_FILE)) { |
||||||
|
PGPSecretKey secretKey = findSecretKeyInLegacySecring(signingKey, |
||||||
|
USER_PGP_LEGACY_SECRING_FILE); |
||||||
|
|
||||||
|
if (secretKey != null) { |
||||||
|
return new BouncyCastleGpgKey(secretKey, USER_PGP_LEGACY_SECRING_FILE); |
||||||
|
} |
||||||
|
|
||||||
|
throw new PGPException(MessageFormat.format( |
||||||
|
JGitText.get().gpgNoKeyInLegacySecring, signingKey)); |
||||||
|
} |
||||||
|
|
||||||
|
throw new PGPException(JGitText.get().gpgNoKeyring); |
||||||
|
} |
||||||
|
|
||||||
|
private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey( |
||||||
|
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(); |
||||||
|
|
||||||
|
PBEProtectionRemoverFactory passphraseProvider = new JcePBEProtectionRemoverFactory( |
||||||
|
passphrasePrompt.getPassphrase(publicKey.getFingerprint(), |
||||||
|
userKeyboxPath)); |
||||||
|
|
||||||
|
try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) { |
||||||
|
for (Path keyFile : keyFiles.filter(Files::isRegularFile) |
||||||
|
.collect(Collectors.toList())) { |
||||||
|
PGPSecretKey secretKey = attemptParseSecretKey(keyFile, |
||||||
|
calculatorProvider, passphraseProvider, publicKey); |
||||||
|
if (secretKey != null) { |
||||||
|
return new BouncyCastleGpgKey(secretKey, userKeyboxPath); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
passphrasePrompt.clear(); |
||||||
|
throw new PGPException(MessageFormat.format( |
||||||
|
JGitText.get().gpgNoSecretKeyForPublicKey, |
||||||
|
Long.toHexString(publicKey.getKeyID()))); |
||||||
|
} catch (RuntimeException e) { |
||||||
|
passphrasePrompt.clear(); |
||||||
|
throw e; |
||||||
|
} catch (IOException e) { |
||||||
|
passphrasePrompt.clear(); |
||||||
|
throw new PGPException(MessageFormat.format( |
||||||
|
JGitText.get().gpgFailedToParseSecretKey, |
||||||
|
USER_SECRET_KEY_DIR.toAbsolutePath()), e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Return the first suitable key for signing in the key ring collection. For |
||||||
|
* this case we only expect there to be one key available for signing. |
||||||
|
* </p> |
||||||
|
* |
||||||
|
* @param signingkey |
||||||
|
* @param secringFile |
||||||
|
* |
||||||
|
* @return the first suitable PGP secret key found for signing |
||||||
|
* @throws IOException |
||||||
|
* on I/O related errors |
||||||
|
* @throws PGPException |
||||||
|
* on BouncyCastle errors |
||||||
|
*/ |
||||||
|
private PGPSecretKey findSecretKeyInLegacySecring(String signingkey, |
||||||
|
Path secringFile) throws IOException, PGPException { |
||||||
|
|
||||||
|
try (InputStream in = newInputStream(secringFile)) { |
||||||
|
PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection( |
||||||
|
PGPUtil.getDecoderStream(new BufferedInputStream(in)), |
||||||
|
new JcaKeyFingerprintCalculator()); |
||||||
|
|
||||||
|
Iterator<PGPSecretKeyRing> keyrings = pgpSec.getKeyRings(); |
||||||
|
while (keyrings.hasNext()) { |
||||||
|
PGPSecretKeyRing keyRing = keyrings.next(); |
||||||
|
Iterator<PGPSecretKey> keys = keyRing.getSecretKeys(); |
||||||
|
while (keys.hasNext()) { |
||||||
|
PGPSecretKey key = keys.next(); |
||||||
|
// try key id
|
||||||
|
String fingerprint = Hex |
||||||
|
.toHexString(key.getPublicKey().getFingerprint()) |
||||||
|
.toLowerCase(Locale.ROOT); |
||||||
|
if (fingerprint |
||||||
|
.endsWith(signingkey.toLowerCase(Locale.ROOT))) { |
||||||
|
return key; |
||||||
|
} |
||||||
|
// try user id
|
||||||
|
Iterator<String> userIDs = key.getUserIDs(); |
||||||
|
while (userIDs.hasNext()) { |
||||||
|
String userId = userIDs.next(); |
||||||
|
if (containsSigningKey(userId)) { |
||||||
|
return key; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private PGPPublicKey getFirstPublicKey(KeyBlob keyBlob) throws IOException { |
||||||
|
return ((PublicKeyRingBlob) keyBlob).getPGPPublicKeyRing() |
||||||
|
.getPublicKey(); |
||||||
|
} |
||||||
|
|
||||||
|
private KeyBox readKeyBoxFile(Path keyboxFile) throws IOException { |
||||||
|
KeyBox keyBox; |
||||||
|
try (InputStream in = new BufferedInputStream( |
||||||
|
newInputStream(keyboxFile))) { |
||||||
|
// note: KeyBox constructor reads in the whole InputStream at once
|
||||||
|
// this code will change in 1.61 to
|
||||||
|
// either 'new BcKeyBox(in)' or 'new JcaKeyBoxBuilder().build(in)'
|
||||||
|
keyBox = new KeyBox(in, new JcaKeyFingerprintCalculator()); |
||||||
|
} |
||||||
|
return keyBox; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
/*- |
||||||
|
* Copyright (C) 2019, Salesforce. |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.lib.internal; |
||||||
|
|
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.nio.file.Path; |
||||||
|
import java.text.MessageFormat; |
||||||
|
|
||||||
|
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.internal.JGitText; |
||||||
|
import org.eclipse.jgit.transport.CredentialItem.CharArrayType; |
||||||
|
import org.eclipse.jgit.transport.CredentialItem.InformationalMessage; |
||||||
|
import org.eclipse.jgit.transport.CredentialsProvider; |
||||||
|
import org.eclipse.jgit.transport.URIish; |
||||||
|
|
||||||
|
/** |
||||||
|
* Prompts for a passphrase and caches it until {@link #clear() cleared}. |
||||||
|
* <p> |
||||||
|
* Implements {@link AutoCloseable} so it can be used within a |
||||||
|
* try-with-resources block. |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable { |
||||||
|
|
||||||
|
private CharArrayType passphrase; |
||||||
|
|
||||||
|
private CredentialsProvider credentialsProvider; |
||||||
|
|
||||||
|
public BouncyCastleGpgKeyPassphrasePrompt( |
||||||
|
CredentialsProvider credentialsProvider) { |
||||||
|
this.credentialsProvider = credentialsProvider; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears any cached passphrase |
||||||
|
*/ |
||||||
|
public void clear() { |
||||||
|
if (passphrase != null) { |
||||||
|
passphrase.clear(); |
||||||
|
passphrase = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
clear(); |
||||||
|
} |
||||||
|
|
||||||
|
private URIish createURI(Path keyLocation) throws URISyntaxException { |
||||||
|
return new URIish(keyLocation.toUri().toString()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Prompts use for a passphrase unless one was cached from a previous |
||||||
|
* prompt. |
||||||
|
* |
||||||
|
* @param keyFingerprint |
||||||
|
* the fingerprint to show to the user during prompting |
||||||
|
* @param keyLocation |
||||||
|
* the location the key was loaded from |
||||||
|
* @return the passphrase (maybe <code>null</code>) |
||||||
|
* @throws PGPException |
||||||
|
* @throws CanceledException |
||||||
|
* in case passphrase was not entered by user |
||||||
|
* @throws URISyntaxException |
||||||
|
* @throws UnsupportedCredentialItem |
||||||
|
*/ |
||||||
|
public char[] getPassphrase(byte[] keyFingerprint, Path keyLocation) |
||||||
|
throws PGPException, CanceledException, UnsupportedCredentialItem, |
||||||
|
URISyntaxException { |
||||||
|
if (passphrase == null) { |
||||||
|
passphrase = new CharArrayType(JGitText.get().credentialPassphrase, |
||||||
|
true); |
||||||
|
} |
||||||
|
|
||||||
|
if (credentialsProvider == null) { |
||||||
|
throw new PGPException(JGitText.get().gpgNoCredentialsProvider); |
||||||
|
} |
||||||
|
|
||||||
|
if (passphrase.getValue() == null |
||||||
|
&& !credentialsProvider.get(createURI(keyLocation), |
||||||
|
new InformationalMessage( |
||||||
|
MessageFormat.format(JGitText.get().gpgKeyInfo, |
||||||
|
Hex.toHexString(keyFingerprint))), |
||||||
|
passphrase)) { |
||||||
|
throw new CanceledException(JGitText.get().gpgSigningCancelled); |
||||||
|
} |
||||||
|
return passphrase.getValue(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Salesforce. |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* - 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. |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.lib.internal; |
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.security.Security; |
||||||
|
|
||||||
|
import org.bouncycastle.bcpg.ArmoredOutputStream; |
||||||
|
import org.bouncycastle.bcpg.BCPGOutputStream; |
||||||
|
import org.bouncycastle.bcpg.HashAlgorithmTags; |
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider; |
||||||
|
import org.bouncycastle.openpgp.PGPException; |
||||||
|
import org.bouncycastle.openpgp.PGPPrivateKey; |
||||||
|
import org.bouncycastle.openpgp.PGPSecretKey; |
||||||
|
import org.bouncycastle.openpgp.PGPSignature; |
||||||
|
import org.bouncycastle.openpgp.PGPSignatureGenerator; |
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; |
||||||
|
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; |
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
import org.eclipse.jgit.api.errors.CanceledException; |
||||||
|
import org.eclipse.jgit.api.errors.JGitInternalException; |
||||||
|
import org.eclipse.jgit.internal.JGitText; |
||||||
|
import org.eclipse.jgit.lib.CommitBuilder; |
||||||
|
import org.eclipse.jgit.lib.GpgSignature; |
||||||
|
import org.eclipse.jgit.lib.GpgSigner; |
||||||
|
import org.eclipse.jgit.lib.PersonIdent; |
||||||
|
import org.eclipse.jgit.transport.CredentialsProvider; |
||||||
|
|
||||||
|
/** |
||||||
|
* GPG Signer using BouncyCastle library |
||||||
|
*/ |
||||||
|
public class BouncyCastleGpgSigner extends GpgSigner { |
||||||
|
|
||||||
|
private static void registerBouncyCastleProviderIfNecessary() { |
||||||
|
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { |
||||||
|
Security.addProvider(new BouncyCastleProvider()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new instance. |
||||||
|
* <p> |
||||||
|
* The BounceCastleProvider will be registered if necessary. |
||||||
|
* </p> |
||||||
|
*/ |
||||||
|
public BouncyCastleGpgSigner() { |
||||||
|
registerBouncyCastleProviderIfNecessary(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sign(@NonNull CommitBuilder commit, String gpgSigningKey, |
||||||
|
@NonNull PersonIdent committer, |
||||||
|
CredentialsProvider credentialsProvider) throws CanceledException { |
||||||
|
if (gpgSigningKey == null || gpgSigningKey.isEmpty()) { |
||||||
|
gpgSigningKey = committer.getEmailAddress(); |
||||||
|
} |
||||||
|
|
||||||
|
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt( |
||||||
|
credentialsProvider)) { |
||||||
|
BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator( |
||||||
|
gpgSigningKey, passphrasePrompt); |
||||||
|
|
||||||
|
BouncyCastleGpgKey gpgKey = keyHelper.findSecretKey(); |
||||||
|
PGPSecretKey secretKey = gpgKey.getSecretKey(); |
||||||
|
if (secretKey == null) { |
||||||
|
throw new JGitInternalException( |
||||||
|
JGitText.get().unableToSignCommitNoSecretKey); |
||||||
|
} |
||||||
|
char[] passphrase = passphrasePrompt |
||||||
|
.getPassphrase(secretKey.getPublicKey().getFingerprint(), |
||||||
|
gpgKey.getOrigin()); |
||||||
|
PGPPrivateKey privateKey = secretKey |
||||||
|
.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() |
||||||
|
.setProvider(BouncyCastleProvider.PROVIDER_NAME) |
||||||
|
.build(passphrase)); |
||||||
|
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( |
||||||
|
new JcaPGPContentSignerBuilder( |
||||||
|
secretKey.getPublicKey().getAlgorithm(), |
||||||
|
HashAlgorithmTags.SHA256).setProvider( |
||||||
|
BouncyCastleProvider.PROVIDER_NAME)); |
||||||
|
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); |
||||||
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
||||||
|
try (BCPGOutputStream out = new BCPGOutputStream( |
||||||
|
new ArmoredOutputStream(buffer))) { |
||||||
|
signatureGenerator.update(commit.build()); |
||||||
|
signatureGenerator.generate().encode(out); |
||||||
|
} |
||||||
|
commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); |
||||||
|
} catch (PGPException | IOException | URISyntaxException e) { |
||||||
|
throw new JGitInternalException(e.getMessage(), e); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue