コード例 #1
0
  @NonNull
  @Override
  public InputDataResult execute(InputDataParcel input, final CryptoInputParcel cryptoInput) {

    final OperationLog log = new OperationLog();

    log.add(LogType.MSG_DATA, 0);

    Uri currentInputUri;

    DecryptVerifyResult decryptResult = null;

    PgpDecryptVerifyInputParcel decryptInput = input.getDecryptInput();

    if (!input.getMimeDecode() && decryptInput == null) {
      throw new AssertionError("no decryption or mime decoding, this is probably a bug");
    }

    if (decryptInput != null) {

      log.add(LogType.MSG_DATA_OPENPGP, 1);

      PgpDecryptVerifyOperation op =
          new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);

      decryptInput.setInputUri(input.getInputUri());

      currentInputUri = TemporaryFileProvider.createFile(mContext);
      decryptInput.setOutputUri(currentInputUri);

      decryptResult = op.execute(decryptInput, cryptoInput);
      if (decryptResult.isPending()) {
        return new InputDataResult(log, decryptResult);
      }
      log.addByMerge(decryptResult, 1);

      if (!decryptResult.success()) {
        return new InputDataResult(InputDataResult.RESULT_ERROR, log);
      }

      // inform the storage provider about the mime type for this uri
      if (decryptResult.getDecryptionMetadata() != null) {
        OpenPgpMetadata meta = decryptResult.getDecryptionMetadata();
        TemporaryFileProvider.setName(mContext, currentInputUri, meta.getFilename());
        TemporaryFileProvider.setMimeType(mContext, currentInputUri, meta.getMimeType());
      }

    } else {
      currentInputUri = input.getInputUri();
    }

    // don't even attempt if we know the data isn't suitable for mime content, or if we have a
    // filename
    boolean skipMimeParsing = false;
    if (decryptResult != null && decryptResult.getDecryptionMetadata() != null) {
      OpenPgpMetadata metadata = decryptResult.getDecryptionMetadata();
      String fileName = metadata.getFilename();
      String contentType = metadata.getMimeType();
      if (!TextUtils.isEmpty(fileName)
          || contentType != null
              && !contentType.startsWith("multipart/")
              && !contentType.startsWith("text/")
              && !"application/octet-stream".equals(contentType)) {
        skipMimeParsing = true;
      }
    }

    // If we aren't supposed to attempt mime decode after decryption, we are done here
    if (skipMimeParsing || !input.getMimeDecode()) {

      log.add(LogType.MSG_DATA_SKIP_MIME, 1);

      ArrayList<Uri> uris = new ArrayList<>();
      uris.add(currentInputUri);
      ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();
      metadatas.add(decryptResult.getDecryptionMetadata());

      log.add(LogType.MSG_DATA_OK, 1);
      return new InputDataResult(InputDataResult.RESULT_OK, log, decryptResult, uris, metadatas);
    }

    final MimeStreamParser parser = new MimeStreamParser((MimeConfig) null);

    final ArrayList<Uri> outputUris = new ArrayList<>();
    final ArrayList<OpenPgpMetadata> metadatas = new ArrayList<>();

    parser.setContentDecoding(true);
    parser.setRecurse();
    parser.setContentHandler(
        new AbstractContentHandler() {
          private boolean mFoundContentTypeHeader = false;
          private Uri uncheckedSignedDataUri;
          String mFilename;

          @Override
          public void startMultipart(BodyDescriptor bd) throws MimeException {
            if ("signed".equals(bd.getSubType())) {
              if (mSignedDataUri != null) {
                // recursive signed data is not supported, and will just be parsed as-is
                log.add(LogType.MSG_DATA_DETACHED_NESTED, 2);
                return;
              }
              log.add(LogType.MSG_DATA_DETACHED, 2);
              if (!outputUris.isEmpty()) {
                // we can't have previous data if we parse a detached signature!
                log.add(LogType.MSG_DATA_DETACHED_CLEAR, 3);
                outputUris.clear();
                metadatas.clear();
              }
              // this is signed data, we require the next part raw
              parser.setRaw();
            }
          }

          @Override
          public void raw(InputStream is) throws MimeException, IOException {

            if (uncheckedSignedDataUri != null) {
              throw new AssertionError(
                  "raw parts must only be received as first part of multipart/signed!");
            }

            log.add(LogType.MSG_DATA_DETACHED_RAW, 3);

            uncheckedSignedDataUri =
                TemporaryFileProvider.createFile(mContext, mFilename, "text/plain");
            OutputStream out =
                mContext.getContentResolver().openOutputStream(uncheckedSignedDataUri, "w");

            if (out == null) {
              throw new IOException("Error getting file for writing!");
            }

            int len;
            while ((len = is.read(buf)) > 0) {
              out.write(buf, 0, len);
            }

            out.close();
            // continue to next body part the usual way
            parser.setFlat();
          }

          @Override
          public void startHeader() throws MimeException {
            mFilename = null;
          }

          @Override
          public void endHeader() throws MimeException {
            if (!mFoundContentTypeHeader) {
              parser.stop();
            }
          }

          @Override
          public void field(Field field) throws MimeException {
            field = DefaultFieldParser.getParser().parse(field, DecodeMonitor.SILENT);
            if (field instanceof ContentDispositionField) {
              mFilename = ((ContentDispositionField) field).getFilename();
            }
            if (field instanceof ContentTypeField) {
              mFoundContentTypeHeader = true;
            }
          }

          private void bodySignature(BodyDescriptor bd, InputStream is)
              throws MimeException, IOException {

            if (!"application/pgp-signature".equals(bd.getMimeType())) {
              log.add(LogType.MSG_DATA_DETACHED_UNSUPPORTED, 3);
              uncheckedSignedDataUri = null;
              parser.setRecurse();
              return;
            }

            log.add(LogType.MSG_DATA_DETACHED_SIG, 3);

            ByteArrayOutputStream detachedSig = new ByteArrayOutputStream();

            int len, totalLength = 0;
            while ((len = is.read(buf)) > 0) {
              totalLength += len;
              detachedSig.write(buf, 0, len);
              if (totalLength > 4096) {
                throw new IOException("detached signature is unreasonably large!");
              }
            }
            detachedSig.close();

            PgpDecryptVerifyInputParcel decryptInput = new PgpDecryptVerifyInputParcel();
            decryptInput.setInputUri(uncheckedSignedDataUri);
            decryptInput.setDetachedSignature(detachedSig.toByteArray());

            PgpDecryptVerifyOperation op =
                new PgpDecryptVerifyOperation(mContext, mProviderHelper, mProgressable);
            DecryptVerifyResult verifyResult = op.execute(decryptInput, cryptoInput);

            log.addByMerge(verifyResult, 4);

            mSignedDataUri = uncheckedSignedDataUri;
            mSignedDataResult = verifyResult;

            // reset parser state
            uncheckedSignedDataUri = null;
            parser.setRecurse();
          }

          @Override
          public void body(BodyDescriptor bd, InputStream is) throws MimeException, IOException {

            // if we have signed data waiting, we expect a signature for checking
            if (uncheckedSignedDataUri != null) {
              bodySignature(bd, is);
              return;
            }

            // we read first, no need to create an output file if nothing was read!
            int len = is.read(buf);
            if (len < 0) {
              return;
            }

            // If mSignedDataUri is non-null, we already parsed a signature. If mSignedDataResult is
            // non-null
            // too, we are still in the same parsing stage, so this is trailing data - skip it!
            if (mSignedDataUri != null && mSignedDataResult != null) {
              log.add(LogType.MSG_DATA_DETACHED_TRAILING, 2);
              return;
            }

            log.add(LogType.MSG_DATA_MIME_PART, 2);

            String mimeType = bd.getMimeType();

            if (mFilename != null) {
              log.add(LogType.MSG_DATA_MIME_FILENAME, 3, mFilename);
              boolean isGenericMimeType =
                  ClipDescription.compareMimeTypes(mimeType, "application/octet-stream")
                      || ClipDescription.compareMimeTypes(mimeType, "application/x-download");
              if (isGenericMimeType) {
                String extension = MimeTypeMap.getFileExtensionFromUrl(mFilename);
                String extMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
                if (extMimeType != null) {
                  mimeType = extMimeType;
                  log.add(LogType.MSG_DATA_MIME_FROM_EXTENSION, 3);
                }
              }
            }
            log.add(LogType.MSG_DATA_MIME_TYPE, 3, mimeType);

            Uri uri = TemporaryFileProvider.createFile(mContext, mFilename, mimeType);
            OutputStream out = mContext.getContentResolver().openOutputStream(uri, "w");

            if (out == null) {
              throw new IOException("Error getting file for writing!");
            }

            // If this data looks like text, we pipe the incoming data into a charset
            // decoder, to see if the data is legal for the assumed charset.
            String charset = bd.getCharset();
            CharsetVerifier charsetVerifier = new CharsetVerifier(buf, mimeType, charset);

            int totalLength = 0;
            do {
              totalLength += len;
              out.write(buf, 0, len);
              charsetVerifier.readBytesFromBuffer(0, len);
            } while ((len = is.read(buf)) > 0);

            log.add(LogType.MSG_DATA_MIME_LENGTH, 3, Long.toString(totalLength));

            OpenPgpMetadata metadata;
            if (charsetVerifier.isDefinitelyBinary()) {
              metadata = new OpenPgpMetadata(mFilename, mimeType, 0L, totalLength);
            } else {
              if (charsetVerifier.isCharsetFaulty() && charsetVerifier.isCharsetGuessed()) {
                log.add(
                    LogType.MSG_DATA_MIME_CHARSET_UNKNOWN,
                    3,
                    charsetVerifier.getMaybeFaultyCharset());
              } else if (charsetVerifier.isCharsetFaulty()) {
                log.add(LogType.MSG_DATA_MIME_CHARSET_FAULTY, 3, charsetVerifier.getCharset());
              } else if (charsetVerifier.isCharsetGuessed()) {
                log.add(LogType.MSG_DATA_MIME_CHARSET_GUESS, 3, charsetVerifier.getCharset());
              } else {
                log.add(LogType.MSG_DATA_MIME_CHARSET, 3, charsetVerifier.getCharset());
              }

              metadata =
                  new OpenPgpMetadata(
                      mFilename,
                      charsetVerifier.getGuessedMimeType(),
                      0L,
                      totalLength,
                      charsetVerifier.getCharset());
            }

            out.close();
            outputUris.add(uri);
            metadatas.add(metadata);
          }
        });

    try {

      log.add(LogType.MSG_DATA_MIME, 1);

      try {

        // open current uri for input
        InputStream in = mContext.getContentResolver().openInputStream(currentInputUri);
        parser.parse(in);

        if (mSignedDataUri != null) {

          if (decryptResult != null) {
            decryptResult.setSignatureResult(mSignedDataResult.getSignatureResult());
          } else {
            decryptResult = mSignedDataResult;
          }

          // the actual content is the signed data now (and will be passed verbatim, if parsing
          // fails)
          currentInputUri = mSignedDataUri;
          in = mContext.getContentResolver().openInputStream(currentInputUri);
          // reset signed data result, to indicate to the parser that it is in the inner part
          mSignedDataResult = null;
          parser.parse(in);
        }
      } catch (MimeException e) {
        // a mime error likely means that this wasn't mime data, after all
        e.printStackTrace();
        log.add(LogType.MSG_DATA_MIME_BAD, 2);
      }

      // if we found data, return success
      if (!outputUris.isEmpty()) {
        log.add(LogType.MSG_DATA_MIME_OK, 2);

        log.add(LogType.MSG_DATA_OK, 1);
        return new InputDataResult(
            InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);
      }

      // if no mime data parsed, just return the raw data as fallback
      log.add(LogType.MSG_DATA_MIME_NONE, 2);

      OpenPgpMetadata metadata;
      if (decryptResult != null) {
        metadata = decryptResult.getDecryptionMetadata();
      } else {
        // if we neither decrypted nor mime-decoded, should this be treated as an error?
        // either way, we know nothing about the data
        metadata = new OpenPgpMetadata();
      }

      outputUris.add(currentInputUri);
      metadatas.add(metadata);

      log.add(LogType.MSG_DATA_OK, 1);
      return new InputDataResult(
          InputDataResult.RESULT_OK, log, decryptResult, outputUris, metadatas);

    } catch (FileNotFoundException e) {
      log.add(LogType.MSG_DATA_ERROR_IO, 2);
      return new InputDataResult(InputDataResult.RESULT_ERROR, log);
    } catch (IOException e) {
      e.printStackTrace();
      log.add(LogType.MSG_DATA_ERROR_IO, 2);
      return new InputDataResult(InputDataResult.RESULT_ERROR, log);
    }
  }
コード例 #2
0
  /** Signs and/or encrypts data based on parameters of class */
  public PgpSignEncryptResult execute(
      PgpSignEncryptInputParcel input,
      CryptoInputParcel cryptoInput,
      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.isEnableAsciiArmorOutput()
            + "\nisHiddenRecipients:"
            + input.isHiddenRecipients());

    // 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.isEnableAsciiArmorOutput()) {
      armorOut = new ArmoredOutputStream(new BufferedOutputStream(outputStream, 1 << 16));
      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) {

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

      try {
        // fetch the indicated master key id (the one whose name we sign in)
        CanonicalizedSecretKeyRing signingKeyRing =
            mProviderHelper.getCanonicalizedSecretKeyRing(input.getSignatureMasterKeyId());

        // fetch the specific subkey to sign with, or just use the master key if none specified
        signingKey = signingKeyRing.getSecretKey(input.getSignatureSubKeyId());

        // 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 (signingKey.getSecretKeyType()) {
          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 PIN:
          case PATTERN:
          case PASSPHRASE:
            {
              Passphrase localPassphrase = cryptoInput.getPassphrase();
              if (localPassphrase == null) {
                try {
                  localPassphrase =
                      getCachedPassphrase(signingKeyRing.getMasterKeyId(), signingKey.getKeyId());
                } catch (PassphraseCacheInterface.NoSecretKeyException ignored) {
                }
              }
              if (localPassphrase == null) {
                log.add(LogType.MSG_PSE_PENDING_PASSPHRASE, indent + 1);
                return new PgpSignEncryptResult(
                    log,
                    RequiredInputParcel.createRequiredSignPassphrase(
                        signingKeyRing.getMasterKeyId(),
                        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 (ProviderHelper.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);
      }

      // Use preferred hash algo
      int requestedAlgorithm = input.getSignatureHashAlgorithm();
      ArrayList<Integer> supported = signingKey.getSupportedHashAlgorithms();
      if (requestedAlgorithm == PgpConstants.OpenKeychainHashAlgorithmTags.USE_PREFERRED) {
        // get most preferred
        input.setSignatureHashAlgorithm(supported.get(0));
      } 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) {

      // Use preferred encryption algo
      int algo = input.getSymmetricEncryptionAlgorithm();
      if (algo == PgpConstants.OpenKeychainSymmetricKeyAlgorithmTags.USE_PREFERRED) {
        // get most preferred
        // TODO: get from recipients
        algo = PgpConstants.sPreferredSymmetricAlgorithms.get(0);
      }
      // 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().getCharArray());
        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));
            Set<Long> encryptSubKeyIds = keyRing.getEncryptIds();
            for (Long subKeyId : encryptSubKeyIds) {
              CanonicalizedPublicKey key = keyRing.getPublicKey(subKeyId);
              cPk.addMethod(key.getPubKeyEncryptionGenerator(input.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(id));
              if (input.isFailOnMissingEncryptionKeyIds()) {
                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.isFailOnMissingEncryptionKeyIds()) {
              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.isEnableAsciiArmorOutput() && !enableEncryption;
        signatureGenerator =
            signingKey.getDataSignatureGenerator(
                input.getSignatureHashAlgorithm(),
                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;

    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.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(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.isEnableAsciiArmorOutput()) {
          detachedArmorOut =
              new ArmoredOutputStream(new BufferedOutputStream(detachedOut, 1 << 16));
          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);
          return new PgpSignEncryptResult(
              log,
              RequiredInputParcel.createNfcSignOperation(
                  signingKey.getRing().getMasterKeyId(),
                  signingKey.getKeyId(),
                  e.hashToSign,
                  e.hashAlgo,
                  cryptoInput.getSignatureTime()),
              cryptoInput);
        }
      }

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