diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index 8d76101ae..fa0286a90 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -454,6 +454,14 @@ + + + + + + + + { } CountingOutputStream outStream = new CountingOutputStream(new BufferedOutputStream(plainOut)); - boolean backupSuccess = exportKeysToStream( - log, backupInput.getMasterKeyIds(), backupInput.getExportSecret(), outStream); + boolean backupSuccess = exportKeysToStream(log, backupInput.getMasterKeyIds(), + backupInput.getExportSecret(), backupInput.getExportPublic(), outStream); if (!backupSuccess) { // if there was an error, it will be in the log so we just have to return @@ -169,9 +171,15 @@ public class BackupOperation extends BaseOperation { PgpSignEncryptOperation signEncryptOperation = new PgpSignEncryptOperation(mContext, mKeyRepository, mProgressable, mCancelled); PgpSignEncryptData.Builder builder = PgpSignEncryptData.builder(); - builder.setSymmetricPassphrase(cryptoInput.getPassphrase()); + Passphrase passphrase = cryptoInput.getPassphrase(); + builder.setSymmetricPassphrase(passphrase); builder.setEnableAsciiArmorOutput(backupInput.getEnableAsciiArmorOutput()); - builder.setAddBackupHeader(true); + boolean isNumeric9x4Passphrase = passphrase != null && Numeric9x4PassphraseUtil.isNumeric9x4Passphrase(passphrase); + if (isNumeric9x4Passphrase) { + builder.setPassphraseFormat("numeric9x4"); + char[] passphraseChars = passphrase.getCharArray(); + builder.setPassphraseBegin("" + passphraseChars[0] + passphraseChars[1]); + } PgpSignEncryptData pgpSignEncryptData = builder.build(); InputStream inStream = mContext.getContentResolver().openInputStream(plainUri); @@ -206,7 +214,8 @@ public class BackupOperation extends BaseOperation { pgpSignEncryptData, CryptoInputParcel.createCryptoInputParcel(), inputData, outStream); } - boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, OutputStream outStream) { + boolean exportKeysToStream(OperationLog log, long[] masterKeyIds, boolean exportSecret, boolean exportPublic, + OutputStream outStream) { // noinspection unused TODO use these in a log entry int okSecret = 0, okPublic = 0; @@ -232,9 +241,15 @@ public class BackupOperation extends BaseOperation { long masterKeyId = cursor.getLong(INDEX_MASTER_KEY_ID); log.add(LogType.MSG_BACKUP_PUBLIC, 1, KeyFormattingUtils.beautifyKeyId(masterKeyId)); - if (writePublicKeyToStream(masterKeyId, log, outStream)) { - okPublic += 1; + boolean publicKeyWriteOk = false; + if (exportPublic) { + publicKeyWriteOk = writePublicKeyToStream(masterKeyId, log, outStream); + if (publicKeyWriteOk) { + okPublic += 1; + } + } + if (publicKeyWriteOk || !exportPublic) { boolean hasSecret = cursor.getInt(INDEX_HAS_ANY_SECRET) > 0; if (exportSecret && hasSecret) { log.add(LogType.MSG_BACKUP_SECRET, 2, KeyFormattingUtils.beautifyKeyId(masterKeyId)); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java index 6982a10e4..966ae84d4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/results/OperationResult.java @@ -627,6 +627,8 @@ public abstract class OperationResult implements Parcelable { MSG_DC_ASYM (LogLevel.DEBUG, R.string.msg_dc_asym), MSG_DC_CHARSET (LogLevel.DEBUG, R.string.msg_dc_charset), MSG_DC_BACKUP_VERSION (LogLevel.DEBUG, R.string.msg_dc_backup_version), + MSG_DC_PASSPHRASE_FORMAT (LogLevel.DEBUG, R.string.msg_dc_passphrase_format), + MSG_DC_PASSPHRASE_BEGIN (LogLevel.DEBUG, R.string.msg_dc_passphrase_begin), MSG_DC_CLEAR_DATA (LogLevel.DEBUG, R.string.msg_dc_clear_data), MSG_DC_CLEAR_DECOMPRESS (LogLevel.DEBUG, R.string.msg_dc_clear_decompress), MSG_DC_CLEAR_META_FILE (LogLevel.DEBUG, R.string.msg_dc_clear_meta_file), diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java index 6da29c9b8..d21f7b1c4 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyInputParcel.java @@ -41,6 +41,7 @@ public abstract class PgpDecryptVerifyInputParcel implements Parcelable { abstract boolean isAllowSymmetricDecryption(); abstract boolean isDecryptMetadataOnly(); + abstract boolean isAutocryptSetup(); @Nullable abstract List getAllowedKeyIds(); @@ -55,7 +56,8 @@ public abstract class PgpDecryptVerifyInputParcel implements Parcelable { public static Builder builder() { return new AutoValue_PgpDecryptVerifyInputParcel.Builder() .setAllowSymmetricDecryption(false) - .setDecryptMetadataOnly(false); + .setDecryptMetadataOnly(false) + .setAutocryptSetup(false); } @AutoValue.Builder @@ -68,6 +70,7 @@ public abstract class PgpDecryptVerifyInputParcel implements Parcelable { public abstract Builder setDecryptMetadataOnly(boolean decryptMetadataOnly); public abstract Builder setDetachedSignature(byte[] detachedSignature); public abstract Builder setSenderAddress(String senderAddress); + public abstract Builder setAutocryptSetup(boolean isAutocryptSetup); public abstract Builder setAllowedKeyIds(List allowedKeyIds); abstract List getAllowedKeyIds(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java index ec97b2213..6451019a5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpDecryptVerifyOperation.java @@ -93,6 +93,7 @@ import static java.lang.String.format; public class PgpDecryptVerifyOperation extends BaseOperation { public static final int PROGRESS_STRIDE_MILLISECONDS = 200; + public static final String PASSPHRASE_FORMAT_NUMERIC9X4 = "numeric9x4"; public PgpDecryptVerifyOperation(Context context, KeyRepository keyRepository, Progressable progressable) { super(context, keyRepository, progressable); @@ -230,6 +231,8 @@ public class PgpDecryptVerifyOperation extends BaseOperation(allowedSigningKeyIds))); return this; } + + public abstract Builder setPassphraseFormat(String passphraseFormat); + public abstract Builder setPassphraseBegin(String passphraseBegin); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java index 96c614dd6..bf24bebfb 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/pgp/PgpSignEncryptOperation.java @@ -202,9 +202,13 @@ public class PgpSignEncryptOperation extends BaseOperation inputUris, boolean canDelete) { + public void displayListFragment(ArrayList inputUris, boolean canDelete, boolean isAutocryptSetup) { - DecryptListFragment frag = DecryptListFragment.newInstance(inputUris, canDelete); + DecryptListFragment frag = DecryptListFragment.newInstance(inputUris, canDelete, isAutocryptSetup); FragmentManager fragMan = getSupportFragmentManager(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java index 17812a13a..b1aaecc20 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/DecryptListFragment.java @@ -125,6 +125,7 @@ public class DecryptListFragment public static final String ARG_CANCELLED_URIS = "cancelled_uris"; public static final String ARG_RESULTS = "results"; public static final String ARG_CAN_DELETE = "can_delete"; + public static final String ARG_IS_AUTOCRYPT_SETUP = "is_autocrypt_setup"; private static final int REQUEST_CODE_OUTPUT = 0x00007007; private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12; @@ -136,6 +137,7 @@ public class DecryptListFragment private Uri mCurrentInputUri; private boolean mCanDelete; + private boolean mIsAutocryptSetup; private DecryptFilesAdapter mAdapter; private Uri mCurrentSaveFileUri; @@ -143,12 +145,13 @@ public class DecryptListFragment /** * Creates new instance of this fragment */ - public static DecryptListFragment newInstance(@NonNull ArrayList uris, boolean canDelete) { + public static DecryptListFragment newInstance(@NonNull ArrayList uris, boolean canDelete, boolean isAutocryptSetup) { DecryptListFragment frag = new DecryptListFragment(); Bundle args = new Bundle(); args.putParcelableArrayList(ARG_INPUT_URIS, uris); args.putBoolean(ARG_CAN_DELETE, canDelete); + args.putBoolean(ARG_IS_AUTOCRYPT_SETUP, isAutocryptSetup); frag.setArguments(args); return frag; @@ -205,6 +208,7 @@ public class DecryptListFragment outState.putParcelable(ARG_OUTPUT_URIS, new ParcelableHashMap<>(mInputDataResults)); outState.putParcelableArrayList(ARG_CANCELLED_URIS, mCancelledInputUris); outState.putBoolean(ARG_CAN_DELETE, mCanDelete); + outState.putBoolean(ARG_IS_AUTOCRYPT_SETUP, mIsAutocryptSetup); // this does not save mCurrentInputUri - if anything is being // processed at fragment recreation time, the operation in @@ -222,6 +226,7 @@ public class DecryptListFragment ParcelableHashMap results = args.getParcelable(ARG_RESULTS); mCanDelete = args.getBoolean(ARG_CAN_DELETE, false); + mIsAutocryptSetup = args.getBoolean(ARG_IS_AUTOCRYPT_SETUP, false); displayInputUris(inputUris, cancelledUris, results != null ? results.getMap() : null @@ -638,11 +643,14 @@ public class DecryptListFragment } PgpDecryptVerifyInputParcel.Builder decryptInput = PgpDecryptVerifyInputParcel.builder() - .setAllowSymmetricDecryption(true); + .setAllowSymmetricDecryption(true) + .setAutocryptSetup(mIsAutocryptSetup); return InputDataParcel.createInputDataParcel(mCurrentInputUri, decryptInput.build()); } + + /** * Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris. *

diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java index fb7147dea..bd0f84764 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -31,6 +31,7 @@ import android.support.v4.app.DialogFragment; import android.support.v4.app.FragmentActivity; import android.support.v7.app.AlertDialog; import android.text.Editable; +import android.text.InputFilter; import android.text.InputType; import android.text.TextWatcher; import android.text.method.PasswordTransformationMethod; @@ -68,6 +69,7 @@ import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.Require import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder; import org.sufficientlysecure.keychain.ui.util.ThemeChanger; import org.sufficientlysecure.keychain.ui.widget.CacheTTLSpinner; +import org.sufficientlysecure.keychain.ui.widget.PrefixedEditText; import org.sufficientlysecure.keychain.util.Passphrase; import org.sufficientlysecure.keychain.util.Preferences; import timber.log.Timber; @@ -193,7 +195,7 @@ public class PassphraseDialogActivity extends FragmentActivity { mBackupCodeEditText[4] = view.findViewById(R.id.backup_code_5); mBackupCodeEditText[5] = view.findViewById(R.id.backup_code_6); - setupEditTextFocusNext(mBackupCodeEditText); + setupEditTextFocusNext(mBackupCodeEditText, false); AlertDialog dialog = alert.create(); dialog.setButton(DialogInterface.BUTTON_POSITIVE, @@ -201,6 +203,49 @@ public class PassphraseDialogActivity extends FragmentActivity { return dialog; } + if (mRequiredInput.mType == RequiredInputType.NUMERIC_9X4 || + mRequiredInput.mType == RequiredInputType.NUMERIC_9X4_AUTOCRYPT) { + LayoutInflater inflater = LayoutInflater.from(theme); + View view = inflater.inflate(R.layout.passphrase_dialog_numeric_9x4, null); + alert.setView(view); + + mBackupCodeEditText = new EditText[9]; + mBackupCodeEditText[0] = view.findViewById(R.id.transfer_code_block_1); + mBackupCodeEditText[1] = view.findViewById(R.id.transfer_code_block_2); + mBackupCodeEditText[2] = view.findViewById(R.id.transfer_code_block_3); + mBackupCodeEditText[3] = view.findViewById(R.id.transfer_code_block_4); + mBackupCodeEditText[4] = view.findViewById(R.id.transfer_code_block_5); + mBackupCodeEditText[5] = view.findViewById(R.id.transfer_code_block_6); + mBackupCodeEditText[6] = view.findViewById(R.id.transfer_code_block_7); + mBackupCodeEditText[7] = view.findViewById(R.id.transfer_code_block_8); + mBackupCodeEditText[8] = view.findViewById(R.id.transfer_code_block_9); + + if (mRequiredInput.hasPassphraseBegin()) { + String beginChars = mRequiredInput.getPassphraseBegin(); + int inputLength = 4 - beginChars.length(); + setupEditTextFocusNext(mBackupCodeEditText, true); + + PrefixedEditText prefixEditText = (PrefixedEditText) mBackupCodeEditText[0]; + if (beginChars.matches("\\d\\d")) { + prefixEditText.setPrefix(beginChars); + prefixEditText.setHint("1234".substring(inputLength)); + prefixEditText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(inputLength) }); + } + } else { + setupEditTextFocusNext(mBackupCodeEditText, false); + } + + if (mRequiredInput.mType == RequiredInputType.NUMERIC_9X4_AUTOCRYPT) { + TextView promptText = view.findViewById(R.id.passphrase_text); + promptText.setText(R.string.passphrase_transfer_autocrypt); + } + + AlertDialog dialog = alert.create(); + dialog.setButton(DialogInterface.BUTTON_POSITIVE, + activity.getString(R.string.btn_proceed), (DialogInterface.OnClickListener) null); + return dialog; + } + LayoutInflater inflater = LayoutInflater.from(theme); mLayout = (ViewAnimator) inflater.inflate(R.layout.passphrase_dialog, null); alert.setView(mLayout); @@ -377,10 +422,9 @@ public class PassphraseDialogActivity extends FragmentActivity { textView.requestFocus(); } - private static void setupEditTextFocusNext(final EditText[] backupCodes) { + private static void setupEditTextFocusNext(EditText[] backupCodes, boolean hasPrefix) { for (int i = 0; i < backupCodes.length - 1; i++) { - - final int next = i + 1; + int idx = i; backupCodes[i].addTextChangedListener(new TextWatcher() { @Override @@ -390,10 +434,11 @@ public class PassphraseDialogActivity extends FragmentActivity { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { boolean inserting = before < count; - boolean cursorAtEnd = (start + count) == 4; + int maxLen = hasPrefix && idx == 0 ? 2 : 4; + boolean cursorAtEnd = (start + count) == maxLen; if (inserting && cursorAtEnd) { - backupCodes[next].requestFocus(); + backupCodes[idx + 1].requestFocus(); } } @@ -401,7 +446,6 @@ public class PassphraseDialogActivity extends FragmentActivity { public void afterTextChanged(Editable s) { } }); - } } @@ -432,6 +476,27 @@ public class PassphraseDialogActivity extends FragmentActivity { return; } + if (mRequiredInput.mType == RequiredInputType.NUMERIC_9X4 || + mRequiredInput.mType == RequiredInputType.NUMERIC_9X4_AUTOCRYPT) { + StringBuilder backupCodeInput = new StringBuilder(36); + if (mRequiredInput.hasPassphraseBegin()) { + backupCodeInput.append(mRequiredInput.getPassphraseBegin()); + } + for (EditText editText : mBackupCodeEditText) { + if (editText.getText().length() != 2 && editText.getText().length() != 4) { + return; + } + backupCodeInput.append(editText.getText()); + backupCodeInput.append('-'); + } + backupCodeInput.deleteCharAt(backupCodeInput.length() - 1); + + Passphrase passphrase = new Passphrase(backupCodeInput.toString()); + finishCaching(passphrase, null); + + return; + } + final Passphrase passphrase = new Passphrase(mPassphraseEditText); final int timeToLiveSeconds = mTimeToLiveSpinner.getSelectedTimeToLive(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java index 248352453..06dc54345 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/base/CryptoOperationHelper.java @@ -162,7 +162,9 @@ public class CryptoOperationHelper. + */ + +package org.sufficientlysecure.keychain.util; + + +import java.nio.CharBuffer; +import java.security.SecureRandom; +import java.util.Random; +import java.util.regex.Pattern; + +import android.support.annotation.VisibleForTesting; + +// see https://autocrypt.org/level1.html#setup-code +public class Numeric9x4PassphraseUtil { + public static Passphrase generateNumeric9x4Passphrase() { + return generateNumeric9x4Passphrase(new SecureRandom()); + } + + @VisibleForTesting + static Passphrase generateNumeric9x4Passphrase(Random r) { + StringBuilder code = new StringBuilder(36); + for (int i = 0; i < 36; i++) { + boolean isBeginningOfBlock = i > 0 && (i % 4) == 0; + if (isBeginningOfBlock) { + code.append('-'); + } + + String digit = Integer.toString(r.nextInt(10)); + code.append(digit); + } + + return new Passphrase(code.toString()); + } + + private static final Pattern AUTOCRYPT_TRANSFER_CODE = Pattern.compile("(\\d{4}-){8}\\d{4}"); + + public static boolean isNumeric9x4Passphrase(Passphrase transferCodeChars) { + return isNumeric9x4Passphrase(CharBuffer.wrap(transferCodeChars.getCharArray())); + } + + public static boolean isNumeric9x4Passphrase(CharSequence code) { + return AUTOCRYPT_TRANSFER_CODE.matcher(code).matches(); + } +} diff --git a/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml b/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml deleted file mode 100644 index dfeed3f2d..000000000 --- a/OpenKeychain/src/main/res/layout-w400dp/backup_code_fragment.xml +++ /dev/null @@ -1,459 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -