@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // do not allow screenshots of passphrase input // to prevent "too easy" passphrase theft by root apps if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) { getWindow() .setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); } CryptoInputParcel cryptoInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); if (cryptoInputParcel == null) { cryptoInputParcel = new CryptoInputParcel(); getIntent().putExtra(EXTRA_CRYPTO_INPUT, cryptoInputParcel); } // this activity itself has no content view (see manifest) RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT); if (requiredInput.mType != RequiredInputType.PASSPHRASE) { return; } // handle empty passphrases by directly returning an empty crypto input parcel try { CanonicalizedSecretKeyRing pubRing = new ProviderHelper(this).getCanonicalizedSecretKeyRing(requiredInput.getMasterKeyId()); // use empty passphrase for empty passphrase if (pubRing.getSecretKey(requiredInput.getSubKeyId()).getSecretKeyType() == SecretKeyType.PASSPHRASE_EMPTY) { // also return passphrase back to activity Intent returnIntent = new Intent(); cryptoInputParcel.mPassphrase = new Passphrase(""); returnIntent.putExtra(RESULT_CRYPTO_INPUT, cryptoInputParcel); setResult(RESULT_OK, returnIntent); finish(); } } catch (NotFoundException e) { Log.e(Constants.TAG, "Key not found?!", e); setResult(RESULT_CANCELED); finish(); } }
/** 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; }
@NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Activity activity = getActivity(); ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); mRequiredInput = getArguments().getParcelable(EXTRA_REQUIRED_INPUT); CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme); // No title, see http://www.google.com/design/spec/components/dialogs.html#dialogs-alerts // alert.setTitle() if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) { LayoutInflater inflater = LayoutInflater.from(theme); View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null); alert.setView(view); mBackupCodeEditText = new EditText[4]; mBackupCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1); mBackupCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2); mBackupCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3); mBackupCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4); setupEditTextFocusNext(mBackupCodeEditText); AlertDialog dialog = alert.create(); dialog.setButton( DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); return dialog; } long subKeyId = mRequiredInput.getSubKeyId(); LayoutInflater inflater = LayoutInflater.from(theme); mLayout = (ViewAnimator) inflater.inflate(R.layout.passphrase_dialog, null); alert.setView(mLayout); mPassphraseText = (TextView) mLayout.findViewById(R.id.passphrase_text); mPassphraseEditText = (EditText) mLayout.findViewById(R.id.passphrase_passphrase); View vTimeToLiveLayout = mLayout.findViewById(R.id.remember_layout); vTimeToLiveLayout.setVisibility(mRequiredInput.mSkipCaching ? View.GONE : View.VISIBLE); mTimeToLiveSpinner = (CacheTTLSpinner) mLayout.findViewById(R.id.ttl_spinner); alert.setNegativeButton( android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); String userId; CanonicalizedSecretKey.SecretKeyType keyType = CanonicalizedSecretKey.SecretKeyType.PASSPHRASE; String message; String hint; if (mRequiredInput.mType == RequiredInputType.PASSPHRASE_SYMMETRIC) { message = getString(R.string.passphrase_for_symmetric_encryption); hint = getString(R.string.label_passphrase); } else { try { ProviderHelper helper = new ProviderHelper(activity); mSecretRing = helper.getCanonicalizedSecretKeyRing( KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId)); // yes the inner try/catch block is necessary, otherwise the final variable // above can't be statically verified to have been set in all cases because // the catch clause doesn't return. try { String mainUserId = mSecretRing.getPrimaryUserIdWithFallback(); KeyRing.UserId mainUserIdSplit = KeyRing.splitUserId(mainUserId); if (mainUserIdSplit.name != null) { userId = mainUserIdSplit.name; } else { userId = getString(R.string.user_id_no_name); } } catch (PgpKeyNotFoundException e) { userId = null; } keyType = mSecretRing.getSecretKey(subKeyId).getSecretKeyType(); switch (keyType) { case PASSPHRASE: message = getString(R.string.passphrase_for, userId); hint = getString(R.string.label_passphrase); break; case PIN: message = getString(R.string.pin_for, userId); hint = getString(R.string.label_pin); break; case DIVERT_TO_CARD: message = getString(R.string.yubikey_pin_for, userId); hint = getString(R.string.label_pin); break; // special case: empty passphrase just returns the empty passphrase case PASSPHRASE_EMPTY: finishCaching(new Passphrase("")); default: throw new AssertionError("Unhandled SecretKeyType (should not happen)"); } } catch (ProviderHelper.NotFoundException e) { alert.setTitle(R.string.title_key_not_found); alert.setMessage(getString(R.string.key_not_found, mRequiredInput.getSubKeyId())); alert.setPositiveButton( android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dismiss(); } }); alert.setCancelable(false); return alert.create(); } } mPassphraseText.setText(message); mPassphraseEditText.setHint(hint); // Hack to open keyboard. // This is the only method that I found to work across all Android versions // http://turbomanage.wordpress.com/2012/05/02/show-soft-keyboard-automatically-when-edittext-receives-focus/ // Notes: * onCreateView can't be used because we want to add buttons to the dialog // * opening in onActivityCreated does not work on Android 4.4 mPassphraseEditText.setOnFocusChangeListener( new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { mPassphraseEditText.post( new Runnable() { @Override public void run() { if (getActivity() == null || mPassphraseEditText == null) { return; } InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mPassphraseEditText, InputMethodManager.SHOW_IMPLICIT); } }); } }); mPassphraseEditText.requestFocus(); mPassphraseEditText.setImeActionLabel( getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE); mPassphraseEditText.setOnEditorActionListener(this); if ((keyType == CanonicalizedSecretKey.SecretKeyType.DIVERT_TO_CARD && Preferences.getPreferences(activity).useNumKeypadForYubiKeyPin()) || keyType == CanonicalizedSecretKey.SecretKeyType.PIN) { mPassphraseEditText.setInputType(InputType.TYPE_CLASS_NUMBER); mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); } else { mPassphraseEditText.setRawInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); } mPassphraseEditText.setTransformationMethod(PasswordTransformationMethod.getInstance()); AlertDialog dialog = alert.create(); dialog.setButton( DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.btn_unlock), (DialogInterface.OnClickListener) null); return dialog; }