GPG: handle extended private key format

Add detection for the key-value pair format that was available in
gpg-agent for some time already and that has become the default since
gpg-agent 2.2.20. If a secret key in the .gnupg/private-keys-v1.d
directory is found to have this format, extract the human-readable key
from it, convert it to the binary serialized form and hand that to
BouncyCastle.

Encrypted keys in the new format may use AES/OCB. OCB is a patent-
encumbered algorithm; although there is a license for open-source
software, that may not be good enough and OCB may not be available in
Java. It is not available in the default security provider in Java,
and it is also not available in the BouncyCastle version included in
Eclipse.

Implement AES/OCB decryption, throwing a PGPException with a nice
message if the algorithm is not available. Include a copy of the normal
s-expression parser of BouncyCastle and fix it to properly handle data
from such keys: such keys do not contain an internal hash since the
AES/OCB cipher includes and checks a MAC already.

Bug: 570501
Change-Id: Ifa6391a809a84cfc6ae7c6610af6a79204b4143b
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2021-01-24 02:13:43 +01:00 committed by Matthias Sohn
parent a14455dfd7
commit bdc48aeac7
20 changed files with 1902 additions and 94 deletions

View File

@ -9,6 +9,7 @@ Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
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)",
org.eclipse.jgit.gpg.bc.internal;version="[5.11.0,5.12.0)",
@ -17,6 +18,7 @@ Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.junit;version="[4.13,5.0.0)",
org.junit.runner;version="[4.13,5.0.0)",
org.junit.runners;version="[4.13,5.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true
Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true,
org.eclipse.jgit.gpg.bc.internal.keys;x-internal:=true
Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
org.hamcrest.library;bundle-version="[1.1.0,2.0.0)"

View File

@ -0,0 +1,155 @@
/*
* 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 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.io.InputStream;
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;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class SecretKeysTest {
@BeforeClass
public static void ensureBC() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
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;
TestData(String name, boolean encrypted) {
this.name = name;
this.encrypted = encrypted;
}
@Override
public String toString() {
return name;
}
}
@Parameters(name = "{0}")
public static TestData[] initTestData() {
return new TestData[] {
new TestData("2FB05DBB70FC07CB84C13431F640CA6CEA1DBF8A", false),
new TestData("66CCECEC2AB46A9735B10FEC54EDF9FD0F77BAF9", true),
new TestData("F727FAB884DA3BD402B6E0F5472E108D21033124", true),
new TestData("faked", false) };
}
private static byte[] readTestKey(String filename) throws Exception {
try (InputStream in = new BufferedInputStream(
SecretKeysTest.class.getResourceAsStream(filename))) {
return SecretKeys.keyFromNameValueFormat(in);
}
}
private static PGPPublicKey readAsc(InputStream in)
throws IOException, PGPException {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());
Iterator<PGPPublicKeyRing> keyRings = pgpPub.getKeyRings();
while (keyRings.hasNext()) {
PGPPublicKeyRing keyRing = keyRings.next();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
if (keys.hasNext()) {
return keys.next();
}
}
return null;
}
// Injected by JUnit
@Parameter
public TestData data;
@Test
public void testKeyRead() throws Exception {
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) {
PGPPublicKey publicKey = readAsc(pubIn);
// Do a full test trying to load the secret key.
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
try (InputStream in = new BufferedInputStream(this.getClass()
.getResourceAsStream(data.name + ".key"))) {
PGPSecretKey secretKey = SecretKeys.readSecretKey(in,
calculatorProvider, () -> "nonsense".toCharArray(),
publicKey);
assertNotNull(secretKey);
} catch (PGPException e) {
// Currently we may not be able to load OCB-encrypted keys.
assertTrue(e.getMessage().contains("OCB"));
assertTrue(data.encrypted);
assertFalse(ocbAvailable());
}
}
}
}
}

View File

@ -18,6 +18,7 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.interfaces;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.util;version="[1.65.0,2.0.0)",
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.math.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.math.field;version="[1.65.0,2.0.0)",
@ -25,14 +26,11 @@ Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.util;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
org.bouncycastle.util.io;version="[1.65.0,2.0.0)",
org.eclipse.jgit.annotations;version="[5.11.0,5.12.0)",
org.eclipse.jgit.api.errors;version="[5.11.0,5.12.0)",
org.eclipse.jgit.errors;version="[5.11.0,5.12.0)",
org.eclipse.jgit.lib;version="[5.11.0,5.12.0)",
org.eclipse.jgit.nls;version="[5.11.0,5.12.0)",
org.eclipse.jgit.transport;version="[5.11.0,5.12.0)",
org.eclipse.jgit.util;version="[5.11.0,5.12.0)",
org.slf4j;version="[1.7.0,2.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc;version="5.11.0",
org.eclipse.jgit.gpg.bc.internal;version="5.11.0";x-friends:="org.eclipse.jgit.gpg.bc.test",

View File

@ -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>

View File

@ -1,5 +1,7 @@
corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
cryptCipherError=Cannot create cipher to decrypt: {0}
cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes
gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
gpgNoCredentialsProvider=missing credentials provider
gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
@ -7,10 +9,20 @@ gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
gpgNoSecretKeyForPublicKey=unable to find associated secret key for public key: {0}
gpgNoSuchAlgorithm=Cannot decrypt encrypted secret key: encryption algorithm {0} is not available
gpgNotASigningKey=Secret key ({0}) is not suitable for signing
gpgKeyInfo=GPG Key (fingerprint {0})
gpgSigningCancelled=Signing was cancelled
nonSignatureError=Signature does not decode into a signature object
secretKeyTooShort=Secret key file corrupt; only {0} bytes read
sexprHexNotClosed=Hex number in s-expression not closed
sexprHexOdd=Hex number in s-expression has an odd number of digits
sexprStringInvalidEscape=Invalid escape {0} in s-expression
sexprStringInvalidEscapeAtEnd=Invalid s-expression: quoted string ends with escape character
sexprStringInvalidHexEscape=Invalid hex escape in s-expression
sexprStringInvalidOctalEscape=Invalid octal escape in s-expression
sexprStringNotClosed=String in s-expression not closed
sexprUnhandled=Unhandled token {0} in s-expression
signatureInconsistent=Inconsistent signature; key ID {0} does not match issuer fingerprint {1}
signatureKeyLookupError=Error occurred while looking for public key
signatureNoKeyInfo=No way to determine a public key from the signature

View File

@ -29,6 +29,8 @@ public static BCText get() {
// @formatter:off
/***/ public String corrupt25519Key;
/***/ public String credentialPassphrase;
/***/ public String cryptCipherError;
/***/ public String cryptWrongDecryptedLength;
/***/ public String gpgFailedToParseSecretKey;
/***/ public String gpgNoCredentialsProvider;
/***/ public String gpgNoKeygrip;
@ -36,10 +38,20 @@ public static BCText get() {
/***/ 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;

View File

@ -30,7 +30,6 @@
import java.util.Iterator;
import java.util.Locale;
import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.KeyBox;
@ -48,16 +47,15 @@
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
import org.eclipse.jgit.gpg.bc.internal.keys.SecretKeys;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.SystemReader;
@ -77,17 +75,10 @@ private static class NoOpenPgpKeyException extends Exception {
}
/** Thrown if we try to read an encrypted private key without password. */
private static class EncryptedPgpKeyException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
private static final Logger log = LoggerFactory
.getLogger(BouncyCastleGpgKeyLocator.class);
private static final Path GPG_DIRECTORY = findGpgDirectory();
static final Path GPG_DIRECTORY = findGpgDirectory();
private static final Path USER_KEYBOX_PATH = GPG_DIRECTORY
.resolve("pubring.kbx"); //$NON-NLS-1$
@ -154,11 +145,13 @@ public BouncyCastleGpgKeyLocator(String signingKey,
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
PBEProtectionRemoverFactory passphraseProvider,
PGPPublicKey publicKey) throws IOException, PGPException {
SecretKeys.PassphraseSupplier passphraseSupplier,
PGPPublicKey publicKey)
throws IOException, PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException {
try (InputStream in = newInputStream(keyFile)) {
return new SExprParser(calculatorProvider).parseSecretKey(
new BufferedInputStream(in), passphraseProvider, publicKey);
return SecretKeys.readSecretKey(in, calculatorProvider,
passphraseSupplier, publicKey);
}
}
@ -483,29 +476,17 @@ private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
try {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
PBEProtectionRemoverFactory passphraseProvider = p -> {
throw new EncryptedPgpKeyException();
};
clearPrompt = true;
PGPSecretKey secretKey = null;
try {
// Try without passphrase
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (EncryptedPgpKeyException e) {
// Let's try again with a passphrase
passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath));
clearPrompt = true;
try {
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (PGPException e1) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
}
() -> passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath),
publicKey);
} catch (PGPException e) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
}
if (secretKey != null) {
if (!secretKey.isSigningKey()) {

View File

@ -17,8 +17,8 @@
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 @@
*/
class BouncyCastleGpgKeyPassphrasePrompt implements AutoCloseable {
private CharArrayType passphrase;
private Password passphrase;
private CredentialsProvider credentialsProvider;
@ -78,8 +78,7 @@ public char[] getPassphrase(byte[] keyFingerprint, Path keyLocation)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
if (passphrase == null) {
passphrase = new CharArrayType(BCText.get().credentialPassphrase,
true);
passphrase = new Password(BCText.get().credentialPassphrase);
}
if (credentialsProvider == null) {

View File

@ -49,7 +49,7 @@
import org.eclipse.jgit.util.StringUtils;
/**
* GPG Signer using BouncyCastle library
* GPG Signer using the BouncyCastle library.
*/
public class BouncyCastleGpgSigner extends GpgSigner
implements GpgObjectSigner {
@ -97,8 +97,9 @@ public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
return gpgKey != null;
} catch (PGPException | IOException | NoSuchAlgorithmException
| NoSuchProviderException | URISyntaxException e) {
} catch (CanceledException e) {
throw e;
} catch (Exception e) {
return false;
}
}
@ -143,7 +144,8 @@ public void signObject(@NonNull ObjectBuilder object,
try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
credentialsProvider)) {
BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
committer, passphrasePrompt);
committer,
passphrasePrompt);
PGPSecretKey secretKey = gpgKey.getSecretKey();
if (secretKey == null) {
throw new JGitInternalException(

View File

@ -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);
}
}
}
};
}
}

View File

@ -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(")"));
}
}

View File

@ -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$
}
}
}

View File

@ -0,0 +1,597 @@
/*
* Copyright (C) 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal.keys;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.operator.PBEProtectionRemoverFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEProtectionRemoverFactory;
import org.bouncycastle.util.io.Streams;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.BCText;
import org.eclipse.jgit.util.RawParseUtils;
/**
* Utilities for reading GPG secret keys from a gpg-agent key file.
*/
public final class SecretKeys {
private SecretKeys() {
// No instantiation.
}
/**
* Something that can supply a passphrase to decrypt an encrypted secret
* key.
*/
public interface PassphraseSupplier {
/**
* Supplies a passphrase.
*
* @return the passphrase
* @throws PGPException
* if no passphrase can be obtained
* @throws CanceledException
* if the user canceled passphrase entry
* @throws UnsupportedCredentialItem
* if an internal error occurred
* @throws URISyntaxException
* if an internal error occurred
*/
char[] getPassphrase() throws PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException;
}
private static final byte[] PROTECTED_KEY = "protected-private-key" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);
private static final byte[] OCB_PROTECTED = "openpgp-s2k3-ocb-aes" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);
/**
* Reads a GPG secret key from the given stream.
*
* @param in
* {@link InputStream} to read from, doesn't need to be buffered
* @param calculatorProvider
* for checking digests
* @param passphraseSupplier
* for decrypting encrypted keys
* @param publicKey
* the secret key should be for
* @return the secret key
* @throws IOException
* if the stream cannot be parsed
* @throws PGPException
* if thrown by the underlying S-Expression parser, for instance
* when the passphrase is wrong
* @throws CanceledException
* if thrown by the {@code passphraseSupplier}
* @throws UnsupportedCredentialItem
* if thrown by the {@code passphraseSupplier}
* @throws URISyntaxException
* if thrown by the {@code passphraseSupplier}
*/
public static PGPSecretKey readSecretKey(InputStream in,
PGPDigestCalculatorProvider calculatorProvider,
PassphraseSupplier passphraseSupplier, PGPPublicKey publicKey)
throws IOException, PGPException, CanceledException,
UnsupportedCredentialItem, URISyntaxException {
byte[] data = Streams.readAll(in);
if (data.length == 0) {
throw new EOFException();
} else if (data.length < 4 + PROTECTED_KEY.length) {
// +4 for "(21:" for a binary protected key
throw new IOException(
MessageFormat.format(BCText.get().secretKeyTooShort,
Integer.toUnsignedString(data.length)));
}
SExprParser parser = new SExprParser(calculatorProvider);
byte firstChar = data[0];
try {
if (firstChar == '(') {
// Binary format.
if (!matches(data, 4, PROTECTED_KEY)) {
// Not encrypted binary format.
return parser.parseSecretKey(in, null, publicKey);
}
// AES/CBC encrypted.
PBEProtectionRemoverFactory decryptor = new JcePBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(), calculatorProvider);
try (InputStream sIn = new ByteArrayInputStream(data)) {
return parser.parseSecretKey(sIn, decryptor, publicKey);
}
}
// Assume it's the new key-value format.
try (ByteArrayInputStream keyIn = new ByteArrayInputStream(data)) {
byte[] rawData = keyFromNameValueFormat(keyIn);
if (!matches(rawData, 1, PROTECTED_KEY)) {
// Not encrypted human-readable format.
try (InputStream sIn = new ByteArrayInputStream(
convertSexpression(rawData))) {
return parser.parseSecretKey(sIn, null, publicKey);
}
}
// An encrypted key from a key-value file. Most likely AES/OCB
// encrypted.
boolean isOCB[] = { false };
byte[] sExp = convertSexpression(rawData, isOCB);
PBEProtectionRemoverFactory decryptor;
if (isOCB[0]) {
decryptor = new OCBPBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(),
calculatorProvider, getAad(sExp));
} else {
decryptor = new JcePBEProtectionRemoverFactory(
passphraseSupplier.getPassphrase(),
calculatorProvider);
}
try (InputStream sIn = new ByteArrayInputStream(sExp)) {
return parser.parseSecretKey(sIn, decryptor, publicKey);
}
}
} catch (IOException e) {
throw new PGPException(e.getLocalizedMessage(), e);
}
}
/**
* Extract the AAD for the OCB decryption from an s-expression.
*
* @param sExp
* buffer containing a valid binary s-expression
* @return the AAD
*/
private static byte[] getAad(byte[] sExp) {
// Given a key
// @formatter:off
// (protected-private-key (rsa ... (protected openpgp-s2k3-ocb-aes ... )(protected-at ...)))
// A B C D
// The AAD is [A..B)[C..D). (From the binary serialized form.)
// @formatter:on
int i = 1; // Skip initial '('
while (sExp[i] != '(') {
i++;
}
int aadStart = i++;
int aadEnd = skip(sExp, aadStart);
byte[] protectedPrefix = "(9:protected" //$NON-NLS-1$
.getBytes(StandardCharsets.US_ASCII);
while (!matches(sExp, i, protectedPrefix)) {
i++;
}
int protectedStart = i;
int protectedEnd = skip(sExp, protectedStart);
byte[] aadData = new byte[aadEnd - aadStart
- (protectedEnd - protectedStart)];
System.arraycopy(sExp, aadStart, aadData, 0, protectedStart - aadStart);
System.arraycopy(sExp, protectedEnd, aadData, protectedStart - aadStart,
aadEnd - protectedEnd);
return aadData;
}
/**
* Skips a list including nested lists.
*
* @param sExp
* buffer containing valid binary s-expression data
* @param start
* index of the opening '(' of the list to skip
* @return the index after the closing ')' of the skipped list
*/
private static int skip(byte[] sExp, int start) {
int i = start + 1;
int depth = 1;
while (depth > 0) {
switch (sExp[i]) {
case '(':
depth++;
break;
case ')':
depth--;
break;
default:
// We must be on a length
int j = i;
while (sExp[j] >= '0' && sExp[j] <= '9') {
j++;
}
// j is on the colon
int length = Integer.parseInt(
new String(sExp, i, j - i, StandardCharsets.US_ASCII));
i = j + length;
}
i++;
}
return i;
}
/**
* Checks whether the {@code needle} matches {@code src} at offset
* {@code from}.
*
* @param src
* to match against {@code needle}
* @param from
* position in {@code src} to start matching
* @param needle
* to match against
* @return {@code true} if {@code src} contains {@code needle} at position
* {@code from}, {@code false} otherwise
*/
private static boolean matches(byte[] src, int from, byte[] needle) {
if (from < 0 || from + needle.length > src.length) {
return false;
}
return org.bouncycastle.util.Arrays.constantTimeAreEqual(needle.length,
src, from, needle, 0);
}
/**
* Converts a human-readable serialized s-expression into a binary
* serialized s-expression.
*
* @param humanForm
* to convert
* @return the converted s-expression
* @throws IOException
* if the conversion fails
*/
private static byte[] convertSexpression(byte[] humanForm)
throws IOException {
boolean[] isOCB = { false };
return convertSexpression(humanForm, isOCB);
}
/**
* Converts a human-readable serialized s-expression into a binary
* serialized s-expression.
*
* @param humanForm
* to convert
* @param isOCB
* returns whether the s-expression specified AES/OCB encryption
* @return the converted s-expression
* @throws IOException
* if the conversion fails
*/
private static byte[] convertSexpression(byte[] humanForm, boolean[] isOCB)
throws IOException {
int pos = 0;
try (ByteArrayOutputStream out = new ByteArrayOutputStream(
humanForm.length)) {
while (pos < humanForm.length) {
byte b = humanForm[pos];
if (b == '(' || b == ')') {
out.write(b);
pos++;
} else if (isGpgSpace(b)) {
pos++;
} else if (b == '#') {
// Hex value follows up to the next #
int i = ++pos;
while (i < humanForm.length && isHex(humanForm[i])) {
i++;
}
if (i == pos || humanForm[i] != '#') {
throw new StreamCorruptedException(
BCText.get().sexprHexNotClosed);
}
if ((i - pos) % 2 != 0) {
throw new StreamCorruptedException(
BCText.get().sexprHexOdd);
}
int l = (i - pos) / 2;
out.write(Integer.toString(l)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
while (pos < i) {
int x = (nibble(humanForm[pos]) << 4)
| nibble(humanForm[pos + 1]);
pos += 2;
out.write(x);
}
pos = i + 1;
} else if (isTokenChar(b)) {
// Scan the token
int start = pos++;
while (pos < humanForm.length
&& isTokenChar(humanForm[pos])) {
pos++;
}
int l = pos - start;
if (pos - start == OCB_PROTECTED.length
&& matches(humanForm, start, OCB_PROTECTED)) {
isOCB[0] = true;
}
out.write(Integer.toString(l)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
out.write(humanForm, start, pos - start);
} else if (b == '"') {
// Potentially quoted string.
int start = ++pos;
boolean escaped = false;
while (pos < humanForm.length
&& (escaped || humanForm[pos] != '"')) {
int ch = humanForm[pos++];
escaped = !escaped && ch == '\\';
}
if (pos >= humanForm.length) {
throw new StreamCorruptedException(
BCText.get().sexprStringNotClosed);
}
// start is on the first character of the string, pos on the
// closing quote.
byte[] dq = dequote(humanForm, start, pos);
out.write(Integer.toString(dq.length)
.getBytes(StandardCharsets.US_ASCII));
out.write(':');
out.write(dq);
pos++;
} else {
throw new StreamCorruptedException(
MessageFormat.format(BCText.get().sexprUnhandled,
Integer.toHexString(b & 0xFF)));
}
}
return out.toByteArray();
}
}
/**
* GPG-style string de-quoting, which is basically C-style, with some
* literal CR/LF escaping.
*
* @param in
* buffer containing the quoted string
* @param from
* index after the opening quote in {@code in}
* @param to
* index of the closing quote in {@code in}
* @return the dequoted raw string value
* @throws StreamCorruptedException
*/
private static byte[] dequote(byte[] in, int from, int to)
throws StreamCorruptedException {
// Result must be shorter or have the same length
byte[] out = new byte[to - from];
int j = 0;
int i = from;
while (i < to) {
byte b = in[i++];
if (b != '\\') {
out[j++] = b;
continue;
}
if (i == to) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidEscapeAtEnd);
}
b = in[i++];
switch (b) {
case 'b':
out[j++] = '\b';
break;
case 'f':
out[j++] = '\f';
break;
case 'n':
out[j++] = '\n';
break;
case 'r':
out[j++] = '\r';
break;
case 't':
out[j++] = '\t';
break;
case 'v':
out[j++] = 0x0B;
break;
case '"':
case '\'':
case '\\':
out[j++] = b;
break;
case '\r':
// Escaped literal line end. If an LF is following, skip that,
// too.
if (i < to && in[i] == '\n') {
i++;
}
break;
case '\n':
// Same for LF possibly followed by CR.
if (i < to && in[i] == '\r') {
i++;
}
break;
case 'x':
if (i + 1 >= to || !isHex(in[i]) || !isHex(in[i + 1])) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidHexEscape);
}
out[j++] = (byte) ((nibble(in[i]) << 4) | nibble(in[i + 1]));
i += 2;
break;
case '0':
case '1':
case '2':
case '3':
if (i + 2 >= to || !isOctal(in[i]) || !isOctal(in[i + 1])
|| !isOctal(in[i + 2])) {
throw new StreamCorruptedException(
BCText.get().sexprStringInvalidOctalEscape);
}
out[j++] = (byte) (((((in[i] - '0') << 3)
| (in[i + 1] - '0')) << 3) | (in[i + 2] - '0'));
i += 3;
break;
default:
throw new StreamCorruptedException(MessageFormat.format(
BCText.get().sexprStringInvalidEscape,
Integer.toHexString(b & 0xFF)));
}
}
return Arrays.copyOf(out, j);
}
/**
* Extracts the key from a GPG name-value-pair key file.
* <p>
* Package-visible for tests only.
* </p>
*
* @param in
* {@link InputStream} to read from; should be buffered
* @return the raw key data as extracted from the file
* @throws IOException
* if the {@code in} stream cannot be read or does not contain a
* key
*/
static byte[] keyFromNameValueFormat(InputStream in) throws IOException {
// It would be nice if we could use RawParseUtils here, but GPG compares
// names case-insensitively. We're only interested in the "Key:"
// name-value pair.
int[] nameLow = { 'k', 'e', 'y', ':' };
int[] nameCap = { 'K', 'E', 'Y', ':' };
int nameIdx = 0;
for (;;) {
int next = in.read();
if (next < 0) {
throw new EOFException();
}
if (next == '\n') {
nameIdx = 0;
} else if (nameIdx >= 0) {
if (nameLow[nameIdx] == next || nameCap[nameIdx] == next) {
nameIdx++;
if (nameIdx == nameLow.length) {
break;
}
} else {
nameIdx = -1;
}
}
}
// We're after "Key:". Read the value as continuation lines.
int last = ':';
byte[] rawData;
try (ByteArrayOutputStream out = new ByteArrayOutputStream(8192)) {
for (;;) {
int next = in.read();
if (next < 0) {
break;
}
if (last == '\n') {
if (next == ' ' || next == '\t') {
// Continuation line; skip this whitespace
last = next;
continue;
}
break; // Not a continuation line
}
out.write(next);
last = next;
}
rawData = out.toByteArray();
}
// GPG trims off trailing whitespace, and a line having only whitespace
// is a single LF.
try (ByteArrayOutputStream out = new ByteArrayOutputStream(
rawData.length)) {
int lineStart = 0;
boolean trimLeading = true;
while (lineStart < rawData.length) {
int nextLineStart = RawParseUtils.nextLF(rawData, lineStart);
if (trimLeading) {
while (lineStart < nextLineStart
&& isGpgSpace(rawData[lineStart])) {
lineStart++;
}
}
// Trim trailing
int i = nextLineStart - 1;
while (lineStart < i && isGpgSpace(rawData[i])) {
i--;
}
if (i <= lineStart) {
// Empty line signifies LF
out.write('\n');
trimLeading = true;
} else {
out.write(rawData, lineStart, i - lineStart + 1);
trimLeading = false;
}
lineStart = nextLineStart;
}
return out.toByteArray();
}
}
private static boolean isGpgSpace(int ch) {
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n';
}
private static boolean isTokenChar(int ch) {
switch (ch) {
case '-':
case '.':
case '/':
case '_':
case ':':
case '*':
case '+':
case '=':
return true;
default:
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
|| (ch >= '0' && ch <= '9')) {
return true;
}
return false;
}
}
private static boolean isHex(int ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')
|| (ch >= 'a' && ch <= 'f');
}
private static boolean isOctal(int ch) {
return (ch >= '0' && ch <= '7');
}
private static int nibble(int ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
} else if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
}
return -1;
}
}