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