Merge branch 'master' into backup-api

Conflicts:
	OpenKeychain/src/main/java/org/sufficientlysecure/keychain/remote/OpenPgpService.java
	extern/openpgp-api-lib
This commit is contained in:
Dominik Schürmann
2016-05-07 12:01:16 +03:00
272 changed files with 12293 additions and 4368 deletions

View File

@@ -18,14 +18,6 @@
package org.sufficientlysecure.keychain.ui;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
@@ -43,7 +35,7 @@ import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentManager.OnBackStackChangedListener;
import android.text.Editable;
import android.text.InputType;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -53,12 +45,9 @@ import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import com.github.pinball83.maskededittext.MaskedEditText;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.ExportResult;
@@ -73,6 +62,14 @@ import org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.Passphrase;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringParcel, ExportResult>
implements OnBackStackChangedListener {
@@ -100,7 +97,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
String mBackupCode;
private boolean mExecuteBackupOperation;
private MaskedEditText mCodeEditText;
private EditText[] mCodeEditText;
private ToolableViewAnimator mStatusAnimator, mTitleAnimator, mCodeFieldsAnimator;
private Integer mBackStackLevel;
@@ -152,8 +149,13 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
boolean newCheckedState = !item.isChecked();
item.setChecked(newCheckedState);
mDebugModeAcceptAnyCode = newCheckedState;
if (newCheckedState) {
mCodeEditText.setText("ABCD-EFGH-IJKL-MNOP-QRST-UVWX");
if (newCheckedState && TextUtils.isEmpty(mCodeEditText[0].getText())) {
mCodeEditText[0].setText("ABCD");
mCodeEditText[1].setText("EFGH");
mCodeEditText[2].setText("IJKL");
mCodeEditText[3].setText("MNOP");
mCodeEditText[4].setText("QRST");
mCodeEditText[5].setText("UVWX");
Notify.create(getActivity(), "Actual backup code is all 'A's", Style.WARN).show();
}
return true;
@@ -177,11 +179,9 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mTitleAnimator.setDisplayedChild(1, animate);
mStatusAnimator.setDisplayedChild(1, animate);
mCodeFieldsAnimator.setDisplayedChild(1, animate);
// use non-breaking spaces to enlarge the empty EditText appropriately
String empty = "\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0" +
"-\u00a0\u00a0\u00a0\u00a0-\u00a0\u00a0\u00a0\u00a0";
mCodeEditText.setText(empty);
for (EditText editText : mCodeEditText) {
editText.setText("");
}
pushBackStackEntry();
@@ -195,7 +195,7 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard();
if (animate) {
@ColorInt int black = mCodeEditText.getCurrentTextColor();
@ColorInt int black = mCodeEditText[0].getCurrentTextColor();
@ColorInt int red = getResources().getColor(R.color.android_red_dark);
animateFlashText(mCodeEditText, black, red, false);
}
@@ -214,14 +214,18 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
hideKeyboard();
mCodeEditText.setEnabled(false);
for (EditText editText : mCodeEditText) {
editText.setEnabled(false);
}
@ColorInt int green = getResources().getColor(R.color.android_green_dark);
if (animate) {
@ColorInt int black = mCodeEditText.getCurrentTextColor();
@ColorInt int black = mCodeEditText[0].getCurrentTextColor();
animateFlashText(mCodeEditText, black, green, true);
} else {
mCodeEditText.setTextColor(green);
for (TextView textView : mCodeEditText) {
textView.setTextColor(green);
}
}
popBackStackNoAction();
@@ -257,23 +261,39 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
mExportSecret = args.getBoolean(ARG_EXPORT_SECRET);
mExecuteBackupOperation = args.getBoolean(ARG_EXECUTE_BACKUP_OPERATION, true);
// NOTE: order of these method calls matter, see setupAutomaticLinebreak()
mCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code_input);
mCodeEditText.setInputType(
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
setupAutomaticLinebreak(mCodeEditText);
mCodeEditText.setImeOptions(EditorInfo.IME_ACTION_DONE);
mCodeEditText = new EditText[6];
mCodeEditText[0] = (EditText) view.findViewById(R.id.backup_code_1);
mCodeEditText[1] = (EditText) view.findViewById(R.id.backup_code_2);
mCodeEditText[2] = (EditText) view.findViewById(R.id.backup_code_3);
mCodeEditText[3] = (EditText) view.findViewById(R.id.backup_code_4);
mCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
mCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
{
TextView[] codeDisplayText = new TextView[6];
codeDisplayText[0] = (TextView) view.findViewById(R.id.backup_code_display_1);
codeDisplayText[1] = (TextView) view.findViewById(R.id.backup_code_display_2);
codeDisplayText[2] = (TextView) view.findViewById(R.id.backup_code_display_3);
codeDisplayText[3] = (TextView) view.findViewById(R.id.backup_code_display_4);
codeDisplayText[4] = (TextView) view.findViewById(R.id.backup_code_display_5);
codeDisplayText[5] = (TextView) view.findViewById(R.id.backup_code_display_6);
// set backup code in code TextViews
char[] backupCode = mBackupCode.toCharArray();
for (int i = 0; i < codeDisplayText.length; i++) {
codeDisplayText[i].setText(backupCode, i * 5, 4);
}
// set background to null in TextViews - this will retain padding from EditText style!
for (TextView textView : codeDisplayText) {
// noinspection deprecation, setBackground(Drawable) is API level >=16
textView.setBackgroundDrawable(null);
}
}
setupEditTextFocusNext(mCodeEditText);
setupEditTextSuccessListener(mCodeEditText);
TextView codeDisplayText = (TextView) view.findViewById(R.id.backup_code_display);
setupAutomaticLinebreak(codeDisplayText);
// set background to null in TextViews - this will retain padding from EditText style!
// noinspection deprecation, setBackground(Drawable) is API level >=16
codeDisplayText.setBackgroundDrawable(null);
codeDisplayText.setText(mBackupCode);
mStatusAnimator = (ToolableViewAnimator) view.findViewById(R.id.status_animator);
mTitleAnimator = (ToolableViewAnimator) view.findViewById(R.id.title_animator);
mCodeFieldsAnimator = (ToolableViewAnimator) view.findViewById(R.id.code_animator);
@@ -350,67 +370,76 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
outState.putInt(ARG_BACK_STACK, mBackStackLevel == null ? -1 : mBackStackLevel);
}
/**
* Automatic line break with max 6 lines for smaller displays
* <p/>
* NOTE: I was not able to get this behaviour using XML!
* Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
*/
private void setupAutomaticLinebreak(TextView textview) {
textview.setSingleLine(true);
textview.setMaxLines(6);
textview.setHorizontallyScrolling(false);
}
private void setupEditTextSuccessListener(final EditText[] backupCodes) {
for (EditText backupCode : backupCodes) {
private void setupEditTextSuccessListener(final MaskedEditText backupCode) {
backupCode.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
backupCode.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
|| mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
boolean partIsComplete = (backupCode.getText().toString().indexOf(' ') == -1)
&& (backupCode.getText().toString().indexOf('\u00a0') == -1);
if (!inInputState || !partIsComplete) {
return;
}
checkIfCodeIsCorrect(backupCode);
}
});
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.length() > 4) {
throw new AssertionError("max length of each field is 4!");
}
boolean inInputState = mCurrentState == BackupCodeState.STATE_INPUT
|| mCurrentState == BackupCodeState.STATE_INPUT_ERROR;
boolean partIsComplete = s.length() == 4;
if (!inInputState || !partIsComplete) {
return;
}
checkIfCodeIsCorrect();
}
});
}
}
private void checkIfCodeIsCorrect(EditText backupCode) {
private void checkIfCodeIsCorrect() {
if (Constants.DEBUG && mDebugModeAcceptAnyCode) {
switchState(BackupCodeState.STATE_OK, true);
return;
}
if (backupCode.toString().equals(mBackupCode)) {
StringBuilder backupCodeInput = new StringBuilder(26);
for (EditText editText : mCodeEditText) {
if (editText.getText().length() < 4) {
return;
}
backupCodeInput.append(editText.getText());
backupCodeInput.append('-');
}
backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
// if they don't match, do nothing
if (backupCodeInput.toString().equals(mBackupCode)) {
switchState(BackupCodeState.STATE_OK, true);
return;
}
switchState(BackupCodeState.STATE_INPUT_ERROR, true);
}
private static void animateFlashText(
final TextView textView, int color1, int color2, boolean staySecondColor) {
final TextView[] textViews, int color1, int color2, boolean staySecondColor) {
ValueAnimator anim = ValueAnimator.ofObject(new ArgbEvaluator(), color1, color2);
anim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
textView.setTextColor((Integer) animator.getAnimatedValue());
for (TextView textView : textViews) {
textView.setTextColor((Integer) animator.getAnimatedValue());
}
}
});
anim.setRepeatMode(ValueAnimator.REVERSE);
@@ -421,6 +450,34 @@ public class BackupCodeFragment extends CryptoOperationFragment<BackupKeyringPar
}
private static void setupEditTextFocusNext(final EditText[] backupCodes) {
for (int i = 0; i < backupCodes.length - 1; i++) {
final int next = i + 1;
backupCodes[i].addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean inserting = before < count;
boolean cursorAtEnd = (start + count) == 4;
if (inserting && cursorAtEnd) {
backupCodes[next].requestFocus();
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
}
private void pushBackStackEntry() {
if (mBackStackLevel != null) {
return;

View File

@@ -28,7 +28,8 @@ import org.sufficientlysecure.keychain.ui.base.BaseActivity;
public class CertifyKeyActivity extends BaseActivity {
public static final String EXTRA_RESULT = "operation_result";
public static final String EXTRA_KEY_IDS = "extra_key_ids";
// For sending masterKeyIds to MultiUserIdsFragment to display list of keys
public static final String EXTRA_KEY_IDS = MultiUserIdsFragment.EXTRA_KEY_IDS ;
public static final String EXTRA_CERTIFY_KEY_ID = "certify_key_id";
@Override

View File

@@ -62,58 +62,26 @@ import java.util.ArrayList;
import java.util.Date;
public class CertifyKeyFragment
extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult>
implements LoaderManager.LoaderCallbacks<Cursor> {
public static final String ARG_CHECK_STATES = "check_states";
extends CachingCryptoOperationFragment<CertifyActionsParcel, CertifyResult> {
private CheckBox mUploadKeyCheckbox;
ListView mUserIds;
private CertifyKeySpinner mCertifyKeySpinner;
private long[] mPubMasterKeyIds;
public static final String[] USER_IDS_PROJECTION = new String[]{
UserPackets._ID,
UserPackets.MASTER_KEY_ID,
UserPackets.USER_ID,
UserPackets.IS_PRIMARY,
UserPackets.IS_REVOKED
};
private static final int INDEX_MASTER_KEY_ID = 1;
private static final int INDEX_USER_ID = 2;
@SuppressWarnings("unused")
private static final int INDEX_IS_PRIMARY = 3;
@SuppressWarnings("unused")
private static final int INDEX_IS_REVOKED = 4;
private MultiUserIdsAdapter mUserIdsAdapter;
private MultiUserIdsFragment mMultiUserIdsFragment;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(CertifyKeyActivity.EXTRA_KEY_IDS);
if (mPubMasterKeyIds == null) {
Log.e(Constants.TAG, "List of key ids to certify missing!");
getActivity().finish();
return;
}
ArrayList<Boolean> checkedStates;
if (savedInstanceState != null) {
checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
// key spinner and the checkbox keep their own state
} else {
checkedStates = null;
if (savedInstanceState == null) {
// preselect certify key id if given
long certifyKeyId = getActivity().getIntent()
.getLongExtra(CertifyKeyActivity.EXTRA_CERTIFY_KEY_ID, Constants.key.none);
if (certifyKeyId != Constants.key.none) {
try {
CachedPublicKeyRing key = (new ProviderHelper(getActivity())).getCachedPublicKeyRing(certifyKeyId);
CachedPublicKeyRing key = (new ProviderHelper(getActivity()))
.getCachedPublicKeyRing(certifyKeyId);
if (key.canCertify()) {
mCertifyKeySpinner.setPreSelectedKeyId(certifyKeyId);
}
@@ -121,15 +89,8 @@ public class CertifyKeyFragment
Log.e(Constants.TAG, "certify certify check failed", e);
}
}
}
mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates);
mUserIds.setAdapter(mUserIdsAdapter);
mUserIds.setDividerHeight(0);
getLoaderManager().initLoader(0, null, this);
OperationResult result = getActivity().getIntent().getParcelableExtra(CertifyKeyActivity.EXTRA_RESULT);
if (result != null) {
// display result from import
@@ -137,22 +98,14 @@ public class CertifyKeyFragment
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
// no proper parceling method available :(
outState.putSerializable(ARG_CHECK_STATES, states);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.certify_key_fragment, null);
mCertifyKeySpinner = (CertifyKeySpinner) view.findViewById(R.id.certify_key_spinner);
mUploadKeyCheckbox = (CheckBox) view.findViewById(R.id.sign_key_upload_checkbox);
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
mMultiUserIdsFragment = (MultiUserIdsFragment)
getChildFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
// make certify image gray, like action icons
ImageView vActionCertifyImage =
@@ -183,128 +136,11 @@ public class CertifyKeyFragment
return view;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = UserPackets.buildUserIdsUri();
String selection, ids[];
{
// generate placeholders and string selection args
ids = new String[mPubMasterKeyIds.length];
StringBuilder placeholders = new StringBuilder("?");
for (int i = 0; i < mPubMasterKeyIds.length; i++) {
ids[i] = Long.toString(mPubMasterKeyIds[i]);
if (i != 0) {
placeholders.append(",?");
}
}
// put together selection string
selection = UserPackets.IS_REVOKED + " = 0" + " AND "
+ Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID
+ " IN (" + placeholders + ")";
}
return new CursorLoader(getActivity(), uri,
USER_IDS_PROJECTION, selection, ids,
Tables.USER_PACKETS + "." + UserPackets.MASTER_KEY_ID + " ASC"
+ ", " + Tables.USER_PACKETS + "." + UserPackets.USER_ID + " ASC"
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
MatrixCursor matrix = new MatrixCursor(new String[]{
"_id", "user_data", "grouped"
}) {
@Override
public byte[] getBlob(int column) {
return super.getBlob(column);
}
};
data.moveToFirst();
long lastMasterKeyId = 0;
String lastName = "";
ArrayList<String> uids = new ArrayList<>();
boolean header = true;
// Iterate over all rows
while (!data.isAfterLast()) {
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
String userId = data.getString(INDEX_USER_ID);
KeyRing.UserId pieces = KeyRing.splitUserId(userId);
// Two cases:
boolean grouped = masterKeyId == lastMasterKeyId;
boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
// Remember for next loop
lastName = pieces.name;
Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
if (!subGrouped) {
// 1. This name should NOT be grouped with the previous, so we flush the buffer
Parcel p = Parcel.obtain();
p.writeStringList(uids);
byte[] d = p.marshall();
p.recycle();
matrix.addRow(new Object[]{
lastMasterKeyId, d, header ? 1 : 0
});
// indicate that we have a header for this masterKeyId
header = false;
// Now clear the buffer, and add the new user id, for the next round
uids.clear();
}
// 2. This name should be grouped with the previous, just add to buffer
uids.add(userId);
lastMasterKeyId = masterKeyId;
// If this one wasn't grouped, the next one's gotta be a header
if (!grouped) {
header = true;
}
// Regardless of the outcome, move to next entry
data.moveToNext();
}
// If there is anything left in the buffer, flush it one last time
if (!uids.isEmpty()) {
Parcel p = Parcel.obtain();
p.writeStringList(uids);
byte[] d = p.marshall();
p.recycle();
matrix.addRow(new Object[]{
lastMasterKeyId, d, header ? 1 : 0
});
}
mUserIdsAdapter.swapCursor(matrix);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mUserIdsAdapter.swapCursor(null);
}
@Override
public CertifyActionsParcel createOperationInput() {
// Bail out if there is not at least one user id selected
ArrayList<CertifyAction> certifyActions = mUserIdsAdapter.getSelectedCertifyActions();
ArrayList<CertifyAction> certifyActions = mMultiUserIdsFragment.getSelectedCertifyActions();
if (certifyActions.isEmpty()) {
Notify.create(getActivity(), "No identities selected!",
Notify.Style.ERROR).show();

View File

@@ -21,6 +21,7 @@ import android.content.Intent;
import android.nfc.NfcAdapter;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import org.sufficientlysecure.keychain.R;
@@ -28,7 +29,7 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -36,7 +37,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.io.IOException;
import java.util.ArrayList;
public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
public class CreateKeyActivity extends BaseSecurityTokenActivity {
public static final String EXTRA_NAME = "name";
public static final String EXTRA_EMAIL = "email";
@@ -47,9 +48,9 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
public static final String EXTRA_SECURITY_TOKEN_PIN = "yubi_key_pin";
public static final String EXTRA_SECURITY_TOKEN_ADMIN_PIN = "yubi_key_admin_pin";
public static final String EXTRA_NFC_USER_ID = "nfc_user_id";
public static final String EXTRA_NFC_AID = "nfc_aid";
public static final String EXTRA_NFC_FINGERPRINTS = "nfc_fingerprints";
public static final String EXTRA_SECURITY_TOKEN_USER_ID = "nfc_user_id";
public static final String EXTRA_SECURITY_TOKEN_AID = "nfc_aid";
public static final String EXTRA_SECURITY_FINGERPRINTS = "nfc_fingerprints";
public static final String FRAGMENT_TAG = "currentFragment";
@@ -66,8 +67,8 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
byte[] mScannedFingerprints;
byte[] mNfcAid;
String mNfcUserId;
byte[] mSecurityTokenAid;
String mSecurityTokenUserId;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -77,7 +78,7 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
// NOTE: ACTION_NDEF_DISCOVERED and not ACTION_TAG_DISCOVERED like in BaseNfcActivity
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
mTagDispatcher.interceptIntent(getIntent());
mNfcTagDispatcher.interceptIntent(getIntent());
setTitle(R.string.title_manage_my_keys);
@@ -107,10 +108,10 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
mFirstTime = intent.getBooleanExtra(EXTRA_FIRST_TIME, false);
mCreateSecurityToken = intent.getBooleanExtra(EXTRA_CREATE_SECURITY_TOKEN, false);
if (intent.hasExtra(EXTRA_NFC_FINGERPRINTS)) {
byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_NFC_FINGERPRINTS);
String nfcUserId = intent.getStringExtra(EXTRA_NFC_USER_ID);
byte[] nfcAid = intent.getByteArrayExtra(EXTRA_NFC_AID);
if (intent.hasExtra(EXTRA_SECURITY_FINGERPRINTS)) {
byte[] nfcFingerprints = intent.getByteArrayExtra(EXTRA_SECURITY_FINGERPRINTS);
String nfcUserId = intent.getStringExtra(EXTRA_SECURITY_TOKEN_USER_ID);
byte[] nfcAid = intent.getByteArrayExtra(EXTRA_SECURITY_TOKEN_AID);
if (containsKeys(nfcFingerprints)) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
@@ -143,24 +144,32 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
}
@Override
protected void doNfcInBackground() throws IOException {
if (mCurrentFragment instanceof NfcListenerFragment) {
((NfcListenerFragment) mCurrentFragment).doNfcInBackground();
protected void doSecurityTokenInBackground() throws IOException {
if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
((SecurityTokenListenerFragment) mCurrentFragment).doSecurityTokenInBackground();
return;
}
mScannedFingerprints = nfcGetFingerprints();
mNfcAid = nfcGetAid();
mNfcUserId = nfcGetUserId();
mScannedFingerprints = mSecurityTokenHelper.getFingerprints();
mSecurityTokenAid = mSecurityTokenHelper.getAid();
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
}
@Override
protected void onNfcPostExecute() {
if (mCurrentFragment instanceof NfcListenerFragment) {
((NfcListenerFragment) mCurrentFragment).onNfcPostExecute();
protected void onSecurityTokenPostExecute() {
if (mCurrentFragment instanceof SecurityTokenListenerFragment) {
((SecurityTokenListenerFragment) mCurrentFragment).onSecurityTokenPostExecute();
return;
}
// We don't want get back to wait activity mainly because it looks weird with otg token
if (mCurrentFragment instanceof CreateSecurityTokenWaitFragment) {
// hack from http://stackoverflow.com/a/11253987
CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = true;
getSupportFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
CreateSecurityTokenWaitFragment.sDisableFragmentAnimations = false;
}
if (containsKeys(mScannedFingerprints)) {
try {
long masterKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mScannedFingerprints);
@@ -169,15 +178,15 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mScannedFingerprints);
startActivity(intent);
finish();
} catch (PgpKeyNotFoundException e) {
Fragment frag = CreateSecurityTokenImportResetFragment.newInstance(
mScannedFingerprints, mNfcAid, mNfcUserId);
mScannedFingerprints, mSecurityTokenAid, mSecurityTokenUserId);
loadFragment(frag, FragAction.TO_RIGHT);
}
} else {
@@ -252,12 +261,11 @@ public class CreateKeyActivity extends BaseSecurityTokenNfcActivity {
// do it immediately!
getSupportFragmentManager().executePendingTransactions();
}
interface NfcListenerFragment {
void doNfcInBackground() throws IOException;
void onNfcPostExecute();
interface SecurityTokenListenerFragment {
void doSecurityTokenInBackground() throws IOException;
void onSecurityTokenPostExecute();
}
@Override

View File

@@ -44,7 +44,6 @@ import org.sufficientlysecure.keychain.ui.widget.EmailEditText;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class CreateKeyEmailFragment extends Fragment {
private CreateKeyActivity mCreateKeyActivity;
@@ -52,10 +51,6 @@ public class CreateKeyEmailFragment extends Fragment {
private ArrayList<EmailAdapter.ViewModel> mAdditionalEmailModels = new ArrayList<>();
private EmailAdapter mEmailAdapter;
// NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
// EMAIL_ADDRESS fails for mails with umlauts for example
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
/**
* Creates new instance of this fragment
*/
@@ -76,16 +71,15 @@ public class CreateKeyEmailFragment extends Fragment {
* @return true if EditText is not empty
*/
private boolean isMainEmailValid(EditText editText) {
boolean output = true;
if (!checkEmail(editText.getText().toString(), false)) {
if (editText.getText().length() == 0) {
editText.setError(getString(R.string.create_key_empty));
editText.requestFocus();
output = false;
} else {
editText.setError(null);
return false;
} else if (!checkEmail(editText.getText().toString(), false)){
return false;
}
return output;
editText.setError(null);
return true;
}
@Override
@@ -146,10 +140,9 @@ public class CreateKeyEmailFragment extends Fragment {
* @return
*/
private boolean checkEmail(String email, boolean additionalEmail) {
// check for email format or if the user did any input
if (!isEmailFormatValid(email)) {
if (email.isEmpty()) {
Notify.create(getActivity(),
getString(R.string.create_key_email_invalid_email),
getString(R.string.create_key_email_empty_email),
Notify.LENGTH_LONG, Notify.Style.ERROR).show(CreateKeyEmailFragment.this);
return false;
}
@@ -166,18 +159,6 @@ public class CreateKeyEmailFragment extends Fragment {
return true;
}
/**
* Checks the email format
* Uses the default Android Email Pattern
*
* @param email
* @return
*/
private boolean isEmailFormatValid(String email) {
// check for email format or if the user did any input
return !(email.length() == 0 || !EMAIL_PATTERN.matcher(email).matches());
}
/**
* Checks for duplicated emails inside the additional email adapter.
*

View File

@@ -44,9 +44,9 @@ import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Algorithm;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -57,6 +57,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
import java.util.Date;
import java.util.Iterator;
import java.util.regex.Pattern;
public class CreateKeyFinalFragment extends Fragment {
@@ -81,6 +82,10 @@ public class CreateKeyFinalFragment extends Fragment {
private OperationResult mQueuedFinishResult;
private EditKeyResult mQueuedDisplayResult;
// NOTE: Do not use more complicated pattern like defined in android.util.Patterns.EMAIL_ADDRESS
// EMAIL_ADDRESS fails for mails with umlauts for example
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\S]+@[\\S]+\\.[a-z]+$");
public static CreateKeyFinalFragment newInstance() {
CreateKeyFinalFragment frag = new CreateKeyFinalFragment();
frag.setRetainInstance(true);
@@ -106,7 +111,11 @@ public class CreateKeyFinalFragment extends Fragment {
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
// set values
mNameEdit.setText(createKeyActivity.mName);
if (createKeyActivity.mName != null) {
mNameEdit.setText(createKeyActivity.mName);
} else {
mNameEdit.setText(getString(R.string.user_id_no_name));
}
if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
String emailText = createKeyActivity.mEmail + ", ";
Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator();
@@ -122,6 +131,8 @@ public class CreateKeyFinalFragment extends Fragment {
mEmailEdit.setText(createKeyActivity.mEmail);
}
checkEmailValidity();
mCreateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -278,18 +289,20 @@ public class CreateKeyFinalFragment extends Fragment {
2048, null, KeyFlags.AUTHENTICATION, 0L));
// use empty passphrase
saveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(new Passphrase(), null);
saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(new Passphrase()));
} else {
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
4096, null, KeyFlags.CERTIFY_OTHER, 0L));
3072, null, KeyFlags.CERTIFY_OTHER, 0L));
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
4096, null, KeyFlags.SIGN_DATA, 0L));
3072, null, KeyFlags.SIGN_DATA, 0L));
saveKeyringParcel.mAddSubKeys.add(new SaveKeyringParcel.SubkeyAdd(Algorithm.RSA,
4096, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
3072, null, KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE, 0L));
saveKeyringParcel.mNewUnlock = createKeyActivity.mPassphrase != null
? new ChangeUnlockParcel(createKeyActivity.mPassphrase, null)
: null;
if (createKeyActivity.mPassphrase != null) {
saveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(createKeyActivity.mPassphrase));
} else {
saveKeyringParcel.setNewUnlock(null);
}
}
String userId = KeyRing.createUserId(
new KeyRing.UserId(createKeyActivity.mName, createKeyActivity.mEmail, null)
@@ -309,6 +322,31 @@ public class CreateKeyFinalFragment extends Fragment {
return saveKeyringParcel;
}
private void checkEmailValidity() {
CreateKeyActivity createKeyActivity = (CreateKeyActivity) getActivity();
boolean emailsValid = true;
if (!EMAIL_PATTERN.matcher(createKeyActivity.mEmail).matches()) {
emailsValid = false;
}
if (createKeyActivity.mAdditionalEmails != null && createKeyActivity.mAdditionalEmails.size() > 0) {
for (Iterator<?> it = createKeyActivity.mAdditionalEmails.iterator(); it.hasNext(); ) {
if (!EMAIL_PATTERN.matcher(it.next().toString()).matches()) {
emailsValid = false;
}
}
}
if (!emailsValid) {
mEmailEdit.setError(getString(R.string.create_key_final_email_valid_warning));
mEmailEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mNameEdit.requestFocus(); // Workaround to remove focus from email
}
});
}
}
private void createKey() {
CreateKeyActivity activity = (CreateKeyActivity) getActivity();
if (activity == null) {

View File

@@ -18,13 +18,11 @@
package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
@@ -50,27 +48,6 @@ public class CreateKeyNameFragment extends Fragment {
return frag;
}
/**
* Checks if text of given EditText is not empty. If it is empty an error is
* set and the EditText gets the focus.
*
* @param context
* @param editText
* @return true if EditText is not empty
*/
private static boolean isEditTextNotEmpty(Context context, EditText editText) {
boolean output = true;
if (editText.getText().length() == 0) {
editText.setError(context.getString(R.string.create_key_empty));
editText.requestFocus();
output = false;
} else {
editText.setError(null);
}
return output;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_key_name_fragment, container, false);
@@ -109,13 +86,11 @@ public class CreateKeyNameFragment extends Fragment {
}
private void nextClicked() {
if (isEditTextNotEmpty(getActivity(), mNameEdit)) {
// save state
mCreateKeyActivity.mName = mNameEdit.getText().toString();
// save state
mCreateKeyActivity.mName = mNameEdit.getText().length() == 0 ? null : mNameEdit.getText().toString();
CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
}
CreateKeyEmailFragment frag = CreateKeyEmailFragment.newInstance();
mCreateKeyActivity.loadFragment(frag, FragAction.TO_RIGHT);
}
}

View File

@@ -25,7 +25,6 @@ import java.util.ArrayList;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
@@ -43,7 +42,7 @@ import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.NfcListenerFragment;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.SecurityTokenListenerFragment;
import org.sufficientlysecure.keychain.ui.base.QueueingCryptoOperationFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Preferences;
@@ -51,7 +50,7 @@ import org.sufficientlysecure.keychain.util.Preferences;
public class CreateSecurityTokenImportResetFragment
extends QueueingCryptoOperationFragment<ImportKeyringParcel, ImportKeyResult>
implements NfcListenerFragment {
implements SecurityTokenListenerFragment {
private static final int REQUEST_CODE_RESET = 0x00005001;
@@ -231,7 +230,7 @@ public class CreateSecurityTokenImportResetFragment
public void resetCard() {
Intent intent = new Intent(getActivity(), SecurityTokenOperationActivity.class);
RequiredInputParcel resetP = RequiredInputParcel.createNfcReset();
RequiredInputParcel resetP = RequiredInputParcel.createSecurityTokenReset();
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, resetP);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, new CryptoInputParcel());
startActivityForResult(intent, REQUEST_CODE_RESET);
@@ -248,11 +247,11 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
public void doNfcInBackground() throws IOException {
public void doSecurityTokenInBackground() throws IOException {
mTokenFingerprints = mCreateKeyActivity.nfcGetFingerprints();
mTokenAid = mCreateKeyActivity.nfcGetAid();
mTokenUserId = mCreateKeyActivity.nfcGetUserId();
mTokenFingerprints = mCreateKeyActivity.getSecurityTokenHelper().getFingerprints();
mTokenAid = mCreateKeyActivity.getSecurityTokenHelper().getAid();
mTokenUserId = mCreateKeyActivity.getSecurityTokenHelper().getUserId();
byte[] fp = new byte[20];
ByteBuffer.wrap(fp).put(mTokenFingerprints, 0, 20);
@@ -260,7 +259,7 @@ public class CreateSecurityTokenImportResetFragment
}
@Override
public void onNfcPostExecute() {
public void onSecurityTokenPostExecute() {
setData();

View File

@@ -17,22 +17,35 @@
package org.sufficientlysecure.keychain.ui;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity.FragAction;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
public class CreateSecurityTokenWaitFragment extends Fragment {
public static boolean sDisableFragmentAnimations = false;
CreateKeyActivity mCreateKeyActivity;
View mBackButton;
@Override
public void onActivityCreated(@Nullable final Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (this.getActivity() instanceof BaseSecurityTokenActivity) {
((BaseSecurityTokenActivity) this.getActivity()).checkDeviceConnection();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.create_security_token_wait_fragment, container, false);
@@ -50,9 +63,22 @@ public class CreateSecurityTokenWaitFragment extends Fragment {
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
public void onAttach(Context context) {
super.onAttach(context);
mCreateKeyActivity = (CreateKeyActivity) getActivity();
}
/**
* hack from http://stackoverflow.com/a/11253987
*/
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
if (sDisableFragmentAnimations) {
Animation a = new Animation() {};
a.setDuration(0);
return a;
}
return super.onCreateAnimation(transit, enter, nextAnim);
}
}

View File

@@ -35,6 +35,7 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
@@ -49,8 +50,8 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.UserPackets;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel.SubkeyChange;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.ui.adapter.SubkeysAdapter;
@@ -128,7 +129,7 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup superContainer, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.edit_key_fragment, null);
View view = inflater.inflate(R.layout.edit_key_fragment, superContainer, false);
mUserIdsList = (ListView) view.findViewById(R.id.edit_key_user_ids);
mSubkeysList = (ListView) view.findViewById(R.id.edit_key_keys);
@@ -338,10 +339,8 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
Bundle data = message.getData();
// cache new returned passphrase!
mSaveKeyringParcel.mNewUnlock = new ChangeUnlockParcel(
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
null
);
mSaveKeyringParcel.setNewUnlock(new ChangeUnlockParcel(
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)));
}
}
};
@@ -441,50 +440,45 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
}
break;
}
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
// TODO: enable later when Admin PIN handling is resolved
Notify.create(getActivity(),
"This feature will be available in an upcoming OpenKeychain version.",
Notify.Style.WARN).show();
break;
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
secretKeyType == SecretKeyType.GNU_DUMMY) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
.show();
break;
}
// Activity activity = EditKeyFragment.this.getActivity();
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
// secretKeyType == SecretKeyType.GNU_DUMMY) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
// if (mSubkeysAdapter.getKeySize(position) != 2048) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
//
//
// SubkeyChange change;
// change = mSaveKeyringParcel.getSubkeyChange(keyId);
// if (change == null) {
// mSaveKeyringParcel.mChangeSubKeys.add(
// new SubkeyChange(keyId, false, true)
// );
// break;
// }
// // toggle
// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// // User had chosen to strip key, but now wants to divert it.
// change.mDummyStrip = false;
// }
// break;
int algorithm = mSubkeysAdapter.getAlgorithm(position);
if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
&& algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
&& algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
.show();
break;
}
if (mSubkeysAdapter.getKeySize(position) != 2048) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
.show();
break;
}
SubkeyChange change;
change = mSaveKeyringParcel.getSubkeyChange(keyId);
if (change == null) {
mSaveKeyringParcel.mChangeSubKeys.add(
new SubkeyChange(keyId, false, true)
);
break;
}
// toggle
change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// User had chosen to strip key, but now wants to divert it.
change.mDummyStrip = false;
}
break;
}
}
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();
@@ -562,15 +556,9 @@ public class EditKeyFragment extends QueueingCryptoOperationFragment<SaveKeyring
}
private void addSubkey() {
boolean willBeMasterKey;
if (mSubkeysAdapter != null) {
willBeMasterKey = mSubkeysAdapter.getCount() == 0 && mSubkeysAddedAdapter.getCount() == 0;
} else {
willBeMasterKey = mSubkeysAddedAdapter.getCount() == 0;
}
// new subkey will never be a masterkey, as masterkey cannot be removed
AddSubkeyDialogFragment addSubkeyDialogFragment =
AddSubkeyDialogFragment.newInstance(willBeMasterKey);
AddSubkeyDialogFragment.newInstance(false);
addSubkeyDialogFragment
.setOnAlgorithmSelectedListener(
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {

View File

@@ -247,10 +247,10 @@ public class EncryptFilesFragment
try {
mFilesAdapter.add(inputUri);
} catch (IOException e) {
String fileName = FileHelper.getFilename(getActivity(), inputUri);
Notify.create(getActivity(),
getActivity().getString(R.string.error_file_added_already, FileHelper.getFilename(getActivity(), inputUri)),
getActivity().getString(R.string.error_file_added_already, fileName),
Notify.Style.ERROR).show(this);
return;
}
// remove from pending input uris
@@ -729,6 +729,8 @@ public class EncryptFilesFragment
// make sure this is correct at this point
mAfterEncryptAction = AfterEncryptAction.SAVE;
cryptoOperation(new CryptoInputParcel(new Date()));
} else if (resultCode == Activity.RESULT_CANCELED) {
onCryptoOperationCancelled();
}
return;
}

View File

@@ -21,6 +21,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ViewAnimator;
import com.tokenautocomplete.TokenCompleteTextView;
@@ -79,9 +80,6 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
mSignKeySpinner = (KeySpinner) view.findViewById(R.id.sign);
mEncryptKeyView = (EncryptKeyCompletionView) view.findViewById(R.id.recipient_list);
mEncryptKeyView.setThreshold(1); // Start working from first character
// TODO: workaround for bug in TokenAutoComplete,
// see https://github.com/open-keychain/open-keychain/issues/1636
mEncryptKeyView.setDeletionStyle(TokenCompleteTextView.TokenDeleteStyle.ToString);
final ViewAnimator vSignatureIcon = (ViewAnimator) view.findViewById(R.id.result_signature_icon);
mSignKeySpinner.setOnKeyChangedListener(new OnKeyChangedListener() {
@@ -112,6 +110,14 @@ public class EncryptModeAsymmetricFragment extends EncryptModeFragment {
}
});
ImageView addRecipientImgView = (ImageView) view.findViewById(R.id.add_recipient);
addRecipientImgView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEncryptKeyView.showAllKeys();
}
});
return view;
}

View File

@@ -131,8 +131,11 @@ public class ImportKeysActivity extends BaseActivity
if (Intent.ACTION_VIEW.equals(action)) {
if (FacebookKeyserver.isFacebookHost(dataUri)) {
action = ACTION_IMPORT_KEY_FROM_FACEBOOK;
} else if ("http".equals(scheme) || "https".equals(scheme)) {
} else if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
} else if ("openpgp4fpr".equalsIgnoreCase(scheme)) {
action = ACTION_IMPORT_KEY_FROM_KEYSERVER;
extras.putString(EXTRA_FINGERPRINT, dataUri.getSchemeSpecificPart());
} else {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// delegate action to ACTION_IMPORT_KEY
@@ -413,11 +416,18 @@ public class ImportKeysActivity extends BaseActivity
intent.putExtra(ImportKeyResult.EXTRA_RESULT, result);
setResult(RESULT_OK, intent);
finish();
return;
}
} else if (result.isOkNew() || result.isOkUpdated()) {
// User has successfully imported a key, hide first time dialog
Preferences.getPreferences(this).setFirstTime(false);
result.createNotify(ImportKeysActivity.this)
.show((ViewGroup) findViewById(R.id.import_snackbar));
// Close activities opened for importing keys and go to the list of keys
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} else {
result.createNotify(ImportKeysActivity.this)
.show((ViewGroup) findViewById(R.id.import_snackbar));
}
}
// methods from CryptoOperationHelper.Callback

View File

@@ -17,29 +17,46 @@
package org.sufficientlysecure.keychain.ui;
import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.ClipboardReflection;
import org.sufficientlysecure.keychain.pgp.PgpHelper;
import org.sufficientlysecure.keychain.ui.ImportKeysListFragment.BytesLoaderState;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.FileHelper;
import org.sufficientlysecure.keychain.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
public class ImportKeysFileFragment extends Fragment {
private ImportKeysActivity mImportActivity;
private View mBrowse;
private View mClipboardButton;
public static final int REQUEST_CODE_FILE = 0x00007003;
private Uri mCurrentUri;
private static final int REQUEST_CODE_FILE = 0x00007003;
private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 12;
/**
* Creates new instance of this fragment
@@ -83,10 +100,10 @@ public class ImportKeysFileFragment extends Fragment {
sendText = clipboardText.toString();
sendText = PgpHelper.getPgpKeyContent(sendText);
if (sendText == null) {
Notify.create(mImportActivity, "Bad data!", Style.ERROR).show();
Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
return;
}
mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(sendText.getBytes(), null));
mImportActivity.loadCallback(new BytesLoaderState(sendText.getBytes(), null));
}
}
});
@@ -106,11 +123,12 @@ public class ImportKeysFileFragment extends Fragment {
switch (requestCode) {
case REQUEST_CODE_FILE: {
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
mCurrentUri = data.getData();
// load data
mImportActivity.loadCallback(new ImportKeysListFragment.BytesLoaderState(null, data.getData()));
if (checkAndRequestReadPermission(mCurrentUri)) {
startImportingKeys();
}
}
break;
}
@@ -121,4 +139,77 @@ public class ImportKeysFileFragment extends Fragment {
}
}
private void startImportingKeys() {
boolean isEncrypted;
try {
isEncrypted = FileHelper.isEncryptedFile(mImportActivity, mCurrentUri);
} catch (IOException e) {
Log.e(Constants.TAG, "Error opening file", e);
Notify.create(mImportActivity, R.string.error_bad_data, Style.ERROR).show();
return;
}
if (isEncrypted) {
Intent intent = new Intent(mImportActivity, DecryptActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.setData(mCurrentUri);
startActivity(intent);
} else {
mImportActivity.loadCallback(new BytesLoaderState(null, mCurrentUri));
}
}
/**
* Request READ_EXTERNAL_STORAGE permission on Android >= 6.0 to read content from "file" Uris.
* <p/>
* This method returns true on Android < 6, or if permission is already granted. It
* requests the permission and returns false otherwise.
* <p/>
* see https://commonsware.com/blog/2015/10/07/runtime-permissions-files-action-send.html
*/
private boolean checkAndRequestReadPermission(final Uri uri) {
if (!ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
return true;
}
// Additional check due to https://commonsware.com/blog/2015/11/09/you-cannot-hold-nonexistent-permissions.html
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return true;
}
if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
return true;
}
requestPermissions(
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_PERMISSION_READ_EXTERNAL_STORAGE);
return false;
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode != REQUEST_PERMISSION_READ_EXTERNAL_STORAGE) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
return;
}
boolean permissionWasGranted = grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (permissionWasGranted) {
startImportingKeys();
} else {
Toast.makeText(getActivity(), R.string.error_denied_storage_permission, Toast.LENGTH_LONG).show();
getActivity().setResult(Activity.RESULT_CANCELED);
getActivity().finish();
}
}
}

View File

@@ -330,9 +330,16 @@ public class ImportKeysListFragment extends ListFragment implements
}
public void loadNew(LoaderState loaderState) {
mLoaderState = loaderState;
if (mLoaderState instanceof BytesLoaderState) {
BytesLoaderState ls = (BytesLoaderState) mLoaderState;
if ( ls.mDataUri != null && ! checkAndRequestReadPermission(ls.mDataUri)) {
return;
}
}
restartLoaders();
}

View File

@@ -40,11 +40,11 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult;
import org.sufficientlysecure.keychain.remote.ui.AppsListFragment;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.util.FabContainer;
import org.sufficientlysecure.keychain.util.Preferences;
public class MainActivity extends BaseSecurityTokenNfcActivity implements FabContainer, OnBackStackChangedListener {
public class MainActivity extends BaseSecurityTokenActivity implements FabContainer, OnBackStackChangedListener {
static final int ID_KEYS = 1;
static final int ID_ENCRYPT_DECRYPT = 2;
@@ -90,8 +90,9 @@ public class MainActivity extends BaseSecurityTokenNfcActivity implements FabCon
@Override
public boolean onItemClick(View view, int position, IDrawerItem drawerItem) {
if (drawerItem != null) {
PrimaryDrawerItem item = (PrimaryDrawerItem) drawerItem;
Intent intent = null;
switch (drawerItem.getIdentifier()) {
switch ((int) item.getIdentifier()) {
case ID_KEYS:
onKeysSelected();
break;

View File

@@ -0,0 +1,223 @@
package org.sufficientlysecure.keychain.ui;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.KeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainDatabase;
import org.sufficientlysecure.keychain.service.CertifyActionsParcel;
import org.sufficientlysecure.keychain.ui.adapter.MultiUserIdsAdapter;
import org.sufficientlysecure.keychain.util.Log;
import java.util.ArrayList;
public class MultiUserIdsFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor>{
public static final String ARG_CHECK_STATES = "check_states";
public static final String EXTRA_KEY_IDS = "extra_key_ids";
private boolean checkboxVisibility = true;
ListView mUserIds;
private MultiUserIdsAdapter mUserIdsAdapter;
private long[] mPubMasterKeyIds;
public static final String[] USER_IDS_PROJECTION = new String[]{
KeychainContract.UserPackets._ID,
KeychainContract.UserPackets.MASTER_KEY_ID,
KeychainContract.UserPackets.USER_ID,
KeychainContract.UserPackets.IS_PRIMARY,
KeychainContract.UserPackets.IS_REVOKED
};
private static final int INDEX_MASTER_KEY_ID = 1;
private static final int INDEX_USER_ID = 2;
@SuppressWarnings("unused")
private static final int INDEX_IS_PRIMARY = 3;
@SuppressWarnings("unused")
private static final int INDEX_IS_REVOKED = 4;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.multi_user_ids_fragment, null);
mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mPubMasterKeyIds = getActivity().getIntent().getLongArrayExtra(EXTRA_KEY_IDS);
if (mPubMasterKeyIds == null) {
Log.e(Constants.TAG, "List of key ids to certify missing!");
getActivity().finish();
return;
}
ArrayList<Boolean> checkedStates = null;
if (savedInstanceState != null) {
checkedStates = (ArrayList<Boolean>) savedInstanceState.getSerializable(ARG_CHECK_STATES);
}
mUserIdsAdapter = new MultiUserIdsAdapter(getActivity(), null, 0, checkedStates, checkboxVisibility);
mUserIds.setAdapter(mUserIdsAdapter);
mUserIds.setDividerHeight(0);
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ArrayList<Boolean> states = mUserIdsAdapter.getCheckStates();
// no proper parceling method available :(
outState.putSerializable(ARG_CHECK_STATES, states);
}
public ArrayList<CertifyActionsParcel.CertifyAction> getSelectedCertifyActions() {
if (!checkboxVisibility) {
throw new AssertionError("Item selection not allowed");
}
return mUserIdsAdapter.getSelectedCertifyActions();
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = KeychainContract.UserPackets.buildUserIdsUri();
String selection, ids[];
{
// generate placeholders and string selection args
ids = new String[mPubMasterKeyIds.length];
StringBuilder placeholders = new StringBuilder("?");
for (int i = 0; i < mPubMasterKeyIds.length; i++) {
ids[i] = Long.toString(mPubMasterKeyIds[i]);
if (i != 0) {
placeholders.append(",?");
}
}
// put together selection string
selection = KeychainContract.UserPackets.IS_REVOKED + " = 0" + " AND "
+ KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID
+ " IN (" + placeholders + ")";
}
return new CursorLoader(getActivity(), uri,
USER_IDS_PROJECTION, selection, ids,
KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.MASTER_KEY_ID + " ASC"
+ ", " + KeychainDatabase.Tables.USER_PACKETS + "." + KeychainContract.UserPackets.USER_ID + " ASC"
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
MatrixCursor matrix = new MatrixCursor(new String[]{
"_id", "user_data", "grouped"
}) {
@Override
public byte[] getBlob(int column) {
return super.getBlob(column);
}
};
data.moveToFirst();
long lastMasterKeyId = 0;
String lastName = "";
ArrayList<String> uids = new ArrayList<>();
boolean header = true;
// Iterate over all rows
while (!data.isAfterLast()) {
long masterKeyId = data.getLong(INDEX_MASTER_KEY_ID);
String userId = data.getString(INDEX_USER_ID);
KeyRing.UserId pieces = KeyRing.splitUserId(userId);
// Two cases:
boolean grouped = masterKeyId == lastMasterKeyId;
boolean subGrouped = data.isFirst() || grouped && lastName.equals(pieces.name);
// Remember for next loop
lastName = pieces.name;
Log.d(Constants.TAG, Long.toString(masterKeyId, 16) + (grouped ? "grouped" : "not grouped"));
if (!subGrouped) {
// 1. This name should NOT be grouped with the previous, so we flush the buffer
Parcel p = Parcel.obtain();
p.writeStringList(uids);
byte[] d = p.marshall();
p.recycle();
matrix.addRow(new Object[]{
lastMasterKeyId, d, header ? 1 : 0
});
// indicate that we have a header for this masterKeyId
header = false;
// Now clear the buffer, and add the new user id, for the next round
uids.clear();
}
// 2. This name should be grouped with the previous, just add to buffer
uids.add(userId);
lastMasterKeyId = masterKeyId;
// If this one wasn't grouped, the next one's gotta be a header
if (!grouped) {
header = true;
}
// Regardless of the outcome, move to next entry
data.moveToNext();
}
// If there is anything left in the buffer, flush it one last time
if (!uids.isEmpty()) {
Parcel p = Parcel.obtain();
p.writeStringList(uids);
byte[] d = p.marshall();
p.recycle();
matrix.addRow(new Object[]{
lastMasterKeyId, d, header ? 1 : 0
});
}
mUserIdsAdapter.swapCursor(matrix);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mUserIdsAdapter.swapCursor(null);
}
public void setCheckboxVisibility(boolean checkboxVisibility) {
this.checkboxVisibility = checkboxVisibility;
}
}

View File

@@ -46,8 +46,6 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.ViewAnimator;
import com.github.pinball83.maskededittext.MaskedEditText;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
@@ -60,7 +58,6 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.remote.CryptoInputParcelCacheService;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
@@ -157,7 +154,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {
private EditText mPassphraseEditText;
private TextView mPassphraseText;
private MaskedEditText mBackupCodeEditText;
private EditText[] mBackupCodeEditText;
private boolean mIsCancelled = false;
private RequiredInputParcel mRequiredInput;
@@ -184,13 +181,15 @@ public class PassphraseDialogActivity extends FragmentActivity {
View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null);
alert.setView(view);
mBackupCodeEditText = (MaskedEditText) view.findViewById(R.id.backup_code);
// NOTE: order of these method calls matter, see setupAutomaticLinebreak()
mBackupCodeEditText.setInputType(
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
setupAutomaticLinebreak(mBackupCodeEditText);
mBackupCodeEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mBackupCodeEditText.setOnEditorActionListener(this);
mBackupCodeEditText = new EditText[6];
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);
mBackupCodeEditText[4] = (EditText) view.findViewById(R.id.backup_code_5);
mBackupCodeEditText[5] = (EditText) view.findViewById(R.id.backup_code_6);
setupEditTextFocusNext(mBackupCodeEditText);
AlertDialog dialog = alert.create();
dialog.setButton(DialogInterface.BUTTON_POSITIVE,
@@ -281,28 +280,7 @@ public class PassphraseDialogActivity extends FragmentActivity {
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();
openKeyboard(mPassphraseEditText);
mPassphraseEditText.setImeActionLabel(getString(android.R.string.ok), EditorInfo.IME_ACTION_DONE);
mPassphraseEditText.setOnEditorActionListener(this);
@@ -325,17 +303,62 @@ public class PassphraseDialogActivity extends FragmentActivity {
}
/**
* Automatic line break with max 6 lines for smaller displays
* <p/>
* NOTE: I was not able to get this behaviour using XML!
* Looks like the order of these method calls matter, see http://stackoverflow.com/a/11171307
* 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
*/
private void setupAutomaticLinebreak(TextView textview) {
textview.setSingleLine(true);
textview.setMaxLines(6);
textview.setHorizontallyScrolling(false);
private void openKeyboard(final TextView textView) {
textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
textView.post(new Runnable() {
@Override
public void run() {
if (getActivity() == null || textView == null) {
return;
}
InputMethodManager imm = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(textView, InputMethodManager.SHOW_IMPLICIT);
}
});
}
});
textView.requestFocus();
}
private static void setupEditTextFocusNext(final EditText[] backupCodes) {
for (int i = 0; i < backupCodes.length - 1; i++) {
final int next = i + 1;
backupCodes[i].addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean inserting = before < count;
boolean cursorAtEnd = (start + count) == 4;
if (inserting && cursorAtEnd) {
backupCodes[next].requestFocus();
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
}
@Override
public void onStart() {
super.onStart();
@@ -347,8 +370,17 @@ public class PassphraseDialogActivity extends FragmentActivity {
public void onClick(View v) {
if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {
Passphrase passphrase =
new Passphrase(mBackupCodeEditText.getText().toString());
StringBuilder backupCodeInput = new StringBuilder(26);
for (EditText editText : mBackupCodeEditText) {
if (editText.getText().length() < 4) {
return;
}
backupCodeInput.append(editText.getText());
backupCodeInput.append('-');
}
backupCodeInput.deleteCharAt(backupCodeInput.length() - 1);
Passphrase passphrase = new Passphrase(backupCodeInput.toString());
finishCaching(passphrase);
return;

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 Dominik Schürmann <dominik@dominikschuermann.de>
*
* 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.ui;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
public class RedirectImportKeysActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
startQrCodeCaptureActivity();
}
private void startQrCodeCaptureActivity() {
final Intent scanQrCode = new Intent(this, ImportKeysProxyActivity.class);
scanQrCode.setAction(ImportKeysProxyActivity.ACTION_QR_CODE_API);
new AlertDialog.Builder(this)
.setTitle(R.string.redirect_import_key_title)
.setMessage(R.string.redirect_import_key_message)
.setPositiveButton(R.string.redirect_import_key_yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// directly scan with OpenKeychain
startActivity(scanQrCode);
finish();
}
})
.setNegativeButton(R.string.redirect_import_key_no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// close window
finish();
}
})
.show();
}
}

View File

@@ -3,6 +3,7 @@
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* 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
@@ -35,10 +36,12 @@ import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey;
import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.securitytoken.KeyType;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.ThemeChanger;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OrientationUtils;
@@ -55,7 +58,7 @@ import nordpol.android.NfcGuideView;
* NFC devices.
* For the full specs, see http://g10code.com/docs/openpgp-card-2.0.pdf
*/
public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity {
public class SecurityTokenOperationActivity extends BaseSecurityTokenActivity {
public static final String EXTRA_REQUIRED_INPUT = "required_input";
public static final String EXTRA_CRYPTO_INPUT = "crypto_input";
@@ -69,8 +72,6 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private RequiredInputParcel mRequiredInput;
private static final byte[] BLANK_FINGERPRINT = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
private CryptoInputParcel mInputParcel;
@Override
@@ -137,9 +138,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
private void obtainPassphraseIfRequired() {
// obtain passphrase for this subkey
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_MOVE_KEY_TO_CARD
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.NFC_RESET_CARD) {
if (mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_MOVE_KEY_TO_CARD
&& mRequiredInput.mType != RequiredInputParcel.RequiredInputType.SECURITY_TOKEN_RESET_CARD) {
obtainSecurityTokenPin(mRequiredInput);
checkPinAvailability();
} else {
checkDeviceConnection();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (REQUEST_CODE_PIN == requestCode) {
checkPinAvailability();
}
}
private void checkPinAvailability() {
try {
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
if (passphrase != null) {
checkDeviceConnection();
}
} catch (PassphraseCacheService.KeyNotFoundException e) {
throw new AssertionError(
"tried to find passphrase for non-existing key. this is a programming error!");
}
}
@@ -149,39 +174,53 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
public void onNfcPreExecute() {
public void onSecurityTokenPreExecute() {
// start with indeterminate progress
vAnimator.setDisplayedChild(1);
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.TRANSFERRING);
}
@Override
protected void doNfcInBackground() throws IOException {
protected void doSecurityTokenInBackground() throws IOException {
switch (mRequiredInput.mType) {
case NFC_DECRYPT: {
case SECURITY_TOKEN_DECRYPT: {
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
mSecurityTokenHelper.getKeyFingerprint(KeyType.ENCRYPT));
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
throw new IOException(getString(R.string.error_wrong_security_token));
}
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] encryptedSessionKey = mRequiredInput.mInputData[i];
byte[] decryptedSessionKey = nfcDecryptSessionKey(encryptedSessionKey);
byte[] decryptedSessionKey = mSecurityTokenHelper.decryptSessionKey(encryptedSessionKey);
mInputParcel.addCryptoData(encryptedSessionKey, decryptedSessionKey);
}
break;
}
case NFC_SIGN: {
case SECURITY_TOKEN_SIGN: {
long tokenKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(
mSecurityTokenHelper.getKeyFingerprint(KeyType.SIGN));
if (tokenKeyId != mRequiredInput.getSubKeyId()) {
throw new IOException(getString(R.string.error_wrong_security_token));
}
mInputParcel.addSignatureTime(mRequiredInput.mSignatureTime);
for (int i = 0; i < mRequiredInput.mInputData.length; i++) {
byte[] hash = mRequiredInput.mInputData[i];
int algo = mRequiredInput.mSignAlgos[i];
byte[] signedHash = nfcCalculateSignature(hash, algo);
byte[] signedHash = mSecurityTokenHelper.calculateSignature(hash, algo);
mInputParcel.addCryptoData(hash, signedHash);
}
break;
}
case NFC_MOVE_KEY_TO_CARD: {
case SECURITY_TOKEN_MOVE_KEY_TO_CARD: {
// TODO: assume PIN and Admin PIN to be default for this operation
mPin = new Passphrase("123456");
mAdminPin = new Passphrase("12345678");
mSecurityTokenHelper.setPin(new Passphrase("123456"));
mSecurityTokenHelper.setAdminPin(new Passphrase("12345678"));
ProviderHelper providerHelper = new ProviderHelper(this);
CanonicalizedSecretKeyRing secretKeyRing;
@@ -202,11 +241,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
long subkeyId = buf.getLong();
CanonicalizedSecretKey key = secretKeyRing.getSecretKey(subkeyId);
long keyGenerationTimestampMillis = key.getCreationTime().getTime();
long keyGenerationTimestamp = keyGenerationTimestampMillis / 1000;
byte[] timestampBytes = ByteBuffer.allocate(4).putInt((int) keyGenerationTimestamp).array();
byte[] tokenSerialNumber = Arrays.copyOf(nfcGetAid(), 16);
byte[] tokenSerialNumber = Arrays.copyOf(mSecurityTokenHelper.getAid(), 16);
Passphrase passphrase;
try {
@@ -216,46 +251,20 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
throw new IOException("Unable to get cached passphrase!");
}
if (key.canSign() || key.canCertify()) {
if (shouldPutKey(key.getFingerprint(), 0)) {
nfcPutKey(0xB6, key, passphrase);
nfcPutData(0xCE, timestampBytes);
nfcPutData(0xC7, key.getFingerprint());
} else {
throw new IOException("Key slot occupied; token must be reset to put new signature key.");
}
} else if (key.canEncrypt()) {
if (shouldPutKey(key.getFingerprint(), 1)) {
nfcPutKey(0xB8, key, passphrase);
nfcPutData(0xCF, timestampBytes);
nfcPutData(0xC8, key.getFingerprint());
} else {
throw new IOException("Key slot occupied; token must be reset to put new decryption key.");
}
} else if (key.canAuthenticate()) {
if (shouldPutKey(key.getFingerprint(), 2)) {
nfcPutKey(0xA4, key, passphrase);
nfcPutData(0xD0, timestampBytes);
nfcPutData(0xC9, key.getFingerprint());
} else {
throw new IOException("Key slot occupied; token must be reset to put new authentication key.");
}
} else {
throw new IOException("Inappropriate key flags for Security Token key.");
}
mSecurityTokenHelper.changeKey(key, passphrase);
// TODO: Is this really used anywhere?
mInputParcel.addCryptoData(subkeyBytes, tokenSerialNumber);
}
// change PINs afterwards
nfcModifyPIN(0x81, newPin);
nfcModifyPIN(0x83, newAdminPin);
mSecurityTokenHelper.modifyPin(0x81, newPin);
mSecurityTokenHelper.modifyPin(0x83, newAdminPin);
break;
}
case NFC_RESET_CARD: {
nfcResetCard();
case SECURITY_TOKEN_RESET_CARD: {
mSecurityTokenHelper.resetAndWipeToken();
break;
}
@@ -267,7 +276,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
protected final void onNfcPostExecute() {
protected final void onSecurityTokenPostExecute() {
handleResult(mInputParcel);
// show finish
@@ -275,28 +284,33 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
nfcGuideView.setCurrentStatus(NfcGuideView.NfcGuideViewStatus.DONE);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// check all 200ms if Security Token has been taken away
while (true) {
if (isNfcConnected()) {
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
if (mSecurityTokenHelper.isPersistentConnectionAllowed()) {
// Just close
finish();
} else {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
// check all 200ms if Security Token has been taken away
while (true) {
if (isSecurityTokenConnected()) {
try {
Thread.sleep(200);
} catch (InterruptedException ignored) {
}
} else {
return null;
}
} else {
return null;
}
}
}
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
finish();
}
}.execute();
@Override
protected void onPostExecute(Void result) {
super.onPostExecute(result);
finish();
}
}.execute();
}
}
/**
@@ -311,7 +325,7 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
protected void onNfcError(String error) {
protected void onSecurityTokenError(String error) {
pauseTagHandling();
vErrorText.setText(error + "\n\n" + getString(R.string.security_token_nfc_try_again_text));
@@ -321,31 +335,11 @@ public class SecurityTokenOperationActivity extends BaseSecurityTokenNfcActivity
}
@Override
public void onNfcPinError(String error) {
onNfcError(error);
public void onSecurityTokenPinError(String error) {
onSecurityTokenError(error);
// clear (invalid) passphrase
PassphraseCacheService.clearCachedPassphrase(
this, mRequiredInput.getMasterKeyId(), mRequiredInput.getSubKeyId());
}
private boolean shouldPutKey(byte[] fingerprint, int idx) throws IOException {
byte[] tokenFingerprint = nfcGetMasterKeyFingerprint(idx);
// Note: special case: This should not happen, but happens with
// https://github.com/FluffyKaon/OpenPGP-Card, thus for now assume true
if (tokenFingerprint == null) {
return true;
}
// Slot is empty, or contains this key already. PUT KEY operation is safe
if (Arrays.equals(tokenFingerprint, BLANK_FINGERPRINT) ||
Arrays.equals(tokenFingerprint, fingerprint)) {
return true;
}
// Slot already contains a different key; don't overwrite it.
return false;
}
}

View File

@@ -19,8 +19,6 @@
package org.sufficientlysecure.keychain.ui;
import java.util.List;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
@@ -49,6 +47,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.KeychainApplication;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;
import org.sufficientlysecure.keychain.service.ContactSyncAdapterService;
@@ -59,6 +58,8 @@ import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
import java.util.List;
public class SettingsActivity extends AppCompatPreferenceActivity {
public static final int REQUEST_CODE_KEYSERVER_PREF = 0x00007005;
@@ -405,7 +406,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
}
/**
* This fragment shows the keyserver/contacts sync preferences
* This fragment shows the keyserver/wifi-only-sync/contacts sync preferences
*/
public static class SyncPrefsFragment extends PresetPreferenceFragment {
@@ -422,8 +423,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
super.onResume();
// this needs to be done in onResume since the user can change sync values from Android
// settings and we need to reflect that change when the user navigates back
AccountManager manager = AccountManager.get(getActivity());
final Account account = manager.getAccountsByType(Constants.ACCOUNT_TYPE)[0];
final Account account = KeychainApplication.createAccountIfNecessary(getActivity());
// for keyserver sync
initializeSyncCheckBox(
(SwitchPreference) findPreference(Constants.Pref.SYNC_KEYSERVER),
@@ -441,8 +441,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
private void initializeSyncCheckBox(final SwitchPreference syncCheckBox,
final Account account,
final String authority) {
boolean syncEnabled = ContentResolver.getSyncAutomatically(account, authority)
&& checkContactsPermission(authority);
// account is null if it could not be created for some reason
boolean syncEnabled =
account != null
&& ContentResolver.getSyncAutomatically(account, authority)
&& checkContactsPermission(authority);
syncCheckBox.setChecked(syncEnabled);
setSummary(syncCheckBox, authority, syncEnabled);
@@ -464,6 +467,11 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
return false;
}
} else {
if (account == null) {
// if account could not be created for some reason,
// we can't have our sync
return false;
}
// disable syncs
ContentResolver.setSyncAutomatically(account, authority, false);
// immediately delete any linked contacts

View File

@@ -40,6 +40,7 @@ import android.widget.TextView;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.ui.dialog.AddEditKeyserverDialogFragment;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperAdapter;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperViewHolder;
import org.sufficientlysecure.keychain.ui.util.recyclerview.ItemTouchHelperDragCallback;
@@ -312,19 +313,19 @@ public class SettingsKeyserverFragment extends Fragment implements RecyclerItemC
public void showAsSelectedKeyserver() {
isSelectedKeyserver = true;
selectedServerLabel.setVisibility(View.VISIBLE);
outerLayout.setBackgroundColor(getResources().getColor(R.color.android_green_dark));
outerLayout.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorPrimaryDark));
}
public void showAsUnselectedKeyserver() {
isSelectedKeyserver = false;
selectedServerLabel.setVisibility(View.GONE);
outerLayout.setBackgroundColor(Color.WHITE);
outerLayout.setBackgroundColor(0);
}
@Override
public void onItemSelected() {
selectedServerLabel.setVisibility(View.GONE);
itemView.setBackgroundColor(Color.LTGRAY);
itemView.setBackgroundColor(FormattingUtils.getColorFromAttr(getContext(), R.attr.colorBrightToolbar));
}
@Override

View File

@@ -31,10 +31,7 @@ import android.widget.Spinner;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.UploadResult;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.UploadKeyringParcel;
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
@@ -53,7 +50,6 @@ public class UploadKeyActivity extends BaseActivity
// CryptoOperationHelper.Callback vars
private String mKeyserver;
private long mMasterKeyId;
private CryptoOperationHelper<UploadKeyringParcel, UploadResult> mUploadOpHelper;
@Override
@@ -63,6 +59,10 @@ public class UploadKeyActivity extends BaseActivity
mUploadButton = findViewById(R.id.upload_key_action_upload);
mKeyServerSpinner = (Spinner) findViewById(R.id.upload_key_keyserver);
MultiUserIdsFragment mMultiUserIdsFragment = (MultiUserIdsFragment)
getSupportFragmentManager().findFragmentById(R.id.multi_user_ids_fragment);
mMultiUserIdsFragment.setCheckboxVisibility(false);
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, Preferences.getPreferences(this)
.getKeyServers()
@@ -89,15 +89,6 @@ public class UploadKeyActivity extends BaseActivity
return;
}
try {
mMasterKeyId = new ProviderHelper(this).getCachedPublicKeyRing(
KeyRings.buildUnifiedKeyRingUri(mDataUri)).getMasterKeyId();
} catch (PgpKeyNotFoundException e) {
Log.e(Constants.TAG, "Intent data pointed to bad key!");
finish();
return;
}
}
@Override
@@ -136,7 +127,9 @@ public class UploadKeyActivity extends BaseActivity
@Override
public UploadKeyringParcel createOperationInput() {
return new UploadKeyringParcel(mKeyserver, mMasterKeyId);
long[] masterKeyIds = getIntent().getLongArrayExtra(MultiUserIdsFragment.EXTRA_KEY_IDS);
return new UploadKeyringParcel(mKeyserver, masterKeyIds[0]);
}
@Override

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* 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.ui;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Bundle;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.util.Log;
public class UsbEventReceiverActivity extends Activity {
public static final String ACTION_USB_PERMISSION =
"org.sufficientlysecure.keychain.ui.USB_PERMISSION";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
final UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
Intent intent = getIntent();
if (intent != null) {
if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
Log.d(Constants.TAG, "Requesting permission for " + usbDevice.getDeviceName());
usbManager.requestPermission(usbDevice,
PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0));
}
}
// Close the activity
finish();
}
}

View File

@@ -32,6 +32,7 @@ import android.app.ActivityOptions;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.nfc.NfcAdapter;
import android.os.AsyncTask;
@@ -80,11 +81,11 @@ import org.sufficientlysecure.keychain.provider.KeychainContract;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;
import org.sufficientlysecure.keychain.service.ChangeUnlockParcel;
import org.sufficientlysecure.keychain.service.ImportKeyringParcel;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.ui.ViewKeyFragment.PostponeType;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenNfcActivity;
import org.sufficientlysecure.keychain.ui.base.BaseSecurityTokenActivity;
import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper;
import org.sufficientlysecure.keychain.ui.dialog.SetPassphraseDialogFragment;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
@@ -102,7 +103,7 @@ import org.sufficientlysecure.keychain.util.Passphrase;
import org.sufficientlysecure.keychain.util.Preferences;
public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
public class ViewKeyActivity extends BaseSecurityTokenActivity implements
LoaderManager.LoaderCallbacks<Cursor>,
CryptoOperationHelper.Callback<ImportKeyringParcel, ImportKeyResult> {
@@ -129,8 +130,8 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private String mKeyserver;
private ArrayList<ParcelableKeyRing> mKeyList;
private CryptoOperationHelper<ImportKeyringParcel, ImportKeyResult> mImportOpHelper;
private CryptoOperationHelper<SaveKeyringParcel, EditKeyResult> mEditOpHelper;
private SaveKeyringParcel mSaveKeyringParcel;
private CryptoOperationHelper<ChangeUnlockParcel, EditKeyResult> mEditOpHelper;
private ChangeUnlockParcel mChangeUnlockParcel;
private TextView mStatusText;
private ImageView mStatusImage;
@@ -170,9 +171,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
private byte[] mFingerprint;
private String mFingerprintString;
private byte[] mNfcFingerprints;
private String mNfcUserId;
private byte[] mNfcAid;
private byte[] mSecurityTokenFingerprints;
private String mSecurityTokenUserId;
private byte[] mSecurityTokenAid;
@SuppressLint("InflateParams")
@Override
@@ -428,13 +429,11 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
private void changePassword() {
mSaveKeyringParcel = new SaveKeyringParcel(mMasterKeyId, mFingerprint);
CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult> editKeyCallback
= new CryptoOperationHelper.Callback<SaveKeyringParcel, EditKeyResult>() {
CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult> editKeyCallback
= new CryptoOperationHelper.Callback<ChangeUnlockParcel, EditKeyResult>() {
@Override
public SaveKeyringParcel createOperationInput() {
return mSaveKeyringParcel;
public ChangeUnlockParcel createOperationInput() {
return mChangeUnlockParcel;
}
@Override
@@ -468,9 +467,10 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Bundle data = message.getData();
// use new passphrase!
mSaveKeyringParcel.mNewUnlock = new SaveKeyringParcel.ChangeUnlockParcel(
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE),
null
mChangeUnlockParcel = new ChangeUnlockParcel(
mMasterKeyId,
mFingerprint,
(Passphrase) data.getParcelable(SetPassphraseDialogFragment.MESSAGE_NEW_PASSPHRASE)
);
mEditOpHelper.cryptoOperation();
@@ -646,17 +646,17 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
@Override
protected void doNfcInBackground() throws IOException {
protected void doSecurityTokenInBackground() throws IOException {
mNfcFingerprints = nfcGetFingerprints();
mNfcUserId = nfcGetUserId();
mNfcAid = nfcGetAid();
mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
mSecurityTokenAid = mSecurityTokenHelper.getAid();
}
@Override
protected void onNfcPostExecute() {
protected void onSecurityTokenPostExecute() {
long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mNfcFingerprints);
long tokenId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try {
@@ -667,7 +667,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
// if the master key of that key matches this one, just show the token dialog
if (KeyFormattingUtils.convertFingerprintToHex(candidateFp).equals(mFingerprintString)) {
showSecurityTokenFragment(mNfcFingerprints, mNfcUserId, mNfcAid);
showSecurityTokenFragment(mSecurityTokenFingerprints, mSecurityTokenUserId, mSecurityTokenAid);
return;
}
@@ -680,9 +680,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
Intent intent = new Intent(
ViewKeyActivity.this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
@@ -695,9 +695,9 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
public void onAction() {
Intent intent = new Intent(
ViewKeyActivity.this, CreateKeyActivity.class);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mNfcAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mNfcUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mNfcFingerprints);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
finish();
}
@@ -924,6 +924,7 @@ public class ViewKeyActivity extends BaseSecurityTokenNfcActivity implements
}
mPhoto.setImageBitmap(photo);
mPhoto.setColorFilter(getResources().getColor(R.color.toolbar_photo_tint), PorterDuff.Mode.SRC_ATOP);
mPhotoLayout.setVisibility(View.VISIBLE);
}
};

View File

@@ -238,7 +238,7 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
// let user choose application
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.setType("text/plain");
sendIntent.setType(Constants.MIME_TYPE_KEYS);
// NOTE: Don't use Intent.EXTRA_TEXT to send the key
// better send it via a Uri!
@@ -455,8 +455,19 @@ public class ViewKeyAdvShareFragment extends LoaderFragment implements
}
private void uploadToKeyserver() {
long keyId;
try {
keyId = new ProviderHelper(getActivity())
.getCachedPublicKeyRing(mDataUri)
.extractOrGetMasterKeyId();
} catch (PgpKeyNotFoundException e) {
Log.e(Constants.TAG, "key not found!", e);
Notify.create(getActivity(), "key not found", Style.ERROR).show();
return;
}
Intent uploadIntent = new Intent(getActivity(), UploadKeyActivity.class);
uploadIntent.setData(mDataUri);
uploadIntent.putExtra(MultiUserIdsFragment.EXTRA_KEY_IDS, new long[]{keyId});
startActivityForResult(uploadIntent, 0);
}

View File

@@ -39,6 +39,7 @@ import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ViewAnimator;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.compatibility.DialogFragmentWorkaround;
@@ -346,50 +347,45 @@ public class ViewKeyAdvSubkeysFragment extends LoaderFragment implements
}
break;
}
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_CARD: {
// TODO: enable later when Admin PIN handling is resolved
Notify.create(getActivity(),
"This feature will be available in an upcoming OpenKeychain version.",
Notify.Style.WARN).show();
break;
case EditSubkeyDialogFragment.MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN: {
SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
secretKeyType == SecretKeyType.GNU_DUMMY) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_stripped, Notify.Style.ERROR)
.show();
break;
}
// Activity activity = EditKeyFragment.this.getActivity();
// SecretKeyType secretKeyType = mSubkeysAdapter.getSecretKeyType(position);
// if (secretKeyType == SecretKeyType.DIVERT_TO_CARD ||
// secretKeyType == SecretKeyType.GNU_DUMMY) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_stripped, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
// int algorithm = mSubkeysAdapter.getAlgorithm(position);
// // these are the PGP constants for RSA_GENERAL, RSA_ENCRYPT and RSA_SIGN
// if (algorithm != 1 && algorithm != 2 && algorithm != 3) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_algo, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
// if (mSubkeysAdapter.getKeySize(position) != 2048) {
// Notify.create(activity, R.string.edit_key_error_bad_nfc_size, Notify.Style.ERROR)
// .show((ViewGroup) activity.findViewById(R.id.import_snackbar));
// break;
// }
//
//
// SubkeyChange change;
// change = mSaveKeyringParcel.getSubkeyChange(keyId);
// if (change == null) {
// mSaveKeyringParcel.mChangeSubKeys.add(
// new SubkeyChange(keyId, false, true)
// );
// break;
// }
// // toggle
// change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
// if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// // User had chosen to strip key, but now wants to divert it.
// change.mDummyStrip = false;
// }
// break;
int algorithm = mSubkeysAdapter.getAlgorithm(position);
if (algorithm != PublicKeyAlgorithmTags.RSA_GENERAL
&& algorithm != PublicKeyAlgorithmTags.RSA_ENCRYPT
&& algorithm != PublicKeyAlgorithmTags.RSA_SIGN) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_algo, Notify.Style.ERROR)
.show();
break;
}
if (mSubkeysAdapter.getKeySize(position) != 2048) {
Notify.create(getActivity(), R.string.edit_key_error_bad_security_token_size, Notify.Style.ERROR)
.show();
break;
}
SubkeyChange change;
change = mEditModeSaveKeyringParcel.getSubkeyChange(keyId);
if (change == null) {
mEditModeSaveKeyringParcel.mChangeSubKeys.add(
new SubkeyChange(keyId, false, true)
);
break;
}
// toggle
change.mMoveKeyToSecurityToken = !change.mMoveKeyToSecurityToken;
if (change.mMoveKeyToSecurityToken && change.mDummyStrip) {
// User had chosen to strip key, but now wants to divert it.
change.mDummyStrip = false;
}
break;
}
}
getLoaderManager().getLoader(LOADER_ID_SUBKEYS).forceLoad();

View File

@@ -141,6 +141,7 @@ public class ImportKeysListLoader
OperationResult.OperationLog log = new OperationResult.OperationLog();
log.add(OperationResult.LogType.MSG_GET_NO_VALID_KEYS, 0);
GetKeyResult getKeyResult = new GetKeyResult(GetKeyResult.RESULT_ERROR_NO_VALID_KEYS, log);
mData.clear();
mEntryListWrapper = new AsyncTaskResultWrapper<>(mData, getKeyResult);
}
}

View File

@@ -66,6 +66,7 @@ public class KeyAdapter extends CursorAdapter {
KeyRings.HAS_DUPLICATE_USER_ID,
KeyRings.FINGERPRINT,
KeyRings.CREATION,
KeyRings.HAS_ENCRYPT
};
public static final int INDEX_MASTER_KEY_ID = 1;
@@ -77,6 +78,7 @@ public class KeyAdapter extends CursorAdapter {
public static final int INDEX_HAS_DUPLICATE_USER_ID = 7;
public static final int INDEX_FINGERPRINT = 8;
public static final int INDEX_CREATION = 9;
public static final int INDEX_HAS_ENCRYPT = 10;
public KeyAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
@@ -289,6 +291,7 @@ public class KeyAdapter extends CursorAdapter {
public final KeyRing.UserId mUserId;
public final long mKeyId;
public final boolean mHasDuplicate;
public final boolean mHasEncrypt;
public final Date mCreation;
public final String mFingerprint;
public final boolean mIsSecret, mIsRevoked, mIsExpired, mIsVerified;
@@ -299,6 +302,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId;
mKeyId = cursor.getLong(INDEX_MASTER_KEY_ID);
mHasDuplicate = cursor.getLong(INDEX_HAS_DUPLICATE_USER_ID) > 0;
mHasEncrypt = cursor.getInt(INDEX_HAS_ENCRYPT) != 0;
mCreation = new Date(cursor.getLong(INDEX_CREATION) * 1000);
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
cursor.getBlob(INDEX_FINGERPRINT));
@@ -315,6 +319,7 @@ public class KeyAdapter extends CursorAdapter {
mUserIdFull = userId;
mKeyId = ring.getMasterKeyId();
mHasDuplicate = false;
mHasEncrypt = key.getKeyRing().getEncryptIds().size() > 0;
mCreation = key.getCreationTime();
mFingerprint = KeyFormattingUtils.convertFingerprintToHex(
ring.getFingerprint());
@@ -333,14 +338,6 @@ public class KeyAdapter extends CursorAdapter {
return mUserId.email;
}
}
// TODO: workaround for bug in TokenAutoComplete,
// see https://github.com/open-keychain/open-keychain/issues/1636
@Override
public String toString() {
return " ";
}
}
public static String[] getProjectionWith(String[] projection) {

View File

@@ -39,6 +39,7 @@ import java.util.ArrayList;
public class MultiUserIdsAdapter extends CursorAdapter {
private LayoutInflater mInflater;
private final ArrayList<Boolean> mCheckStates;
private boolean checkboxVisibility = true;
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates) {
super(context, c, flags);
@@ -46,6 +47,11 @@ public class MultiUserIdsAdapter extends CursorAdapter {
mCheckStates = preselectStates == null ? new ArrayList<Boolean>() : preselectStates;
}
public MultiUserIdsAdapter(Context context, Cursor c, int flags, ArrayList<Boolean> preselectStates, boolean checkboxVisibility) {
this(context,c,flags,preselectStates);
this.checkboxVisibility = checkboxVisibility;
}
@Override
public Cursor swapCursor(Cursor newCursor) {
if (newCursor != null) {
@@ -138,6 +144,7 @@ public class MultiUserIdsAdapter extends CursorAdapter {
}
});
vCheckBox.setClickable(false);
vCheckBox.setVisibility(checkboxVisibility?View.VISIBLE:View.GONE);
View vUidBody = view.findViewById(R.id.user_id_body);
vUidBody.setClickable(true);

View File

@@ -20,6 +20,7 @@ package org.sufficientlysecure.keychain.ui.adapter;
import android.app.Activity;
import android.content.Context;
import android.graphics.Typeface;
import android.support.v4.app.FragmentActivity;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
@@ -32,6 +33,7 @@ import android.widget.TextView;
import org.bouncycastle.bcpg.sig.KeyFlags;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.service.SaveKeyringParcel;
import org.sufficientlysecure.keychain.ui.dialog.AddSubkeyDialogFragment;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.util.Calendar;
@@ -65,10 +67,11 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
public SaveKeyringParcel.SubkeyAdd mModel;
}
public View getView(final int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// Not recycled, inflate a new view
convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, null);
convertView = mInflater.inflate(R.layout.view_key_adv_subkey_item, parent, false);
final ViewHolder holder = new ViewHolder();
holder.vKeyId = (TextView) convertView.findViewById(R.id.subkey_item_key_id);
holder.vKeyDetails = (TextView) convertView.findViewById(R.id.subkey_item_details);
@@ -88,16 +91,8 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
vStatus.setVisibility(View.GONE);
convertView.setTag(holder);
holder.vDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// remove reference model item from adapter (data and notify about change)
SubkeysAddedAdapter.this.remove(holder.mModel);
}
});
}
final ViewHolder holder = (ViewHolder) convertView.getTag();
// save reference to model item
@@ -113,8 +108,41 @@ public class SubkeysAddedAdapter extends ArrayAdapter<SaveKeyringParcel.SubkeyAd
boolean isMasterKey = mNewKeyring && position == 0;
if (isMasterKey) {
holder.vKeyId.setTypeface(null, Typeface.BOLD);
holder.vDelete.setImageResource(R.drawable.ic_change_grey_24dp);
holder.vDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// swapping out the old master key with newly set master key
AddSubkeyDialogFragment addSubkeyDialogFragment =
AddSubkeyDialogFragment.newInstance(true);
addSubkeyDialogFragment
.setOnAlgorithmSelectedListener(
new AddSubkeyDialogFragment.OnAlgorithmSelectedListener() {
@Override
public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey) {
// calculate manually as the provided position variable
// is not always accurate
int pos = SubkeysAddedAdapter.this.getPosition(holder.mModel);
SubkeysAddedAdapter.this.remove(holder.mModel);
SubkeysAddedAdapter.this.insert(newSubkey, pos);
}
}
);
addSubkeyDialogFragment.show(
((FragmentActivity)mActivity).getSupportFragmentManager()
, "addSubkeyDialog");
}
});
} else {
holder.vKeyId.setTypeface(null, Typeface.NORMAL);
holder.vDelete.setImageResource(R.drawable.ic_close_grey_24dp);
holder.vDelete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// remove reference model item from adapter (data and notify about change)
SubkeysAddedAdapter.this.remove(holder.mModel);
}
});
}
holder.vKeyId.setText(R.string.edit_key_new_subkey);

View File

@@ -26,6 +26,7 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -45,8 +46,8 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
initTheme();
super.onCreate(savedInstanceState);
initTheme();
initLayout();
initToolbar();
}
@@ -65,6 +66,16 @@ public abstract class BaseActivity extends AppCompatActivity {
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home :
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public static void onResumeChecks(Context context) {
KeyserverSyncAdapterService.cancelUpdates(context);
// in case user has disabled sync from Android account settings

View File

@@ -0,0 +1,513 @@
/*
* Copyright (C) 2015 Dominik Schürmann <dominik@dominikschuermann.de>
* Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@mugenguild.com>
* Copyright (C) 2013-2014 Signe Rüsch
* Copyright (C) 2013-2014 Philipp Jakubeit
* Copyright (C) 2016 Nikita Mikhailov <nikita.s.mikhailov@gmail.com>
*
* 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.ui.base;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.TagLostException;
import android.os.AsyncTask;
import android.os.Bundle;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings;
import org.sufficientlysecure.keychain.provider.ProviderHelper;
import org.sufficientlysecure.keychain.service.PassphraseCacheService;
import org.sufficientlysecure.keychain.service.input.CryptoInputParcel;
import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;
import org.sufficientlysecure.keychain.securitytoken.CardException;
import org.sufficientlysecure.keychain.securitytoken.NfcTransport;
import org.sufficientlysecure.keychain.securitytoken.SecurityTokenHelper;
import org.sufficientlysecure.keychain.securitytoken.Transport;
import org.sufficientlysecure.keychain.util.UsbConnectionDispatcher;
import org.sufficientlysecure.keychain.securitytoken.UsbTransport;
import org.sufficientlysecure.keychain.ui.CreateKeyActivity;
import org.sufficientlysecure.keychain.ui.PassphraseDialogActivity;
import org.sufficientlysecure.keychain.ui.ViewKeyActivity;
import org.sufficientlysecure.keychain.ui.dialog.FidesmoInstallDialog;
import org.sufficientlysecure.keychain.ui.dialog.FidesmoPgpInstallDialog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.Passphrase;
import java.io.IOException;
import nordpol.android.OnDiscoveredTagListener;
import nordpol.android.TagDispatcher;
public abstract class BaseSecurityTokenActivity extends BaseActivity
implements OnDiscoveredTagListener, UsbConnectionDispatcher.OnDiscoveredUsbDeviceListener {
public static final int REQUEST_CODE_PIN = 1;
public static final String EXTRA_TAG_HANDLING_ENABLED = "tag_handling_enabled";
private static final String FIDESMO_APP_PACKAGE = "com.fidesmo.sec.android";
protected SecurityTokenHelper mSecurityTokenHelper = SecurityTokenHelper.getInstance();
protected TagDispatcher mNfcTagDispatcher;
protected UsbConnectionDispatcher mUsbDispatcher;
private boolean mTagHandlingEnabled;
private byte[] mSecurityTokenFingerprints;
private String mSecurityTokenUserId;
private byte[] mSecurityTokenAid;
/**
* Override to change UI before SecurityToken handling (UI thread)
*/
protected void onSecurityTokenPreExecute() {
}
/**
* Override to implement SecurityToken operations (background thread)
*/
protected void doSecurityTokenInBackground() throws IOException {
mSecurityTokenFingerprints = mSecurityTokenHelper.getFingerprints();
mSecurityTokenUserId = mSecurityTokenHelper.getUserId();
mSecurityTokenAid = mSecurityTokenHelper.getAid();
}
/**
* Override to handle result of SecurityToken operations (UI thread)
*/
protected void onSecurityTokenPostExecute() {
final long subKeyId = KeyFormattingUtils.getKeyIdFromFingerprint(mSecurityTokenFingerprints);
try {
CachedPublicKeyRing ring = new ProviderHelper(this).getCachedPublicKeyRing(
KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId));
long masterKeyId = ring.getMasterKeyId();
Intent intent = new Intent(this, ViewKeyActivity.class);
intent.setData(KeyRings.buildGenericKeyRingUri(masterKeyId));
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(ViewKeyActivity.EXTRA_SECURITY_TOKEN_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
} catch (PgpKeyNotFoundException e) {
Intent intent = new Intent(this, CreateKeyActivity.class);
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_AID, mSecurityTokenAid);
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_TOKEN_USER_ID, mSecurityTokenUserId);
intent.putExtra(CreateKeyActivity.EXTRA_SECURITY_FINGERPRINTS, mSecurityTokenFingerprints);
startActivity(intent);
}
}
/**
* Override to use something different than Notify (UI thread)
*/
protected void onSecurityTokenError(String error) {
Notify.create(this, error, Style.WARN).show();
}
/**
* Override to do something when PIN is wrong, e.g., clear passphrases (UI thread)
*/
protected void onSecurityTokenPinError(String error) {
onSecurityTokenError(error);
}
public void tagDiscovered(final Tag tag) {
// Actual NFC operations are executed in doInBackground to not block the UI thread
if (!mTagHandlingEnabled)
return;
securityTokenDiscovered(new NfcTransport(tag));
}
public void usbDeviceDiscovered(final UsbDevice usbDevice) {
// Actual USB operations are executed in doInBackground to not block the UI thread
if (!mTagHandlingEnabled)
return;
UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
securityTokenDiscovered(new UsbTransport(usbDevice, usbManager));
}
public void securityTokenDiscovered(final Transport transport) {
// Actual Security Token operations are executed in doInBackground to not block the UI thread
if (!mTagHandlingEnabled)
return;
new AsyncTask<Void, Void, IOException>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
onSecurityTokenPreExecute();
}
@Override
protected IOException doInBackground(Void... params) {
try {
handleSecurityToken(transport);
} catch (IOException e) {
return e;
}
return null;
}
@Override
protected void onPostExecute(IOException exception) {
super.onPostExecute(exception);
if (exception != null) {
handleSecurityTokenError(exception);
return;
}
onSecurityTokenPostExecute();
}
}.execute();
}
protected void pauseTagHandling() {
mTagHandlingEnabled = false;
}
protected void resumeTagHandling() {
mTagHandlingEnabled = true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNfcTagDispatcher = TagDispatcher.get(this, this, false, false, true, false);
mUsbDispatcher = new UsbConnectionDispatcher(this, this);
// Check whether we're recreating a previously destroyed instance
if (savedInstanceState != null) {
// Restore value of members from saved state
mTagHandlingEnabled = savedInstanceState.getBoolean(EXTRA_TAG_HANDLING_ENABLED);
} else {
mTagHandlingEnabled = true;
}
Intent intent = getIntent();
String action = intent.getAction();
if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) {
throw new AssertionError("should not happen: NfcOperationActivity.onCreate is called instead of onNewIntent!");
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(EXTRA_TAG_HANDLING_ENABLED, mTagHandlingEnabled);
}
/**
* This activity is started as a singleTop activity.
* All new NFC Intents which are delivered to this activity are handled here
*/
@Override
public void onNewIntent(final Intent intent) {
mNfcTagDispatcher.interceptIntent(intent);
}
private void handleSecurityTokenError(IOException e) {
if (e instanceof TagLostException) {
onSecurityTokenError(getString(R.string.security_token_error_tag_lost));
return;
}
if (e instanceof IsoDepNotSupportedException) {
onSecurityTokenError(getString(R.string.security_token_error_iso_dep_not_supported));
return;
}
short status;
if (e instanceof CardException) {
status = ((CardException) e).getResponseCode();
} else {
status = -1;
}
// Wrong PIN, a status of 63CX indicates X attempts remaining.
// NOTE: Used in ykneo-openpgp version < 1.0.10, changed to 0x6982 in 1.0.11
// https://github.com/Yubico/ykneo-openpgp/commit/90c2b91e86fb0e43ee234dd258834e75e3416410
if ((status & (short) 0xFFF0) == 0x63C0) {
int tries = status & 0x000F;
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getResources().getQuantityString(R.plurals.security_token_error_pin, tries, tries));
return;
}
// Otherwise, all status codes are fixed values.
switch (status) {
// These error conditions are likely to be experienced by an end user.
/* OpenPGP Card Spec: Security status not satisfied, PW wrong,
PW not checked (command not allowed), Secure messaging incorrect (checksum and/or cryptogram) */
// NOTE: Used in ykneo-openpgp >= 1.0.11 for wrong PIN
case 0x6982: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_security_not_satisfied));
break;
}
/* OpenPGP Card Spec: Selected file in termination state */
case 0x6285: {
onSecurityTokenError(getString(R.string.security_token_error_terminated));
break;
}
/* OpenPGP Card Spec: Wrong length (Lc and/or Le) */
// NOTE: Used in ykneo-openpgp < 1.0.10 for too short PIN, changed in 1.0.11 to 0x6A80 for too short PIN
// https://github.com/Yubico/ykneo-openpgp/commit/b49ce8241917e7c087a4dab7b2c755420ff4500f
case 0x6700: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_wrong_length));
break;
}
/* OpenPGP Card Spec: Incorrect parameters in the data field */
// NOTE: Used in ykneo-openpgp >= 1.0.11 for too short PIN
case 0x6A80: {
// hook to do something different when PIN is wrong
onSecurityTokenPinError(getString(R.string.security_token_error_bad_data));
break;
}
/* OpenPGP Card Spec: Authentication method blocked, PW blocked (error counter zero) */
case 0x6983: {
onSecurityTokenError(getString(R.string.security_token_error_authentication_blocked));
break;
}
/* OpenPGP Card Spec: Condition of use not satisfied */
case 0x6985: {
onSecurityTokenError(getString(R.string.security_token_error_conditions_not_satisfied));
break;
}
/* OpenPGP Card Spec: SM data objects incorrect (e.g. wrong TLV-structure in command data) */
// NOTE: 6A88 is "Not Found" in the spec, but ykneo-openpgp also returns 6A83 for this in some cases.
case 0x6A88:
case 0x6A83: {
onSecurityTokenError(getString(R.string.security_token_error_data_not_found));
break;
}
// 6F00 is a JavaCard proprietary status code, SW_UNKNOWN, and usually represents an
// unhandled exception on the security token.
case 0x6F00: {
onSecurityTokenError(getString(R.string.security_token_error_unknown));
break;
}
// 6A82 app not installed on security token!
case 0x6A82: {
if (mSecurityTokenHelper.isFidesmoToken()) {
// Check if the Fidesmo app is installed
if (isAndroidAppInstalled(FIDESMO_APP_PACKAGE)) {
promptFidesmoPgpInstall();
} else {
promptFidesmoAppInstall();
}
} else { // Other (possibly) compatible hardware
onSecurityTokenError(getString(R.string.security_token_error_pgp_app_not_installed));
}
break;
}
// These errors should not occur in everyday use; if they are returned, it means we
// made a mistake sending data to the token, or the token is misbehaving.
/* OpenPGP Card Spec: Last command of the chain expected */
case 0x6883: {
onSecurityTokenError(getString(R.string.security_token_error_chaining_error));
break;
}
/* OpenPGP Card Spec: Wrong parameters P1-P2 */
case 0x6B00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "P1/P2"));
break;
}
/* OpenPGP Card Spec: Instruction (INS) not supported */
case 0x6D00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "INS"));
break;
}
/* OpenPGP Card Spec: Class (CLA) not supported */
case 0x6E00: {
onSecurityTokenError(getString(R.string.security_token_error_header, "CLA"));
break;
}
default: {
onSecurityTokenError(getString(R.string.security_token_error, e.getMessage()));
break;
}
}
}
/**
* Called when the system is about to start resuming a previous activity,
* disables NFC Foreground Dispatch
*/
public void onPause() {
super.onPause();
Log.d(Constants.TAG, "BaseNfcActivity.onPause");
mNfcTagDispatcher.disableExclusiveNfc();
}
/**
* Called when the activity will start interacting with the user,
* enables NFC Foreground Dispatch
*/
public void onResume() {
super.onResume();
Log.d(Constants.TAG, "BaseNfcActivity.onResume");
mNfcTagDispatcher.enableExclusiveNfc();
}
protected void obtainSecurityTokenPin(RequiredInputParcel requiredInput) {
try {
Passphrase passphrase = PassphraseCacheService.getCachedPassphrase(this,
requiredInput.getMasterKeyId(), requiredInput.getSubKeyId());
if (passphrase != null) {
mSecurityTokenHelper.setPin(passphrase);
return;
}
Intent intent = new Intent(this, PassphraseDialogActivity.class);
intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT,
RequiredInputParcel.createRequiredPassphrase(requiredInput));
startActivityForResult(intent, REQUEST_CODE_PIN);
} catch (PassphraseCacheService.KeyNotFoundException e) {
throw new AssertionError(
"tried to find passphrase for non-existing key. this is a programming error!");
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_PIN: {
if (resultCode != Activity.RESULT_OK) {
setResult(resultCode);
finish();
return;
}
CryptoInputParcel input = data.getParcelableExtra(PassphraseDialogActivity.RESULT_CRYPTO_INPUT);
mSecurityTokenHelper.setPin(input.getPassphrase());
break;
}
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
protected void handleSecurityToken(Transport transport) throws IOException {
// Don't reconnect if device was already connected
if (!(mSecurityTokenHelper.isPersistentConnectionAllowed()
&& mSecurityTokenHelper.isConnected()
&& mSecurityTokenHelper.getTransport().equals(transport))) {
mSecurityTokenHelper.setTransport(transport);
mSecurityTokenHelper.connectToDevice();
}
doSecurityTokenInBackground();
}
public boolean isSecurityTokenConnected() {
return mSecurityTokenHelper.isConnected();
}
public static class IsoDepNotSupportedException extends IOException {
public IsoDepNotSupportedException(String detailMessage) {
super(detailMessage);
}
}
/**
* Ask user if she wants to install PGP onto her Fidesmo token
*/
private void promptFidesmoPgpInstall() {
FidesmoPgpInstallDialog fidesmoPgpInstallDialog = new FidesmoPgpInstallDialog();
fidesmoPgpInstallDialog.show(getSupportFragmentManager(), "fidesmoPgpInstallDialog");
}
/**
* Show a Dialog to the user informing that Fidesmo App must be installed and with option
* to launch the Google Play store.
*/
private void promptFidesmoAppInstall() {
FidesmoInstallDialog fidesmoInstallDialog = new FidesmoInstallDialog();
fidesmoInstallDialog.show(getSupportFragmentManager(), "fidesmoInstallDialog");
}
/**
* Use the package manager to detect if an application is installed on the phone
*
* @param uri an URI identifying the application's package
* @return 'true' if the app is installed
*/
private boolean isAndroidAppInstalled(String uri) {
PackageManager mPackageManager = getPackageManager();
boolean mAppInstalled;
try {
mPackageManager.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
mAppInstalled = true;
} catch (PackageManager.NameNotFoundException e) {
Log.e(Constants.TAG, "App not installed on Android device");
mAppInstalled = false;
}
return mAppInstalled;
}
@Override
protected void onStop() {
super.onStop();
mUsbDispatcher.onStop();
}
@Override
protected void onStart() {
super.onStart();
mUsbDispatcher.onStart();
}
public SecurityTokenHelper getSecurityTokenHelper() {
return mSecurityTokenHelper;
}
/**
* Run Security Token routines if last used token is connected and supports
* persistent connections
*/
public void checkDeviceConnection() {
mUsbDispatcher.rescanDevices();
}
}

View File

@@ -130,9 +130,9 @@ public class CryptoOperationHelper<T extends Parcelable, S extends OperationResu
switch (requiredInput.mType) {
// always use CryptoOperationHelper.startActivityForResult!
case NFC_MOVE_KEY_TO_CARD:
case NFC_DECRYPT:
case NFC_SIGN: {
case SECURITY_TOKEN_MOVE_KEY_TO_CARD:
case SECURITY_TOKEN_DECRYPT:
case SECURITY_TOKEN_SIGN: {
Intent intent = new Intent(activity, SecurityTokenOperationActivity.class);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_REQUIRED_INPUT, requiredInput);
intent.putExtra(SecurityTokenOperationActivity.EXTRA_CRYPTO_INPUT, cryptoInputParcel);

View File

@@ -50,14 +50,13 @@ import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.keyimport.HkpKeyserver;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.util.Log;
import org.sufficientlysecure.keychain.util.OkHttpClientFactory;
import org.sufficientlysecure.keychain.util.Preferences;
import org.sufficientlysecure.keychain.util.TlsHelper;
import org.sufficientlysecure.keychain.util.orbot.OrbotHelper;
@@ -354,19 +353,15 @@ public class AddEditKeyserverDialogFragment extends DialogFragment implements On
Log.d("Converted URL", newKeyserver.toString());
OkHttpClient client = HkpKeyserver.getClient(newKeyserver.toURL(), proxy);
// don't follow any redirects
client.setFollowRedirects(false);
client.setFollowSslRedirects(false);
if (onlyTrustedKeyserver
&& !TlsHelper.usePinnedCertificateIfAvailable(client, newKeyserver.toURL())) {
&& TlsHelper.getPinnedSslSocketFactory(newKeyserver.toURL()) == null) {
Log.w(Constants.TAG, "No pinned certificate for this host in OpenKeychain's assets.");
reason = FailureReason.NO_PINNED_CERTIFICATE;
return reason;
}
OkHttpClient client = OkHttpClientFactory.getClientPinnedIfAvailable(newKeyserver.toURL(), proxy);
client.newCall(new Request.Builder().url(newKeyserver.toURL()).build()).execute();
} catch (TlsHelper.TlsHelperException e) {
reason = FailureReason.CONNECTION_FAILED;

View File

@@ -17,25 +17,26 @@
package org.sufficientlysecure.keychain.ui.dialog;
import android.annotation.TargetApi;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.os.Build;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
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.TextWatcher;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import android.widget.TableRow;
import android.widget.TextView;
@@ -49,14 +50,18 @@ import org.sufficientlysecure.keychain.service.SaveKeyringParcel.Curve;
import org.sufficientlysecure.keychain.util.Choice;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
public class AddSubkeyDialogFragment extends DialogFragment {
public interface OnAlgorithmSelectedListener {
public void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
void onAlgorithmSelected(SaveKeyringParcel.SubkeyAdd newSubkey);
}
public enum SupportedKeyType {
RSA_2048, RSA_3072, RSA_4096, ECC_P256, ECC_P521
}
private static final String ARG_WILL_BE_MASTER_KEY = "will_be_master_key";
@@ -66,18 +71,12 @@ public class AddSubkeyDialogFragment extends DialogFragment {
private CheckBox mNoExpiryCheckBox;
private TableRow mExpiryRow;
private DatePicker mExpiryDatePicker;
private Spinner mAlgorithmSpinner;
private View mKeySizeRow;
private Spinner mKeySizeSpinner;
private View mCurveRow;
private Spinner mCurveSpinner;
private TextView mCustomKeyTextView;
private EditText mCustomKeyEditText;
private TextView mCustomKeyInfoTextView;
private CheckBox mFlagCertify;
private CheckBox mFlagSign;
private CheckBox mFlagEncrypt;
private CheckBox mFlagAuthenticate;
private Spinner mKeyTypeSpinner;
private RadioGroup mUsageRadioGroup;
private RadioButton mUsageNone;
private RadioButton mUsageSign;
private RadioButton mUsageEncrypt;
private RadioButton mUsageSignAndEncrypt;
private boolean mWillBeMasterKey;
@@ -96,6 +95,8 @@ public class AddSubkeyDialogFragment extends DialogFragment {
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final FragmentActivity context = getActivity();
@@ -106,25 +107,27 @@ public class AddSubkeyDialogFragment extends DialogFragment {
CustomAlertDialogBuilder dialog = new CustomAlertDialogBuilder(context);
@SuppressLint("InflateParams")
View view = mInflater.inflate(R.layout.add_subkey_dialog, null);
dialog.setView(view);
dialog.setTitle(R.string.title_add_subkey);
mNoExpiryCheckBox = (CheckBox) view.findViewById(R.id.add_subkey_no_expiry);
mExpiryRow = (TableRow) view.findViewById(R.id.add_subkey_expiry_row);
mExpiryDatePicker = (DatePicker) view.findViewById(R.id.add_subkey_expiry_date_picker);
mAlgorithmSpinner = (Spinner) view.findViewById(R.id.add_subkey_algorithm);
mKeySizeSpinner = (Spinner) view.findViewById(R.id.add_subkey_size);
mCurveSpinner = (Spinner) view.findViewById(R.id.add_subkey_curve);
mKeySizeRow = view.findViewById(R.id.add_subkey_row_size);
mCurveRow = view.findViewById(R.id.add_subkey_row_curve);
mCustomKeyTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_label);
mCustomKeyEditText = (EditText) view.findViewById(R.id.add_subkey_custom_key_size_input);
mCustomKeyInfoTextView = (TextView) view.findViewById(R.id.add_subkey_custom_key_size_info);
mFlagCertify = (CheckBox) view.findViewById(R.id.add_subkey_flag_certify);
mFlagSign = (CheckBox) view.findViewById(R.id.add_subkey_flag_sign);
mFlagEncrypt = (CheckBox) view.findViewById(R.id.add_subkey_flag_encrypt);
mFlagAuthenticate = (CheckBox) view.findViewById(R.id.add_subkey_flag_authenticate);
mKeyTypeSpinner = (Spinner) view.findViewById(R.id.add_subkey_type);
mUsageRadioGroup = (RadioGroup) view.findViewById(R.id.add_subkey_usage_group);
mUsageNone = (RadioButton) view.findViewById(R.id.add_subkey_usage_none);
mUsageSign = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign);
mUsageEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_encrypt);
mUsageSignAndEncrypt = (RadioButton) view.findViewById(R.id.add_subkey_usage_sign_and_encrypt);
if(mWillBeMasterKey) {
dialog.setTitle(R.string.title_change_master_key);
mUsageNone.setVisibility(View.VISIBLE);
mUsageNone.setChecked(true);
} else {
dialog.setTitle(R.string.title_add_subkey);
}
mNoExpiryCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
@@ -143,65 +146,24 @@ public class AddSubkeyDialogFragment extends DialogFragment {
mExpiryDatePicker.setMinDate(minDateCal.getTime().getTime());
{
ArrayList<Choice<Algorithm>> choices = new ArrayList<>();
choices.add(new Choice<>(Algorithm.DSA, getResources().getString(
R.string.dsa)));
if (!mWillBeMasterKey) {
choices.add(new Choice<>(Algorithm.ELGAMAL, getResources().getString(
R.string.elgamal)));
}
choices.add(new Choice<>(Algorithm.RSA, getResources().getString(
R.string.rsa)));
choices.add(new Choice<>(Algorithm.ECDSA, getResources().getString(
R.string.ecdsa)));
choices.add(new Choice<>(Algorithm.ECDH, getResources().getString(
R.string.ecdh)));
ArrayAdapter<Choice<Algorithm>> adapter = new ArrayAdapter<>(context,
ArrayList<Choice<SupportedKeyType>> choices = new ArrayList<>();
choices.add(new Choice<>(SupportedKeyType.RSA_2048, getResources().getString(
R.string.rsa_2048), getResources().getString(R.string.rsa_2048_description_html)));
choices.add(new Choice<>(SupportedKeyType.RSA_3072, getResources().getString(
R.string.rsa_3072), getResources().getString(R.string.rsa_3072_description_html)));
choices.add(new Choice<>(SupportedKeyType.RSA_4096, getResources().getString(
R.string.rsa_4096), getResources().getString(R.string.rsa_4096_description_html)));
choices.add(new Choice<>(SupportedKeyType.ECC_P256, getResources().getString(
R.string.ecc_p256), getResources().getString(R.string.ecc_p256_description_html)));
choices.add(new Choice<>(SupportedKeyType.ECC_P521, getResources().getString(
R.string.ecc_p521), getResources().getString(R.string.ecc_p521_description_html)));
TwoLineArrayAdapter adapter = new TwoLineArrayAdapter(context,
android.R.layout.simple_spinner_item, choices);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mAlgorithmSpinner.setAdapter(adapter);
// make RSA the default
mKeyTypeSpinner.setAdapter(adapter);
// make RSA 3072 the default
for (int i = 0; i < choices.size(); ++i) {
if (choices.get(i).getId() == Algorithm.RSA) {
mAlgorithmSpinner.setSelection(i);
break;
}
}
}
// dynamic ArrayAdapter must be created (instead of ArrayAdapter.getFromResource), because it's content may change
ArrayAdapter<CharSequence> keySizeAdapter = new ArrayAdapter<>(context, android.R.layout.simple_spinner_item,
new ArrayList<CharSequence>(Arrays.asList(getResources().getStringArray(R.array.rsa_key_size_spinner_values))));
keySizeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mKeySizeSpinner.setAdapter(keySizeAdapter);
mKeySizeSpinner.setSelection(1); // Default to 4096 for the key length
{
ArrayList<Choice<Curve>> choices = new ArrayList<>();
choices.add(new Choice<>(Curve.NIST_P256, getResources().getString(
R.string.key_curve_nist_p256)));
choices.add(new Choice<>(Curve.NIST_P384, getResources().getString(
R.string.key_curve_nist_p384)));
choices.add(new Choice<>(Curve.NIST_P521, getResources().getString(
R.string.key_curve_nist_p521)));
/* @see SaveKeyringParcel
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P256, getResources().getString(
R.string.key_curve_bp_p256)));
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P384, getResources().getString(
R.string.key_curve_bp_p384)));
choices.add(new Choice<Curve>(Curve.BRAINPOOL_P512, getResources().getString(
R.string.key_curve_bp_p512)));
*/
ArrayAdapter<Choice<Curve>> adapter = new ArrayAdapter<>(context,
android.R.layout.simple_spinner_item, choices);
mCurveSpinner.setAdapter(adapter);
// make NIST P-256 the default
for (int i = 0; i < choices.size(); ++i) {
if (choices.get(i).getId() == Curve.NIST_P256) {
mCurveSpinner.setSelection(i);
if (choices.get(i).getId() == SupportedKeyType.RSA_3072) {
mKeyTypeSpinner.setSelection(i);
break;
}
}
@@ -215,45 +177,35 @@ public class AddSubkeyDialogFragment extends DialogFragment {
final AlertDialog alertDialog = dialog.show();
mCustomKeyEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
mKeyTypeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setOkButtonAvailability(alertDialog);
}
});
mKeySizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
setCustomKeyVisibility();
setOkButtonAvailability(alertDialog);
// noinspection unchecked
SupportedKeyType keyType = ((Choice<SupportedKeyType>) parent.getSelectedItem()).getId();
// RadioGroup.getCheckedRadioButtonId() gives the wrong RadioButton checked
// when programmatically unchecking children radio buttons. Clearing all is the only option.
mUsageRadioGroup.clearCheck();
if(mWillBeMasterKey) {
mUsageNone.setChecked(true);
}
if (keyType == SupportedKeyType.ECC_P521 || keyType == SupportedKeyType.ECC_P256) {
mUsageSignAndEncrypt.setEnabled(false);
if (mWillBeMasterKey) {
mUsageEncrypt.setEnabled(false);
}
} else {
// need to enable if previously disabled for ECC masterkey
mUsageEncrypt.setEnabled(true);
mUsageSignAndEncrypt.setEnabled(true);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
mAlgorithmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
updateUiForAlgorithm(((Choice<Algorithm>) parent.getSelectedItem()).getId());
setCustomKeyVisibility();
setOkButtonAvailability(alertDialog);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
public void onNothingSelected(AdapterView<?> parent) {}
});
return alertDialog;
@@ -269,36 +221,74 @@ public class AddSubkeyDialogFragment extends DialogFragment {
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!mFlagCertify.isChecked() && !mFlagSign.isChecked()
&& !mFlagEncrypt.isChecked() && !mFlagAuthenticate.isChecked()) {
Toast.makeText(getActivity(), R.string.edit_key_select_flag, Toast.LENGTH_LONG).show();
if (mUsageRadioGroup.getCheckedRadioButtonId() == -1) {
Toast.makeText(getActivity(), R.string.edit_key_select_usage, Toast.LENGTH_LONG).show();
return;
}
Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
// noinspection unchecked
SupportedKeyType keyType = ((Choice<SupportedKeyType>) mKeyTypeSpinner.getSelectedItem()).getId();
Curve curve = null;
Integer keySize = null;
// For EC keys, add a curve
if (algorithm == Algorithm.ECDH || algorithm == Algorithm.ECDSA) {
curve = ((Choice<Curve>) mCurveSpinner.getSelectedItem()).getId();
// Otherwise, get a keysize
} else {
keySize = getProperKeyLength(algorithm, getSelectedKeyLength());
Algorithm algorithm = null;
// set keysize & curve, for RSA & ECC respectively
switch (keyType) {
case RSA_2048: {
keySize = 2048;
break;
}
case RSA_3072: {
keySize = 3072;
break;
}
case RSA_4096: {
keySize = 4096;
break;
}
case ECC_P256: {
curve = Curve.NIST_P256;
break;
}
case ECC_P521: {
curve = Curve.NIST_P521;
break;
}
}
// set algorithm
switch (keyType) {
case RSA_2048:
case RSA_3072:
case RSA_4096: {
algorithm = Algorithm.RSA;
break;
}
case ECC_P256:
case ECC_P521: {
if(mUsageEncrypt.isChecked()) {
algorithm = Algorithm.ECDH;
} else {
algorithm = Algorithm.ECDSA;
}
break;
}
}
// set flags
int flags = 0;
if (mFlagCertify.isChecked()) {
if (mWillBeMasterKey) {
flags |= KeyFlags.CERTIFY_OTHER;
}
if (mFlagSign.isChecked()) {
if (mUsageSign.isChecked()) {
flags |= KeyFlags.SIGN_DATA;
}
if (mFlagEncrypt.isChecked()) {
} else if (mUsageEncrypt.isChecked()) {
flags |= KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
} else if (mUsageSignAndEncrypt.isChecked()) {
flags |= KeyFlags.SIGN_DATA | KeyFlags.ENCRYPT_COMMS | KeyFlags.ENCRYPT_STORAGE;
}
if (mFlagAuthenticate.isChecked()) {
flags |= KeyFlags.AUTHENTICATION;
}
long expiry;
if (mNoExpiryCheckBox.isChecked()) {
@@ -332,206 +322,29 @@ public class AddSubkeyDialogFragment extends DialogFragment {
}
}
private int getSelectedKeyLength() {
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
final String customLengthString = getResources().getString(R.string.key_size_custom);
final boolean customSelected = customLengthString.equals(selectedItemString);
String keyLengthString = customSelected ? mCustomKeyEditText.getText().toString() : selectedItemString;
int keySize;
try {
keySize = Integer.parseInt(keyLengthString);
} catch (NumberFormatException e) {
keySize = 0;
private class TwoLineArrayAdapter extends ArrayAdapter<Choice<SupportedKeyType>> {
public TwoLineArrayAdapter(Context context, int resource, List<Choice<SupportedKeyType>> objects) {
super(context, resource, objects);
}
return keySize;
}
/**
* <h3>RSA</h3>
* <p>for RSA algorithm, key length must be greater than 2048. Possibility to generate keys bigger
* than 8192 bits is currently disabled, because it's almost impossible to generate them on a mobile device (check
* <a href="http://www.javamex.com/tutorials/cryptography/rsa_key_length.shtml">RSA key length plot</a> and
* <a href="http://www.keylength.com/">Cryptographic Key Length Recommendation</a>). Also, key length must be a
* multiplicity of 8.</p>
* <h3>ElGamal</h3>
* <p>For ElGamal algorithm, supported key lengths are 2048, 3072, 4096 or 8192 bits.</p>
* <h3>DSA</h3>
* <p>For DSA algorithm key length must be between 2048 and 3072. Also, it must me dividable by 64.</p>
*
* @return correct key length, according to BouncyCastle specification. Returns <code>-1</code>, if key length is
* inappropriate.
*/
private int getProperKeyLength(Algorithm algorithm, int currentKeyLength) {
final int[] elGamalSupportedLengths = {2048, 3072, 4096, 8192};
int properKeyLength = -1;
switch (algorithm) {
case RSA: {
if (currentKeyLength >= 2048 && currentKeyLength <= 16384) {
properKeyLength = currentKeyLength + ((8 - (currentKeyLength % 8)) % 8);
}
break;
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
// inflate view if not given one
if (convertView == null) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.two_line_spinner_dropdown_item, parent, false);
}
case ELGAMAL: {
int[] elGammalKeyDiff = new int[elGamalSupportedLengths.length];
for (int i = 0; i < elGamalSupportedLengths.length; i++) {
elGammalKeyDiff[i] = Math.abs(elGamalSupportedLengths[i] - currentKeyLength);
}
int minimalValue = Integer.MAX_VALUE;
int minimalIndex = -1;
for (int i = 0; i < elGammalKeyDiff.length; i++) {
if (elGammalKeyDiff[i] <= minimalValue) {
minimalValue = elGammalKeyDiff[i];
minimalIndex = i;
}
}
properKeyLength = elGamalSupportedLengths[minimalIndex];
break;
}
case DSA: {
// Bouncy Castle supports 4096 maximum
if (currentKeyLength >= 2048 && currentKeyLength <= 4096) {
properKeyLength = currentKeyLength + ((64 - (currentKeyLength % 64)) % 64);
}
break;
}
}
return properKeyLength;
}
private void setOkButtonAvailability(AlertDialog alertDialog) {
Algorithm algorithm = ((Choice<Algorithm>) mAlgorithmSpinner.getSelectedItem()).getId();
boolean enabled = algorithm == Algorithm.ECDSA || algorithm == Algorithm.ECDH
|| getProperKeyLength(algorithm, getSelectedKeyLength()) > 0;
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(enabled);
}
Choice c = this.getItem(position);
private void setCustomKeyVisibility() {
final String selectedItemString = (String) mKeySizeSpinner.getSelectedItem();
final String customLengthString = getResources().getString(R.string.key_size_custom);
final boolean customSelected = customLengthString.equals(selectedItemString);
final int visibility = customSelected ? View.VISIBLE : View.GONE;
TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
mCustomKeyEditText.setVisibility(visibility);
mCustomKeyTextView.setVisibility(visibility);
mCustomKeyInfoTextView.setVisibility(visibility);
text1.setText(c.getName());
text2.setText(Html.fromHtml(c.getDescription()));
// hide keyboard after setting visibility to gone
if (visibility == View.GONE) {
InputMethodManager imm = (InputMethodManager)
getActivity().getSystemService(FragmentActivity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mCustomKeyEditText.getWindowToken(), 0);
}
}
private void updateUiForAlgorithm(Algorithm algorithm) {
final ArrayAdapter<CharSequence> keySizeAdapter = (ArrayAdapter<CharSequence>) mKeySizeSpinner.getAdapter();
keySizeAdapter.clear();
switch (algorithm) {
case RSA: {
replaceArrayAdapterContent(keySizeAdapter, R.array.rsa_key_size_spinner_values);
mKeySizeSpinner.setSelection(1);
mKeySizeRow.setVisibility(View.VISIBLE);
mCurveRow.setVisibility(View.GONE);
mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_rsa));
// allowed flags:
mFlagSign.setEnabled(true);
mFlagEncrypt.setEnabled(true);
mFlagAuthenticate.setEnabled(true);
if (mWillBeMasterKey) {
mFlagCertify.setEnabled(true);
mFlagCertify.setChecked(true);
mFlagSign.setChecked(false);
mFlagEncrypt.setChecked(false);
} else {
mFlagCertify.setEnabled(false);
mFlagCertify.setChecked(false);
mFlagSign.setChecked(true);
mFlagEncrypt.setChecked(true);
}
mFlagAuthenticate.setChecked(false);
break;
}
case ELGAMAL: {
replaceArrayAdapterContent(keySizeAdapter, R.array.elgamal_key_size_spinner_values);
mKeySizeSpinner.setSelection(3);
mKeySizeRow.setVisibility(View.VISIBLE);
mCurveRow.setVisibility(View.GONE);
mCustomKeyInfoTextView.setText(""); // ElGamal does not support custom key length
// allowed flags:
mFlagCertify.setChecked(false);
mFlagCertify.setEnabled(false);
mFlagSign.setChecked(false);
mFlagSign.setEnabled(false);
mFlagEncrypt.setChecked(true);
mFlagEncrypt.setEnabled(true);
mFlagAuthenticate.setChecked(false);
mFlagAuthenticate.setEnabled(false);
break;
}
case DSA: {
replaceArrayAdapterContent(keySizeAdapter, R.array.dsa_key_size_spinner_values);
mKeySizeSpinner.setSelection(2);
mKeySizeRow.setVisibility(View.VISIBLE);
mCurveRow.setVisibility(View.GONE);
mCustomKeyInfoTextView.setText(getResources().getString(R.string.key_size_custom_info_dsa));
// allowed flags:
mFlagCertify.setChecked(false);
mFlagCertify.setEnabled(false);
mFlagSign.setChecked(true);
mFlagSign.setEnabled(true);
mFlagEncrypt.setChecked(false);
mFlagEncrypt.setEnabled(false);
mFlagAuthenticate.setChecked(false);
mFlagAuthenticate.setEnabled(false);
break;
}
case ECDSA: {
mKeySizeRow.setVisibility(View.GONE);
mCurveRow.setVisibility(View.VISIBLE);
mCustomKeyInfoTextView.setText("");
// allowed flags:
mFlagCertify.setEnabled(mWillBeMasterKey);
mFlagCertify.setChecked(mWillBeMasterKey);
mFlagSign.setEnabled(true);
mFlagSign.setChecked(!mWillBeMasterKey);
mFlagEncrypt.setEnabled(false);
mFlagEncrypt.setChecked(false);
mFlagAuthenticate.setEnabled(true);
mFlagAuthenticate.setChecked(false);
break;
}
case ECDH: {
mKeySizeRow.setVisibility(View.GONE);
mCurveRow.setVisibility(View.VISIBLE);
mCustomKeyInfoTextView.setText("");
// allowed flags:
mFlagCertify.setChecked(false);
mFlagCertify.setEnabled(false);
mFlagSign.setChecked(false);
mFlagSign.setEnabled(false);
mFlagEncrypt.setChecked(true);
mFlagEncrypt.setEnabled(true);
mFlagAuthenticate.setChecked(false);
mFlagAuthenticate.setEnabled(false);
break;
}
}
keySizeAdapter.notifyDataSetChanged();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void replaceArrayAdapterContent(ArrayAdapter<CharSequence> arrayAdapter, int stringArrayResourceId) {
final String[] spinnerValuesStringArray = getResources().getStringArray(stringArrayResourceId);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
arrayAdapter.addAll(spinnerValuesStringArray);
} else {
for (final String value : spinnerValuesStringArray) {
arrayAdapter.add(value);
}
return convertView;
}
}

View File

@@ -35,7 +35,11 @@ public class EditSubkeyDialogFragment extends DialogFragment {
public static final int MESSAGE_CHANGE_EXPIRY = 1;
public static final int MESSAGE_REVOKE = 2;
public static final int MESSAGE_STRIP = 3;
public static final int MESSAGE_MOVE_KEY_TO_CARD = 4;
public static final int MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN = 4;
public static final int SUBKEY_MENU_CHANGE_EXPIRY = 0;
public static final int SUBKEY_MENU_REVOKE_SUBKEY = 1;
public static final int SUBKEY_MENU_STRIP_SUBKEY = 2;
public static final int SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN = 3;
private Messenger mMessenger;
@@ -68,17 +72,17 @@ public class EditSubkeyDialogFragment extends DialogFragment {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0:
case SUBKEY_MENU_CHANGE_EXPIRY:
sendMessageToHandler(MESSAGE_CHANGE_EXPIRY, null);
break;
case 1:
case SUBKEY_MENU_REVOKE_SUBKEY:
sendMessageToHandler(MESSAGE_REVOKE, null);
break;
case 2:
sendMessageToHandler(MESSAGE_STRIP, null);
case SUBKEY_MENU_STRIP_SUBKEY:
showAlertDialog();
break;
case 3:
sendMessageToHandler(MESSAGE_MOVE_KEY_TO_CARD, null);
case SUBKEY_MENU_MOVE_TO_SECURITY_TOKEN:
sendMessageToHandler(MESSAGE_MOVE_KEY_TO_SECURITY_TOKEN, null);
break;
default:
break;
@@ -95,6 +99,25 @@ public class EditSubkeyDialogFragment extends DialogFragment {
return builder.show();
}
private void showAlertDialog() {
CustomAlertDialogBuilder stripAlertDialog = new CustomAlertDialogBuilder(getActivity());
stripAlertDialog.setTitle(R.string.title_alert_strip).
setMessage(R.string.alert_strip).setCancelable(true);
stripAlertDialog.setPositiveButton(R.string.strip, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
sendMessageToHandler(MESSAGE_STRIP, null);
}
});
stripAlertDialog.setNegativeButton(R.string.btn_do_not_save, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dismiss();
}
});
stripAlertDialog.show();
}
/**
* Send message back to handler which is initialized in a activity
*

View File

@@ -119,7 +119,7 @@ public class LinkedIdCreateTwitterStep1Fragment extends Fragment {
private static Boolean checkHandle(String handle) {
try {
HttpURLConnection nection =
(HttpURLConnection) new URL("https://twitter.com/" + handle).openConnection();
(HttpURLConnection) new URL("https://twitter.com/" + handle).getUrlResponse();
nection.setRequestMethod("HEAD");
nection.setRequestProperty("User-Agent", "OpenKeychain");
return nection.getResponseCode() == 200;

View File

@@ -64,8 +64,8 @@ public class KeyFormattingUtils {
String algorithmStr;
switch (algorithm) {
case PublicKeyAlgorithmTags.RSA_ENCRYPT:
case PublicKeyAlgorithmTags.RSA_GENERAL:
case PublicKeyAlgorithmTags.RSA_ENCRYPT:
case PublicKeyAlgorithmTags.RSA_SIGN: {
algorithmStr = "RSA";
break;

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Alex Fong Jie Wen <alexfongg@gmail.com>
*
* 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.ui.util.spinner;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.Spinner;
/**
* Custom spinner which uses a hack to
* always set focus on first item in list
*
*/
public class FocusFirstItemSpinner extends Spinner {
/**
* Spinner is originally designed to set focus on the currently selected item.
* When Spinner is selected to show dropdown, 'performClick()' is called internally.
* 'getSelectedItemPosition()' is then called to obtain the item to focus on.
* We use a toggle to have 'getSelectedItemPosition()' return the 0th index
* for this particular case.
*/
private boolean mToggleFlag = true;
public FocusFirstItemSpinner(Context context, AttributeSet attrs,
int defStyle, int mode) {
super(context, attrs, defStyle, mode);
}
public FocusFirstItemSpinner(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
public FocusFirstItemSpinner(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FocusFirstItemSpinner(Context context, int mode) {
super(context, mode);
}
public FocusFirstItemSpinner(Context context) {
super(context);
}
@Override
public int getSelectedItemPosition() {
if (!mToggleFlag) {
return 0;
}
return super.getSelectedItemPosition();
}
@Override
public boolean performClick() {
mToggleFlag = false;
boolean result = super.performClick();
mToggleFlag = true;
return result;
}
}

View File

@@ -113,11 +113,6 @@ public class CertifyKeySpinner extends KeySpinner {
@Override
boolean isItemEnabled(Cursor cursor) {
// "none" entry is always enabled!
if (cursor.getPosition() == 0) {
return true;
}
if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
return false;
}

View File

@@ -23,15 +23,11 @@ import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Patterns;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.util.ContactHelper;
import java.util.regex.Matcher;
public class EmailEditText extends AppCompatAutoCompleteTextView {
public EmailEditText(Context context) {
@@ -70,20 +66,7 @@ public class EmailEditText extends AppCompatAutoCompleteTextView {
@Override
public void afterTextChanged(Editable editable) {
String email = editable.toString();
if (email.length() > 0) {
Matcher emailMatcher = Patterns.EMAIL_ADDRESS.matcher(email);
if (emailMatcher.matches()) {
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_ok, 0);
} else {
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0,
R.drawable.ic_stat_retyped_bad, 0);
}
} else {
// remove drawable if email is empty
EmailEditText.this.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
}
}
};

View File

@@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.ui.widget;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
@@ -83,6 +84,11 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView<KeyItem>
LayoutInflater l = LayoutInflater.from(getContext());
View view = l.inflate(R.layout.recipient_box_entry, null);
((TextView) view.findViewById(android.R.id.text1)).setText(keyItem.getReadableName());
if (keyItem.mIsRevoked || !keyItem.mHasEncrypt || keyItem.mIsExpired) {
((TextView) view.findViewById(android.R.id.text1)).setTextColor(Color.RED);
}
return view;
}
@@ -171,4 +177,22 @@ public class EncryptKeyCompletionView extends TokenCompleteTextView<KeyItem>
mLoaderManager.restartLoader(0, args, this);
}
@Override
public boolean enoughToFilter() {
return true;
}
public void showAllKeys(){
Bundle args = new Bundle();
args.putString(ARG_QUERY, "");
mLoaderManager.restartLoader(0, args, this);
super.showDropDown();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// increase width to include add button
this.setDropDownWidth(this.getRight());
}
}

View File

@@ -72,11 +72,6 @@ public class SignKeySpinner extends KeySpinner {
@Override
boolean isItemEnabled(Cursor cursor) {
// "none" entry is always enabled!
if (cursor.getPosition() == 0) {
return true;
}
if (cursor.getInt(KeyAdapter.INDEX_IS_REVOKED) != 0) {
return false;
}