Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-17
Require-Bundle: org.hamcrest.core;bundle-version="[1.3.0,2.0.0)"
-Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
- org.bouncycastle.openpgp;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.encoders;version="[1.65.0,2.0.0)",
+Import-Package: org.bouncycastle.asn1.cryptlib;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jce.provider;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
org.eclipse.jgit.gpg.bc.internal;version="[7.1.0,7.2.0)",
org.eclipse.jgit.gpg.bc.internal.keys;version="[7.1.0,7.2.0)",
org.eclipse.jgit.util.sha1;version="[7.1.0,7.2.0)",
/*
- * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> 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
*/
package org.eclipse.jgit.gpg.bc.internal.keys;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.security.Security;
import java.util.Iterator;
-import javax.crypto.Cipher;
-
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
}
}
- private static volatile Boolean haveOCB;
-
- private static boolean ocbAvailable() {
- Boolean haveIt = haveOCB;
- if (haveIt != null) {
- return haveIt.booleanValue();
- }
- try {
- Cipher c = Cipher.getInstance("AES/OCB/NoPadding"); //$NON-NLS-1$
- if (c == null) {
- haveOCB = Boolean.FALSE;
- return false;
- }
- } catch (NoClassDefFoundError | Exception e) {
- haveOCB = Boolean.FALSE;
- return false;
- }
- haveOCB = Boolean.TRUE;
- return true;
- }
-
private static class TestData {
final String name;
final boolean encrypted;
- final boolean keyValue;
-
- TestData(String name, boolean encrypted, boolean keyValue) {
+ TestData(String name, boolean encrypted) {
this.name = name;
this.encrypted = encrypted;
- this.keyValue = keyValue;
}
@Override
@Parameters(name = "{0}")
public static TestData[] initTestData() {
return new TestData[] {
- new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false, false),
- new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false, true),
- new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true, true),
- new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true, true),
- new TestData("62D43D7F117F7A5E4998ECB6617EE9942D069C14", true, true),
- new TestData("faked", false, true) };
- }
-
- private static byte[] readTestKey(String filename) throws Exception {
- try (InputStream in = new BufferedInputStream(
- SecretKeysTest.class.getResourceAsStream(filename))) {
- return SecretKeys.keyFromNameValueFormat(in);
- }
+ new TestData("AFDA8EA10E185ACF8C0D0F8885A0EF61A72ECB11", false),
+ new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false),
+ new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true),
+ new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true),
+ new TestData("62D43D7F117F7A5E4998ECB6617EE9942D069C14", true),
+ new TestData("faked", false) };
}
private static PGPPublicKey readAsc(InputStream in)
@Test
public void testKeyRead() throws Exception {
- if (data.keyValue) {
- byte[] bytes = readTestKey(data.name + ".key");
- assertEquals('(', bytes[0]);
- assertEquals(')', bytes[bytes.length - 1]);
- }
try (InputStream pubIn = this.getClass()
.getResourceAsStream(data.name + ".asc")) {
if (pubIn != null) {
: null,
publicKey);
assertNotNull(secretKey);
- } catch (PGPException e) {
- // Currently we may not be able to load OCB-encrypted keys.
- assertTrue(e.toString(), e.getMessage().contains("OCB"));
- assertTrue(data.encrypted);
- assertFalse(ocbAvailable());
}
}
}
Bundle-Localization: OSGI-INF/l10n/gpg_bc
Bundle-Version: 7.1.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-17
-Import-Package: org.bouncycastle.asn1;version="[1.69.0,2.0.0)",
- org.bouncycastle.asn1.x9;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.bcpg.sig;version="[1.69.0,2.0.0)",
- org.bouncycastle.crypto.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox;version="[1.69.0,2.0.0)",
- org.bouncycastle.gpg.keybox.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.interfaces;version="[1.69.0,2.0.0)",
- org.bouncycastle.jcajce.util;version="[1.69.0,2.0.0)",
- org.bouncycastle.jce.provider;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.ec;version="[1.69.0,2.0.0)",
- org.bouncycastle.math.field;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator;version="[1.69.0,2.0.0)",
- org.bouncycastle.openpgp.operator.jcajce;version="[1.69.0,2.0.0)",
- org.bouncycastle.util;version="[1.69.0,2.0.0)",
- org.bouncycastle.util.encoders;version="[1.69.0,2.0.0)",
- org.bouncycastle.util.io;version="[1.69.0,2.0.0)",
+Import-Package: org.bouncycastle.asn1;version="[1.79.0,2.0.0)",
+ org.bouncycastle.asn1.x9;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.bcpg.sig;version="[1.79.0,2.0.0)",
+ org.bouncycastle.crypto.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox;version="[1.79.0,2.0.0)",
+ org.bouncycastle.gpg.keybox.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.interfaces;version="[1.79.0,2.0.0)",
+ org.bouncycastle.jcajce.util;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.ec;version="[1.79.0,2.0.0)",
+ org.bouncycastle.math.field;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator;version="[1.79.0,2.0.0)",
+ org.bouncycastle.openpgp.operator.jcajce;version="[1.79.0,2.0.0)",
+ org.bouncycastle.util.encoders;version="[1.79.0,2.0.0)",
org.slf4j;version="[1.7.0,3.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc.internal;version="7.1.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
org.eclipse.jgit.gpg.bc.internal.keys;version="7.1.0";x-friends:="org.eclipse.jgit.gpg.bc.test"
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.</p>
-<hr>
-<p><b>org.eclipse.jgit.gpg.bc.internal.keys.SExprParser - MIT</b></p>
-
-<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>
-
</body>
</html>
corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
-cryptCipherError=Cannot create cipher to decrypt: {0}
-cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
gpgNoCredentialsProvider=missing credentials provider
gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
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
+keyAlgorithmMismatch=Secret key has a different algorithm than the public key
+keyMismatch=Secret key does not match public key; public key is {0} {1} while secret key is for {2} {3}
logWarnGnuPGHome=Cannot access GPG home directory given by environment variable GNUPGHOME={}
logWarnGpgHomeProperty=Cannot access GPG home directory given by Java system property jgit.gpg.home={}
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
/*
- * Copyright (C) 2018, 2021 Salesforce and others
+ * Copyright (C) 2018, 2024 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
// @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 gpgNoKeyInLegacySecring;
/***/ public String gpgNoPublicKeyFound;
/***/ public String gpgNoSecretKeyForPublicKey;
- /***/ public String gpgNoSuchAlgorithm;
/***/ public String gpgNotASigningKey;
/***/ public String gpgKeyInfo;
/***/ public String gpgSigningCancelled;
+ /***/ public String keyAlgorithmMismatch;
+ /***/ public String keyMismatch;
/***/ public String logWarnGnuPGHome;
/***/ public String logWarnGpgHomeProperty;
/***/ 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;
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
new JcaPGPContentSignerBuilder(
publicKey.getAlgorithm(),
- HashAlgorithmTags.SHA256));
+ HashAlgorithmTags.SHA256),
+ publicKey);
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
subpackets.setIssuerFingerprint(false, publicKey);
+++ /dev/null
-/*
- * 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 {
- // errorprone: "Dynamically constructed transformation
- // strings are also flagged, as they may conceal an instance
- // of ECB mode."
- @SuppressWarnings("InsecureCryptoUsage")
- 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);
- }
- }
- }
-
- @Override
- public byte[] recoverKeyData(int encAlgorithm, int aeadAlgorithm,
- byte[] s2kKey, byte[] iv, int packetTag, int keyVersion,
- byte[] keyData, byte[] pubkeyData) throws PGPException {
- throw new UnsupportedOperationException();
- }
- };
- }
-}
\ No newline at end of file
+++ /dev/null
-/*
- * 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 org.bouncycastle.asn1.ASN1ObjectIdentifier;
-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.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.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)</li>
- * <li>handle secret keys using AES/OCB as encryption (those don't have a
- * hash)</li>
- * <li>fix EC parsing to account for "flags" sub-list present for ed25519 and
- * curve25519</li>
- * <li>add support for ed25519 OIDs unknown to BouncyCastle</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
- * if an IO error occurred
- * @throws PGPException
- * if some PGP error occurred
- */
- 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());
- // JGit: c.f. https://github.com/bcgit/bc-java/issues/1590.
- // There may be a flags sub-list here for ed25519 or curve25519.
- if (type.equals("flags")) {
- SXprUtils.readString(inputStream, inputStream.read());
- SXprUtils.skipCloseParenthesis(inputStream);
- 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());
- }
-
- // JGit: BC doesn't know Ed25519 curve name.
- ASN1ObjectIdentifier curveOid = ECNamedCurveTable
- .getOID(curveName);
- if (curveOid == null) {
- curveOid = ObjectIds.getByName(curveName);
- }
- ECPublicBCPGKey basePubKey = new ECDSAPublicBCPGKey(
- curveOid,
- new BigInteger(1, qVal));
- ECPublicBCPGKey assocPubKey = (ECPublicBCPGKey) pubKey
- .getPublicKeyPacket().getKey();
- if (!ObjectIds.match(basePubKey.getCurveOID(),
- 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");
- }
-
- 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(")"));
- }
-}
+++ /dev/null
-/*
- * 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$
- }
- }
-}
/*
- * Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2021, 2024 Thomas Wolf <twolf@apache.org> 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
*/
package org.eclipse.jgit.gpg.bc.internal.keys;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.EOFException;
+import java.io.BufferedInputStream;
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.bcpg.ECPublicBCPGKey;
+import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
+import org.bouncycastle.gpg.PGPSecretKeyParser;
+import org.bouncycastle.gpg.SExprParser;
+import org.bouncycastle.openpgp.OpenedPGPKeyData;
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.JcaKeyFingerprintCalculator;
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 {
+ // Maximum nesting depth of sub-lists in an S-Expression for a secret key.
+ private static final int MAX_SEXPR_NESTING = 20;
+
private SecretKeys() {
// No instantiation.
}
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.
*
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();
+ OpenedPGPKeyData data;
+ try (InputStream keyIn = new BufferedInputStream(in)) {
+ data = PGPSecretKeyParser.parse(keyIn, MAX_SEXPR_NESTING);
}
- }
-
- /**
- * 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
- * if object stream is corrupt
- */
- 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)));
- }
+ PBEProtectionRemoverFactory decryptor = null;
+ if (isProtected(data)) {
+ decryptor = new JcePBEProtectionRemoverFactory(
+ passphraseSupplier.getPassphrase(), calculatorProvider);
}
- 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();
+ switch (publicKey.getAlgorithm()) {
+ case PublicKeyAlgorithmTags.EDDSA_LEGACY:
+ case PublicKeyAlgorithmTags.Ed25519:
+ // If we let Bouncy Castle check whether the secret key matches the
+ // given public key it may get into trouble in some cases with
+ // ed25519 keys. It appears that we may end up with secret keys
+ // using the official RFC 8410 OID for ed25519, "1.3.101.112", while
+ // the public key passed in may have a non-standard OpenPGP-specific
+ // OID "1.3.6.1.4.1.11591.15.1", or vice versa. Bouncy Castle then
+ // throws an exception because of the different OIDs.
+ //
+ // The work-around is to just read the secret key, and double-check
+ // later that the OIDs are compatible and the curve points match.
+ PGPSecretKey secret = data.getKeyData(null, calculatorProvider,
+ decryptor, new JcaKeyFingerprintCalculator(),
+ MAX_SEXPR_NESTING);
+ PGPPublicKey pubKeyRead = secret.getPublicKey();
+ int algoRead = pubKeyRead.getAlgorithm();
+ if (algoRead != PublicKeyAlgorithmTags.EDDSA_LEGACY
+ && algoRead != PublicKeyAlgorithmTags.Ed25519) {
+ throw new PGPException(BCText.get().keyAlgorithmMismatch);
}
- 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;
+ ECPublicBCPGKey ec1 = (ECPublicBCPGKey) publicKey
+ .getPublicKeyPacket().getKey();
+ ECPublicBCPGKey ec2 = (ECPublicBCPGKey) pubKeyRead
+ .getPublicKeyPacket().getKey();
+ if (!ObjectIds.match(ec1.getCurveOID(), ec2.getCurveOID())
+ || !ec1.getEncodedPoint().equals(ec2.getEncodedPoint())) {
+ throw new PGPException(
+ MessageFormat.format(BCText.get().keyMismatch,
+ ec1.getCurveOID(), ec1.getEncodedPoint(),
+ ec2.getCurveOID(), ec2.getEncodedPoint()));
}
- 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;
+ return secret;
default:
- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
- || (ch >= '0' && ch <= '9')) {
- return true;
- }
- return false;
+ // For other key types let Bouncy Castle do the check.
+ return data.getKeyData(publicKey, calculatorProvider, decryptor,
+ null, MAX_SEXPR_NESTING);
}
}
- private static boolean isHex(int ch) {
- return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
- || (ch >= 'a' && ch <= 'f');
+ private static boolean isProtected(OpenedPGPKeyData data) {
+ return SExprParser.ProtectionFormatTypeTags.PROTECTED_PRIVATE_KEY == SExprParser
+ .getProtectionType(data.getKeyExpression().getString(0));
}
- 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;
- }
}