From 7b24ee7b55db99467dd63e631ba55a27d08587d5 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sun, 1 Feb 2015 23:14:26 +0100 Subject: rewrite PgpSignEncrypt data flow - introduce high-level SignEncryptOperation for uri to uri signing/encryption - use SignEncryptParcel for high-level operation parameters - use PgpSignEncryptInput plus streams for low-level operation parameters - get rid of all sign/encrypt logic in KeychainIntentService --- .../keychain/pgp/PgpSignEncryptOperation.java | 578 +++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java (limited to 'OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java') diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java new file mode 100644 index 000000000..2fa01d241 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2010-2014 Thialfihar + * Copyright (C) 2014 Vincent Breitmoser + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.pgp; + +import android.content.Context; + +import org.spongycastle.bcpg.ArmoredOutputStream; +import org.spongycastle.bcpg.BCPGOutputStream; +import org.spongycastle.bcpg.CompressionAlgorithmTags; +import org.spongycastle.openpgp.PGPCompressedDataGenerator; +import org.spongycastle.openpgp.PGPEncryptedData; +import org.spongycastle.openpgp.PGPEncryptedDataGenerator; +import org.spongycastle.openpgp.PGPException; +import org.spongycastle.openpgp.PGPLiteralData; +import org.spongycastle.openpgp.PGPLiteralDataGenerator; +import org.spongycastle.openpgp.PGPSignatureGenerator; +import org.spongycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator; +import org.spongycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; +import org.spongycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder; +import org.spongycastle.util.encoders.Hex; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.operations.BaseOperation; +import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; +import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; +import org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult; +import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException; +import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils; +import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.ProgressScaler; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** This class supports a single, low-level, sign/encrypt operation. + * + * The operation of this class takes an Input- and OutputStream plus a + * PgpSignEncryptInput, and signs and/or encrypts the stream as + * parametrized in the PgpSignEncryptInput object. It returns its status + * and a possible detached signature as a SignEncryptResult. + * + * For a high-level operation based on URIs, see SignEncryptOperation. + * + * @see org.sufficientlysecure.keychain.pgp.PgpSignEncryptInput + * @see org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult + * @see org.sufficientlysecure.keychain.operations.SignEncryptOperation + * + */ +public class PgpSignEncryptOperation extends BaseOperation { + + private static byte[] NEW_LINE; + + static { + try { + NEW_LINE = "\r\n".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(Constants.TAG, "UnsupportedEncodingException", e); + } + } + + public PgpSignEncryptOperation(Context context, ProviderHelper providerHelper, Progressable progressable, AtomicBoolean cancelled) { + super(context, providerHelper, progressable, cancelled); + } + + public PgpSignEncryptOperation(Context context, ProviderHelper providerHelper, Progressable progressable) { + super(context, providerHelper, progressable); + } + + /** + * Signs and/or encrypts data based on parameters of class + */ + public PgpSignEncryptResult execute(PgpSignEncryptInput input, + InputData inputData, OutputStream outputStream) { + + int indent = 0; + OperationLog log = new OperationLog(); + + log.add(LogType.MSG_PSE, indent); + indent += 1; + + boolean enableSignature = input.getSignatureMasterKeyId() != Constants.key.none; + boolean enableEncryption = ((input.getEncryptionMasterKeyIds() != null && input.getEncryptionMasterKeyIds().length > 0) + || input.getSymmetricPassphrase() != null); + boolean enableCompression = (input.getCompressionId() != CompressionAlgorithmTags.UNCOMPRESSED); + + Log.d(Constants.TAG, "enableSignature:" + enableSignature + + "\nenableEncryption:" + enableEncryption + + "\nenableCompression:" + enableCompression + + "\nenableAsciiArmorOutput:" + input.ismEnableAsciiArmorOutput()); + + // add additional key id to encryption ids (mostly to do self-encryption) + if (enableEncryption && input.getAdditionalEncryptId() != Constants.key.none) { + input.setEncryptionMasterKeyIds(Arrays.copyOf(input.getEncryptionMasterKeyIds(), input.getEncryptionMasterKeyIds().length + 1)); + input.getEncryptionMasterKeyIds()[input.getEncryptionMasterKeyIds().length - 1] = input.getAdditionalEncryptId(); + } + + ArmoredOutputStream armorOut = null; + OutputStream out; + if (input.ismEnableAsciiArmorOutput()) { + armorOut = new ArmoredOutputStream(outputStream); + if (input.getVersionHeader() != null) { + armorOut.setHeader("Version", input.getVersionHeader()); + } + // if we have a charset, put it in the header + if (input.getCharset() != null) { + armorOut.setHeader("Charset", input.getCharset()); + } + out = armorOut; + } else { + out = outputStream; + } + + /* Get keys for signature generation for later usage */ + CanonicalizedSecretKey signingKey = null; + if (enableSignature) { + + try { + // fetch the indicated master key id (the one whose name we sign in) + CanonicalizedSecretKeyRing signingKeyRing = + mProviderHelper.getCanonicalizedSecretKeyRing(input.getSignatureMasterKeyId()); + + long signKeyId; + // use specified signing subkey, or find the one to use + if (input.getSignatureSubKeyId() == null) { + signKeyId = signingKeyRing.getSecretSignId(); + } else { + signKeyId = input.getSignatureSubKeyId(); + } + + // fetch the specific subkey to sign with, or just use the master key if none specified + signingKey = signingKeyRing.getSecretKey(signKeyId); + + } catch (ProviderHelper.NotFoundException | PgpGeneralException e) { + log.add(LogType.MSG_PSE_ERROR_SIGN_KEY, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + + // Make sure we are allowed to sign here! + if (!signingKey.canSign()) { + log.add(LogType.MSG_PSE_ERROR_KEY_SIGN, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + + // if no passphrase was explicitly set try to get it from the cache service + if (input.getSignaturePassphrase() == null) { + try { + // returns "" if key has no passphrase + input.setSignaturePassphrase(getCachedPassphrase(signingKey.getKeyId())); + // TODO +// log.add(LogType.MSG_DC_PASS_CACHED, indent + 1); + } catch (PassphraseCacheInterface.NoSecretKeyException e) { + // TODO +// log.add(LogType.MSG_DC_ERROR_NO_KEY, indent + 1); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + + // if passphrase was not cached, return here indicating that a passphrase is missing! + if (input.getSignaturePassphrase() == null) { + log.add(LogType.MSG_PSE_PENDING_PASSPHRASE, indent + 1); + PgpSignEncryptResult result = new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_PENDING_PASSPHRASE, log); + result.setKeyIdPassphraseNeeded(signingKey.getKeyId()); + return result; + } + } + + updateProgress(R.string.progress_extracting_signature_key, 0, 100); + + try { + if (!signingKey.unlock(input.getSignaturePassphrase())) { + log.add(LogType.MSG_PSE_ERROR_BAD_PASSPHRASE, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + } catch (PgpGeneralException e) { + log.add(LogType.MSG_PSE_ERROR_UNLOCK, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + + // check if hash algo is supported + int requestedAlgorithm = input.getSignatureHashAlgorithm(); + LinkedList supported = signingKey.getSupportedHashAlgorithms(); + if (requestedAlgorithm == 0) { + // get most preferred + input.setSignatureHashAlgorithm(supported.getLast()); + } else if (!supported.contains(requestedAlgorithm)) { + log.add(LogType.MSG_PSE_ERROR_HASH_ALGO, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + } + updateProgress(R.string.progress_preparing_streams, 2, 100); + + /* Initialize PGPEncryptedDataGenerator for later usage */ + PGPEncryptedDataGenerator cPk = null; + if (enableEncryption) { + int algo = input.getSymmetricEncryptionAlgorithm(); + if (algo == 0) { + algo = PGPEncryptedData.AES_128; + } + // has Integrity packet enabled! + JcePGPDataEncryptorBuilder encryptorBuilder = + new JcePGPDataEncryptorBuilder(algo) + .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME) + .setWithIntegrityPacket(true); + + cPk = new PGPEncryptedDataGenerator(encryptorBuilder); + + if (input.getSymmetricPassphrase() != null) { + // Symmetric encryption + log.add(LogType.MSG_PSE_SYMMETRIC, indent); + + JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator = + new JcePBEKeyEncryptionMethodGenerator(input.getSymmetricPassphrase().toCharArray()); + cPk.addMethod(symmetricEncryptionGenerator); + } else { + log.add(LogType.MSG_PSE_ASYMMETRIC, indent); + + // Asymmetric encryption + for (long id : input.getEncryptionMasterKeyIds()) { + try { + CanonicalizedPublicKeyRing keyRing = mProviderHelper.getCanonicalizedPublicKeyRing( + KeyRings.buildUnifiedKeyRingUri(id)); + CanonicalizedPublicKey key = keyRing.getEncryptionSubKey(); + cPk.addMethod(key.getPubKeyEncryptionGenerator()); + log.add(LogType.MSG_PSE_KEY_OK, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(id)); + } catch (PgpKeyNotFoundException e) { + log.add(LogType.MSG_PSE_KEY_WARN, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(id)); + if (input.ismFailOnMissingEncryptionKeyIds()) { + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + } catch (ProviderHelper.NotFoundException e) { + log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1, + KeyFormattingUtils.convertKeyIdToHex(id)); + if (input.ismFailOnMissingEncryptionKeyIds()) { + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + } + } + } + } + + /* Initialize signature generator object for later usage */ + PGPSignatureGenerator signatureGenerator = null; + if (enableSignature) { + updateProgress(R.string.progress_preparing_signature, 4, 100); + + try { + boolean cleartext = input.isCleartextSignature() && input.ismEnableAsciiArmorOutput() && !enableEncryption; + signatureGenerator = signingKey.getSignatureGenerator( + input.getSignatureHashAlgorithm(), cleartext, input.getNfcSignedHash(), input.getNfcCreationTimestamp()); + } catch (PgpGeneralException e) { + log.add(LogType.MSG_PSE_ERROR_NFC, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + } + + ProgressScaler progressScaler = + new ProgressScaler(mProgressable, 8, 95, 100); + PGPCompressedDataGenerator compressGen = null; + OutputStream pOut; + OutputStream encryptionOut = null; + BCPGOutputStream bcpgOut; + + ByteArrayOutputStream detachedByteOut = null; + ArmoredOutputStream detachedArmorOut = null; + BCPGOutputStream detachedBcpgOut = null; + + try { + + if (enableEncryption) { + /* actual encryption */ + updateProgress(R.string.progress_encrypting, 8, 100); + log.add(enableSignature + ? LogType.MSG_PSE_SIGCRYPTING + : LogType.MSG_PSE_ENCRYPTING, + indent + ); + indent += 1; + + encryptionOut = cPk.open(out, new byte[1 << 16]); + + if (enableCompression) { + log.add(LogType.MSG_PSE_COMPRESSING, indent); + compressGen = new PGPCompressedDataGenerator(input.getCompressionId()); + bcpgOut = new BCPGOutputStream(compressGen.open(encryptionOut)); + } else { + bcpgOut = new BCPGOutputStream(encryptionOut); + } + + if (enableSignature) { + signatureGenerator.generateOnePassVersion(false).encode(bcpgOut); + } + + PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator(); + char literalDataFormatTag; + if (input.isCleartextSignature()) { + literalDataFormatTag = PGPLiteralData.UTF8; + } else { + literalDataFormatTag = PGPLiteralData.BINARY; + } + pOut = literalGen.open(bcpgOut, literalDataFormatTag, + inputData.getOriginalFilename(), new Date(), new byte[1 << 16]); + + long alreadyWritten = 0; + int length; + byte[] buffer = new byte[1 << 16]; + InputStream in = inputData.getInputStream(); + while ((length = in.read(buffer)) > 0) { + pOut.write(buffer, 0, length); + + // update signature buffer if signature is requested + if (enableSignature) { + signatureGenerator.update(buffer, 0, length); + } + + alreadyWritten += length; + if (inputData.getSize() > 0) { + long progress = 100 * alreadyWritten / inputData.getSize(); + progressScaler.setProgress((int) progress, 100); + } + } + + literalGen.close(); + indent -= 1; + + } else if (enableSignature && input.isCleartextSignature() && input.ismEnableAsciiArmorOutput()) { + /* cleartext signature: sign-only of ascii text */ + + updateProgress(R.string.progress_signing, 8, 100); + log.add(LogType.MSG_PSE_SIGNING_CLEARTEXT, indent); + + // write -----BEGIN PGP SIGNED MESSAGE----- + armorOut.beginClearText(input.getSignatureHashAlgorithm()); + + InputStream in = inputData.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + + // update signature buffer with first line + processLine(reader.readLine(), armorOut, signatureGenerator); + + // TODO: progress: fake annealing? + while (true) { + String line = reader.readLine(); + + // end cleartext signature with newline, see http://tools.ietf.org/html/rfc4880#section-7 + if (line == null) { + armorOut.write(NEW_LINE); + break; + } + + armorOut.write(NEW_LINE); + + // update signature buffer with input line + signatureGenerator.update(NEW_LINE); + processLine(line, armorOut, signatureGenerator); + } + + armorOut.endClearText(); + + pOut = new BCPGOutputStream(armorOut); + } else if (enableSignature && input.isDetachedSignature()) { + /* detached signature */ + + updateProgress(R.string.progress_signing, 8, 100); + log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent); + + InputStream in = inputData.getInputStream(); + + // handle output stream separately for detached signatures + detachedByteOut = new ByteArrayOutputStream(); + OutputStream detachedOut = detachedByteOut; + if (input.ismEnableAsciiArmorOutput()) { + detachedArmorOut = new ArmoredOutputStream(detachedOut); + if (input.getVersionHeader() != null) { + detachedArmorOut.setHeader("Version", input.getVersionHeader()); + } + + detachedOut = detachedArmorOut; + } + detachedBcpgOut = new BCPGOutputStream(detachedOut); + + long alreadyWritten = 0; + int length; + byte[] buffer = new byte[1 << 16]; + while ((length = in.read(buffer)) > 0) { + // no output stream is written, no changed to original data! + + signatureGenerator.update(buffer, 0, length); + + alreadyWritten += length; + if (inputData.getSize() > 0) { + long progress = 100 * alreadyWritten / inputData.getSize(); + progressScaler.setProgress((int) progress, 100); + } + } + + pOut = null; + } else if (enableSignature && !input.isCleartextSignature() && !input.isDetachedSignature()) { + /* sign-only binary (files/data stream) */ + + updateProgress(R.string.progress_signing, 8, 100); + log.add(LogType.MSG_PSE_SIGNING, indent); + + InputStream in = inputData.getInputStream(); + + if (enableCompression) { + compressGen = new PGPCompressedDataGenerator(input.getCompressionId()); + bcpgOut = new BCPGOutputStream(compressGen.open(out)); + } else { + bcpgOut = new BCPGOutputStream(out); + } + + signatureGenerator.generateOnePassVersion(false).encode(bcpgOut); + + PGPLiteralDataGenerator literalGen = new PGPLiteralDataGenerator(); + pOut = literalGen.open(bcpgOut, PGPLiteralData.BINARY, + inputData.getOriginalFilename(), new Date(), + new byte[1 << 16]); + + long alreadyWritten = 0; + int length; + byte[] buffer = new byte[1 << 16]; + while ((length = in.read(buffer)) > 0) { + pOut.write(buffer, 0, length); + + signatureGenerator.update(buffer, 0, length); + + alreadyWritten += length; + if (inputData.getSize() > 0) { + long progress = 100 * alreadyWritten / inputData.getSize(); + progressScaler.setProgress((int) progress, 100); + } + } + + literalGen.close(); + } else { + pOut = null; + // TODO: Is this log right? + log.add(LogType.MSG_PSE_CLEARSIGN_ONLY, indent); + } + + if (enableSignature) { + updateProgress(R.string.progress_generating_signature, 95, 100); + try { + if (detachedBcpgOut != null) { + signatureGenerator.generate().encode(detachedBcpgOut); + } else { + signatureGenerator.generate().encode(pOut); + } + } catch (NfcSyncPGPContentSignerBuilder.NfcInteractionNeeded e) { + // this secret key diverts to a OpenPGP card, throw exception with hash that will be signed + log.add(LogType.MSG_PSE_PENDING_NFC, indent); + PgpSignEncryptResult result = + new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_PENDING_NFC, log); + // Note that the checked key here is the master key, not the signing key + // (although these are always the same on Yubikeys) + result.setNfcData(input.getSignatureSubKeyId(), e.hashToSign, e.hashAlgo, e.creationTimestamp, input.getSignaturePassphrase()); + Log.d(Constants.TAG, "e.hashToSign" + Hex.toHexString(e.hashToSign)); + return result; + } + } + + // closing outputs + // NOTE: closing needs to be done in the correct order! + if (encryptionOut != null) { + if (compressGen != null) { + compressGen.close(); + } + + encryptionOut.close(); + } + // Note: Closing ArmoredOutputStream does not close the underlying stream + if (armorOut != null) { + armorOut.close(); + } + // Note: Closing ArmoredOutputStream does not close the underlying stream + if (detachedArmorOut != null) { + detachedArmorOut.close(); + } + // Also closes detachedBcpgOut + if (detachedByteOut != null) { + detachedByteOut.close(); + } + if (out != null) { + out.close(); + } + if (outputStream != null) { + outputStream.close(); + } + + } catch (SignatureException e) { + log.add(LogType.MSG_PSE_ERROR_SIG, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } catch (PGPException e) { + log.add(LogType.MSG_PSE_ERROR_PGP, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } catch (IOException e) { + log.add(LogType.MSG_PSE_ERROR_IO, indent); + return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log); + } + + updateProgress(R.string.progress_done, 100, 100); + + log.add(LogType.MSG_PSE_OK, indent); + PgpSignEncryptResult result = new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_OK, log); + if (detachedByteOut != null) { + try { + detachedByteOut.flush(); + detachedByteOut.close(); + } catch (IOException e) { + // silently catch + } + result.setDetachedSignature(detachedByteOut.toByteArray()); + } + return result; + } + + /** + * Remove whitespaces on line endings + */ + private static void processLine(final String pLine, final ArmoredOutputStream pArmoredOutput, + final PGPSignatureGenerator pSignatureGenerator) + throws IOException, SignatureException { + + if (pLine == null) { + return; + } + + final char[] chars = pLine.toCharArray(); + int len = chars.length; + + while (len > 0) { + if (!Character.isWhitespace(chars[len - 1])) { + break; + } + len--; + } + + final byte[] data = pLine.substring(0, len).getBytes("UTF-8"); + + if (pArmoredOutput != null) { + pArmoredOutput.write(data); + } + pSignatureGenerator.update(data); + } + +} -- cgit v1.2.3