/*
 * Copyright (C) 2017 Schürmann & Breitmoser GbR
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.keychain.pgp;


import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
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.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;

import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.BCPGOutputStream;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.NfcSyncPGPContentSignerBuilder;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.daos.KeyRepository;
import org.sufficientlysecure.keychain.daos.KeyRepository.NotFoundException;
import org.sufficientlysecure.keychain.daos.KeyWritableRepository;
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.operations.results.SignEncryptResult;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainCompressionAlgorithmTags;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainHashAlgorithmTags;
import org.sufficientlysecure.keychain.pgp.PgpSecurityConstants.OpenKeychainSymmetricKeyAlgorithmTags;
import org.sufficientlysecure.keychain.pgp.exception.PgpGeneralException;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.InputData;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.ProgressScaler;
import timber.log.Timber;

import static java.lang.String.format;


/**
 * This class supports a single, low-level, sign/encrypt operation.
 * <p/>
 * 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.
 * <p/>
 * For a high-level operation based on URIs, see SignEncryptOperation.
 *
 * @see PgpSignEncryptInputParcel
 * @see org.sufficientlysecure.keychain.operations.results.PgpSignEncryptResult
 * @see org.sufficientlysecure.keychain.operations.SignEncryptOperation
 */
public class PgpSignEncryptOperation extends BaseOperation<PgpSignEncryptInputParcel> {

    private static byte[] NEW_LINE;

    static {
        try {
            NEW_LINE = "\r\n".getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            Timber.e(e, "UnsupportedEncodingException");
        }
    }

    public PgpSignEncryptOperation(Context context, KeyRepository keyRepository, Progressable progressable, AtomicBoolean cancelled) {
        super(context, keyRepository, progressable, cancelled);
    }

    public PgpSignEncryptOperation(Context context, KeyRepository keyRepository, Progressable progressable) {
        super(context, keyRepository, progressable);
    }

    @NonNull
    @Override
    public PgpSignEncryptResult execute(PgpSignEncryptInputParcel input, CryptoInputParcel cryptoInput) {
        OperationLog log = new OperationLog();

        InputData inputData;
        {
            if (input.getInputBytes() != null) {
                log.add(LogType.MSG_PSE_INPUT_BYTES, 1);
                InputStream is = new ByteArrayInputStream(input.getInputBytes());
                inputData = new InputData(is, input.getInputBytes().length);
            } else {
                log.add(LogType.MSG_PSE_INPUT_URI, 1);
                Uri uri = input.getInputUri();
                try {
                    InputStream is = FileHelper.openInputStreamSafe(mContext.getContentResolver(), uri);
                    long fileSize = FileHelper.getFileSize(mContext, uri, 0);
                    String filename = FileHelper.getFilename(mContext, uri);
                    inputData = new InputData(is, fileSize, filename);
                } catch (FileNotFoundException e) {
                    log.add(LogType.MSG_PSE_ERROR_INPUT_URI_NOT_FOUND, 1);
                    return new PgpSignEncryptResult(SignEncryptResult.RESULT_ERROR, log);
                }
            }
        }

        OutputStream outStream;
        {
            if (input.getOutputUri() != null) {
                try {
                    Uri outputUri = input.getOutputUri();
                    outStream = mContext.getContentResolver().openOutputStream(outputUri);
                } catch (FileNotFoundException e) {
                    log.add(LogType.MSG_PSE_ERROR_OUTPUT_URI_NOT_FOUND, 1);
                    return new PgpSignEncryptResult(SignEncryptResult.RESULT_ERROR, log);
                }
            } else {
                outStream = new ByteArrayOutputStream();
            }
        }

        PgpSignEncryptResult result = executeInternal(input.getData(), cryptoInput, inputData, outStream);
        if (outStream instanceof ByteArrayOutputStream) {
            byte[] outputData = ((ByteArrayOutputStream) outStream).toByteArray();
            result.setOutputBytes(outputData);
        }

        return result;
    }

    @NonNull
    public PgpSignEncryptResult execute(PgpSignEncryptData data, CryptoInputParcel cryptoInput,
            InputData inputData, OutputStream outputStream) {
        return executeInternal(data, cryptoInput, inputData, outputStream);
    }

    /**
     * Signs and/or encrypts data based on parameters of class
     */
    private PgpSignEncryptResult executeInternal(PgpSignEncryptData data, CryptoInputParcel cryptoInput,
            InputData inputData, OutputStream outputStream) {
        int indent = 0;
        OperationLog log = new OperationLog();

        log.add(LogType.MSG_PSE, indent);
        indent += 1;

        boolean enableSignature = data.getSignatureMasterKeyId() != Constants.key.none;
        boolean enableEncryption = ((data.getEncryptionMasterKeyIds() != null && data.getEncryptionMasterKeyIds().length > 0)
                || data.getSymmetricPassphrase() != null);

        int compressionAlgorithm = data.getCompressionAlgorithm();
        if (compressionAlgorithm == OpenKeychainCompressionAlgorithmTags.USE_DEFAULT) {
            compressionAlgorithm = PgpSecurityConstants.DEFAULT_COMPRESSION_ALGORITHM;
        }

        Timber.d(data.toString());

        ArmoredOutputStream armorOut = null;
        OutputStream out;
        if (data.isEnableAsciiArmorOutput()) {
            armorOut = new ArmoredOutputStream(new BufferedOutputStream(outputStream, 1 << 16));
            if (data.getVersionHeader() != null) {
                armorOut.setHeader("Version", data.getVersionHeader());
            }
            // if we have a charset, put it in the header
            if (data.getCharset() != null) {
                armorOut.setHeader("Charset", data.getCharset());
            }
            String passphraseFormat = data.getPassphraseFormat();
            if (passphraseFormat != null) {
                armorOut.setHeader("Passphrase-Format", passphraseFormat);
            }
            String passphraseBegin = data.getPassphraseBegin();
            if (passphraseBegin != null) {
                armorOut.setHeader("Passphrase-Begin", passphraseBegin);
            }
            out = armorOut;
        } else {
            out = outputStream;
        }

        /* Get keys for signature generation for later usage */
        CanonicalizedSecretKey signingKey = null;
        if (enableSignature) {

            updateProgress(R.string.progress_extracting_signature_key, 0, 100);

            try {
                long signingMasterKeyId = data.getSignatureMasterKeyId();
                Long signingSubKeyId = data.getSignatureSubKeyId();
                if (signingSubKeyId == null) {
                    try {
                        signingSubKeyId = mKeyRepository.getSecretSignId(signingMasterKeyId);
                    } catch (NotFoundException e) {
                        log.add(LogType.MSG_PSE_ERROR_KEY_SIGN, indent);
                        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
                    }
                }

                CanonicalizedSecretKeyRing signingKeyRing =
                        mKeyRepository.getCanonicalizedSecretKeyRing(signingMasterKeyId);
                signingKey = signingKeyRing.getSecretKey(signingSubKeyId);

                Collection<Long> allowedSigningKeyIds = data.getAllowedSigningKeyIds();
                if (allowedSigningKeyIds != null && !allowedSigningKeyIds.contains(signingMasterKeyId)) {
                    // this key is in our db, but NOT allowed!
                    log.add(LogType.MSG_PSE_ERROR_KEY_NOT_ALLOWED, indent + 1);
                    return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_KEY_DISALLOWED, log);
                }

                // Make sure key is not expired or revoked
                if (signingKeyRing.isExpired() || signingKeyRing.isRevoked()
                        || signingKey.isExpired() || signingKey.isRevoked()) {
                    log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, 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);
                }

                switch (mKeyRepository.getSecretKeyType(signingSubKeyId)) {
                    case DIVERT_TO_CARD:
                    case PASSPHRASE_EMPTY: {
                        if (!signingKey.unlock(new Passphrase())) {
                            throw new AssertionError(
                                    "PASSPHRASE_EMPTY/DIVERT_TO_CARD keyphrase not unlocked with empty passphrase."
                                            + " This is a programming error!");
                        }
                        break;
                    }

                    case PASSPHRASE: {
                        Passphrase localPassphrase = cryptoInput.getPassphrase();
                        if (localPassphrase == null) {
                            try {
                                localPassphrase = getCachedPassphrase(signingMasterKeyId, signingKey.getKeyId());
                            } catch (PassphraseCacheInterface.NoSecretKeyException ignored) {
                            }
                        }
                        if (localPassphrase == null) {
                            log.add(LogType.MSG_PSE_PENDING_PASSPHRASE, indent + 1);
                            return new PgpSignEncryptResult(log, RequiredInputParcel.createRequiredSignPassphrase(
                                    signingMasterKeyId, signingKey.getKeyId(),
                                    cryptoInput.getSignatureTime()), cryptoInput);
                        }
                        if (!signingKey.unlock(localPassphrase)) {
                            log.add(LogType.MSG_PSE_ERROR_BAD_PASSPHRASE, indent);
                            return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
                        }
                        break;
                    }

                    case GNU_DUMMY: {
                        log.add(LogType.MSG_PSE_ERROR_UNLOCK, indent);
                        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
                    }
                    default: {
                        throw new AssertionError("Unhandled SecretKeyType! (should not happen)");
                    }

                }

            } catch (KeyWritableRepository.NotFoundException e) {
                log.add(LogType.MSG_PSE_ERROR_SIGN_KEY, 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);
            }
        }
        updateProgress(R.string.progress_preparing_streams, 2, 100);

        /* Initialize PGPEncryptedDataGenerator for later usage */
        PGPEncryptedDataGenerator cPk = null;
        if (enableEncryption) {

            // Use requested encryption algo
            int symmetricEncryptionAlgorithm = data.getSymmetricEncryptionAlgorithm();
            if (symmetricEncryptionAlgorithm == OpenKeychainSymmetricKeyAlgorithmTags.USE_DEFAULT) {
                symmetricEncryptionAlgorithm = PgpSecurityConstants.DEFAULT_SYMMETRIC_ALGORITHM;
            }
            JcePGPDataEncryptorBuilder encryptorBuilder =
                    new JcePGPDataEncryptorBuilder(symmetricEncryptionAlgorithm)
                            .setProvider(Constants.BOUNCY_CASTLE_PROVIDER_NAME)
                            .setWithIntegrityPacket(true);

            cPk = new PGPEncryptedDataGenerator(encryptorBuilder);

            if (data.getSymmetricPassphrase() != null) {
                // Symmetric encryption
                log.add(LogType.MSG_PSE_SYMMETRIC, indent);

                JcePBEKeyEncryptionMethodGenerator symmetricEncryptionGenerator =
                        new JcePBEKeyEncryptionMethodGenerator(data.getSymmetricPassphrase().getCharArray());
                cPk.addMethod(symmetricEncryptionGenerator);
            } else {
                log.add(LogType.MSG_PSE_ASYMMETRIC, indent);

                long additionalEncryptId = data.getAdditionalEncryptId();
                for (long encryptMasterKeyId : data.getEncryptionMasterKeyIds()) {
                    if (encryptMasterKeyId == additionalEncryptId) {
                        continue;
                    }

                    boolean success = processEncryptionMasterKeyId(indent, log, data, cPk, encryptMasterKeyId);
                    if (!success) {
                        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
                    }
                }

                if (additionalEncryptId != Constants.key.none) {
                    boolean success = processEncryptionMasterKeyId(indent, log, data, cPk, additionalEncryptId);
                    if (!success) {
                        return new PgpSignEncryptResult(PgpSignEncryptResult.RESULT_ERROR, log);
                    }
                }
            }
        }

        int signatureHashAlgorithm = data.getSignatureHashAlgorithm();
        if (signatureHashAlgorithm == OpenKeychainHashAlgorithmTags.USE_DEFAULT) {
            signatureHashAlgorithm = PgpSecurityConstants.DEFAULT_HASH_ALGORITHM;
        }

        /* Initialize signature generator object for later usage */
        PGPSignatureGenerator signatureGenerator = null;
        if (enableSignature) {
            updateProgress(R.string.progress_preparing_signature, 4, 100);

            try {
                boolean cleartext = data.isCleartextSignature() && data.isEnableAsciiArmorOutput() && !enableEncryption;
                signatureGenerator = signingKey.getDataSignatureGenerator(
                        signatureHashAlgorithm, cleartext,
                        cryptoInput.getCryptoData(), cryptoInput.getSignatureTime());
            } 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;

        long opTime, startTime = System.currentTimeMillis();

        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 (compressionAlgorithm != CompressionAlgorithmTags.UNCOMPRESSED) {
                    log.add(LogType.MSG_PSE_COMPRESSING, indent);

                    compressGen = new PGPCompressedDataGenerator(compressionAlgorithm);
                    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 (data.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 = new BufferedInputStream(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 && data.isCleartextSignature() && data.isEnableAsciiArmorOutput()) {
                /* 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(signatureHashAlgorithm);

                InputStream in = new BufferedInputStream(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 && data.isDetachedSignature()) {
                /* detached signature */

                updateProgress(R.string.progress_signing, 8, 100);
                log.add(LogType.MSG_PSE_SIGNING_DETACHED, indent);

                InputStream in = new BufferedInputStream(inputData.getInputStream());

                // handle output stream separately for detached signatures
                detachedByteOut = new ByteArrayOutputStream();
                OutputStream detachedOut = detachedByteOut;
                if (data.isEnableAsciiArmorOutput()) {
                    detachedArmorOut = new ArmoredOutputStream(new BufferedOutputStream(detachedOut, 1 << 16));
                    if (data.getVersionHeader() != null) {
                        detachedArmorOut.setHeader("Version", data.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 && !data.isCleartextSignature() && !data.isDetachedSignature()) {
                /* sign-only binary (files/data stream) */

                updateProgress(R.string.progress_signing, 8, 100);
                log.add(LogType.MSG_PSE_SIGNING, indent);

                InputStream in = new BufferedInputStream(inputData.getInputStream());

                if (compressionAlgorithm != CompressionAlgorithmTags.UNCOMPRESSED) {
                    log.add(LogType.MSG_PSE_COMPRESSING, indent);

                    compressGen = new PGPCompressedDataGenerator(compressionAlgorithm);
                    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 {
                throw new AssertionError("cannot clearsign in non-ascii armored text, this is a bug!");
            }

            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);
                    return new PgpSignEncryptResult(log, RequiredInputParcel.createSecurityTokenSignOperation(
                            signingKey.getRing().getMasterKeyId(), signingKey.getKeyId(),
                            e.hashToSign, e.hashAlgo, cryptoInput.getSignatureTime()), cryptoInput);
                }
            }

            opTime = System.currentTimeMillis() - startTime;
            Timber.d("sign/encrypt time taken: " + format("%.2f", opTime / 1000.0) + "s");

            // closing outputs
            // NOTE: closing needs to be done in the correct order!
            if (compressGen != null) {
                compressGen.close();
            }

            if (encryptionOut != null) {
                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);
        result.mOperationTime = opTime;
        if (detachedByteOut != null) {
            try {
                detachedByteOut.flush();
                detachedByteOut.close();
            } catch (IOException e) {
                // silently catch
            }
            result.setDetachedSignature(detachedByteOut.toByteArray());
            try {
                String digestName = PGPUtil.getDigestName(signatureHashAlgorithm);
                // construct micalg parameter according to https://tools.ietf.org/html/rfc3156#section-5
                result.setMicAlgDigestName("pgp-" + digestName.toLowerCase());
            } catch (PGPException e) {
                Timber.e(e, "error setting micalg parameter!");
            }
        }
        return result;
    }

    private boolean processEncryptionMasterKeyId(int indent, OperationLog log, PgpSignEncryptData data,
            PGPEncryptedDataGenerator cPk, long encryptMasterKeyId) {
        try {
            CanonicalizedPublicKeyRing keyRing = mKeyRepository.getCanonicalizedPublicKeyRing(encryptMasterKeyId);
            List<Long> encryptSubKeyIds = mKeyRepository.getPublicEncryptionIds(encryptMasterKeyId);
            for (Long subKeyId : encryptSubKeyIds) {
                CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId);
                cPk.addMethod(key.getPubKeyEncryptionGenerator(data.isHiddenRecipients()));
                log.add(LogType.MSG_PSE_KEY_OK, indent + 1,
                        KeyFormattingUtils.convertKeyIdToHex(subKeyId));
            }
            if (encryptSubKeyIds.isEmpty()) {
                log.add(LogType.MSG_PSE_KEY_WARN, indent + 1,
                        KeyFormattingUtils.convertKeyIdToHex(encryptMasterKeyId));
                return false;
            }
            // Make sure key is not expired or revoked
            if (keyRing.isExpired() || keyRing.isRevoked()) {
                log.add(LogType.MSG_PSE_ERROR_REVOKED_OR_EXPIRED, indent);
                return false;
            }
        } catch (KeyWritableRepository.NotFoundException e) {
            log.add(LogType.MSG_PSE_KEY_UNKNOWN, indent + 1,
                    KeyFormattingUtils.convertKeyIdToHex(encryptMasterKeyId));
            return false;
        }
        return true;
    }

    /**
     * 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);
    }

}
