diff options
22 files changed, 756 insertions, 257 deletions
diff --git a/OpenKeychain/src/main/AndroidManifest.xml b/OpenKeychain/src/main/AndroidManifest.xml index b9c8f8a2b..945d7164c 100644 --- a/OpenKeychain/src/main/AndroidManifest.xml +++ b/OpenKeychain/src/main/AndroidManifest.xml @@ -467,6 +467,11 @@              android:label="@string/title_key_server_preference"              android:windowSoftInputMode="stateHidden" />          <activity +            android:name=".ui.SettingsCacheTTLActivity" +            android:configChanges="orientation|screenSize|keyboardHidden|keyboard" +            android:label="@string/title_cache_ttl_preference" +            android:windowSoftInputMode="stateHidden" /> +        <activity              android:name=".ui.BackupActivity"              android:configChanges="keyboardHidden|keyboard"              android:label="@string/title_backup"> diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java index 2de1cb38b..7bac8aa97 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/Constants.java @@ -95,7 +95,8 @@ public final class Constants {      }      public static final class Pref { -        public static final String PASSPHRASE_CACHE_TTL = "passphraseCacheTtl"; +        public static final String PASSPHRASE_CACHE_TTLS = "passphraseCacheTtls"; +        public static final String PASSPHRASE_CACHE_DEFAULT = "passphraseCacheDefault";          public static final String PASSPHRASE_CACHE_SUBS = "passphraseCacheSubs";          public static final String LANGUAGE = "language";          public static final String KEY_SERVERS = "keyServers"; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java index 51485a35d..8af2fdc67 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/operations/EditKeyOperation.java @@ -171,27 +171,6 @@ public class EditKeyOperation extends BaseOperation<SaveKeyringParcel> {              return new EditKeyResult(EditKeyResult.RESULT_ERROR, log, null);          } -        // There is a new passphrase - cache it -        if (saveParcel.mNewUnlock != null && cryptoInput.mCachePassphrase) { -            log.add(LogType.MSG_ED_CACHING_NEW, 1); - -            // NOTE: Don't cache empty passphrases! Important for MOVE_KEY_TO_CARD -            if (saveParcel.mNewUnlock.mNewPassphrase != null -                    && ( ! saveParcel.mNewUnlock.mNewPassphrase.isEmpty())) { -                PassphraseCacheService.addCachedPassphrase(mContext, -                        ring.getMasterKeyId(), -                        ring.getMasterKeyId(), -                        saveParcel.mNewUnlock.mNewPassphrase, -                        ring.getPublicKey().getPrimaryUserIdWithFallback()); -            } else if (saveParcel.mNewUnlock.mNewPin != null) { -                PassphraseCacheService.addCachedPassphrase(mContext, -                        ring.getMasterKeyId(), -                        ring.getMasterKeyId(), -                        saveParcel.mNewUnlock.mNewPin, -                        ring.getPublicKey().getPrimaryUserIdWithFallback()); -            } -        } -          updateProgress(R.string.progress_done, 100, 100);          // make sure new data is synced into contacts diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java index 73da3aff9..606daabed 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/PassphraseCacheService.java @@ -93,7 +93,7 @@ public class PassphraseCacheService extends Service {      public static final String EXTRA_MESSENGER = "messenger";      public static final String EXTRA_USER_ID = "user_id"; -    private static final long DEFAULT_TTL = 15; +    private static final int DEFAULT_TTL = 0;      private static final int MSG_PASSPHRASE_CACHE_GET_OKAY = 1;      private static final int MSG_PASSPHRASE_CACHE_GET_KEY_NOT_FOUND = 2; @@ -120,13 +120,14 @@ public class PassphraseCacheService extends Service {       */      public static void addCachedPassphrase(Context context, long masterKeyId, long subKeyId,                                             Passphrase passphrase, -                                           String primaryUserId) { +                                           String primaryUserId, +                                           int timeToLiveSeconds) {          Log.d(Constants.TAG, "PassphraseCacheService.addCachedPassphrase() for " + masterKeyId);          Intent intent = new Intent(context, PassphraseCacheService.class);          intent.setAction(ACTION_PASSPHRASE_CACHE_ADD); -        intent.putExtra(EXTRA_TTL, Preferences.getPreferences(context).getPassphraseCacheTtl()); +        intent.putExtra(EXTRA_TTL, timeToLiveSeconds);          intent.putExtra(EXTRA_PASSPHRASE, passphrase);          intent.putExtra(EXTRA_KEY_ID, masterKeyId);          intent.putExtra(EXTRA_SUBKEY_ID, subKeyId); @@ -236,9 +237,7 @@ public class PassphraseCacheService extends Service {              if (cachedPassphrase == null) {                  return null;              } -            addCachedPassphrase(this, Constants.key.symmetric, Constants.key.symmetric, -                    cachedPassphrase.getPassphrase(), getString(R.string.passp_cache_notif_pwd)); -            return cachedPassphrase.getPassphrase(); +            return cachedPassphrase.mPassphrase;          }          // try to get master key id which is used as an identifier for cached passphrases @@ -285,10 +284,7 @@ public class PassphraseCacheService extends Service {          } -        // set it again to reset the cache life cycle -        Log.d(Constants.TAG, "PassphraseCacheService: Cache passphrase again when getting it!"); -        addCachedPassphrase(this, masterKeyId, subKeyId, cachedPassphrase.getPassphrase(), cachedPassphrase.getPrimaryUserID()); -        return cachedPassphrase.getPassphrase(); +        return cachedPassphrase.mPassphrase;      }      /** @@ -307,13 +303,18 @@ public class PassphraseCacheService extends Service {                      if (action.equals(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE)) {                          long keyId = intent.getLongExtra(EXTRA_KEY_ID, -1); -                        timeout(keyId); +                        removeTimeoutedPassphrase(keyId); +                    } + +                    if (action.equals(Intent.ACTION_SCREEN_OFF)) { +                        removeScreenLockPassphrases();                      }                  }              };              IntentFilter filter = new IntentFilter();              filter.addAction(BROADCAST_ACTION_PASSPHRASE_CACHE_SERVICE); +            filter.addAction(Intent.ACTION_SCREEN_OFF);              registerReceiver(mIntentReceiver, filter);          }      } @@ -341,35 +342,40 @@ public class PassphraseCacheService extends Service {              return START_STICKY;          } -        // register broadcastreceiver -        registerReceiver(); -          String action = intent.getAction();          switch (action) {              case ACTION_PASSPHRASE_CACHE_ADD: { -                long ttl = intent.getLongExtra(EXTRA_TTL, DEFAULT_TTL);                  long masterKeyId = intent.getLongExtra(EXTRA_KEY_ID, -1);                  long subKeyId = intent.getLongExtra(EXTRA_SUBKEY_ID, -1); +                long timeoutTtl = intent.getIntExtra(EXTRA_TTL, DEFAULT_TTL);                  Passphrase passphrase = intent.getParcelableExtra(EXTRA_PASSPHRASE);                  String primaryUserID = intent.getStringExtra(EXTRA_USER_ID);                  Log.d(Constants.TAG,                          "PassphraseCacheService: Received ACTION_PASSPHRASE_CACHE_ADD intent in onStartCommand() with masterkeyId: " -                                + masterKeyId + ", subKeyId: " + subKeyId + ", ttl: " + ttl + ", usrId: " + primaryUserID +                                + masterKeyId + ", subKeyId: " + subKeyId + ", ttl: " + timeoutTtl + ", usrId: " + primaryUserID                  );                  // if we don't cache by specific subkey id, or the requested subkey is the master key,                  // just add master key id to the cache, otherwise, add this specific subkey to the cache                  long referenceKeyId =                          Preferences.getPreferences(mContext).getPassphraseCacheSubs() ? subKeyId : masterKeyId; -                mPassphraseCache.put(referenceKeyId, new CachedPassphrase(passphrase, primaryUserID)); -                if (ttl > 0) { + +                CachedPassphrase cachedPassphrase; +                if (timeoutTtl == 0L) { +                    cachedPassphrase = CachedPassphrase.getPassphraseLock(passphrase, primaryUserID); +                } else { +                    cachedPassphrase = CachedPassphrase.getPassphraseTtlTimeout(passphrase, primaryUserID, timeoutTtl); + +                    long triggerTime = new Date().getTime() + (timeoutTtl * 1000);                      // register new alarm with keyId for this passphrase -                    long triggerTime = new Date().getTime() + (ttl * 1000);                      AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);                      am.set(AlarmManager.RTC_WAKEUP, triggerTime, buildIntent(this, referenceKeyId));                  } + +                mPassphraseCache.put(referenceKeyId, cachedPassphrase); +                  break;              }              case ACTION_PASSPHRASE_CACHE_GET: { @@ -439,16 +445,14 @@ public class PassphraseCacheService extends Service {          return START_STICKY;      } -    /** -     * Called when one specific passphrase for keyId timed out -     */ -    private void timeout(long keyId) { +    /** Called when one specific passphrase for keyId timed out. */ +    private void removeTimeoutedPassphrase(long keyId) {          CachedPassphrase cPass = mPassphraseCache.get(keyId);          if (cPass != null) { -            if (cPass.getPassphrase() != null) { +            if (cPass.mPassphrase != null) {                  // clean internal char[] from memory! -                cPass.getPassphrase().removeFromMemory(); +                cPass.mPassphrase.removeFromMemory();              }              // remove passphrase object              mPassphraseCache.remove(keyId); @@ -459,6 +463,24 @@ public class PassphraseCacheService extends Service {          updateService();      } +    private void removeScreenLockPassphrases() { + +        for (int i = 0; i < mPassphraseCache.size(); ) { +            CachedPassphrase cPass = mPassphraseCache.valueAt(i); +            if (cPass.mTimeoutMode == TimeoutMode.LOCK) { +                // remove passphrase object +                mPassphraseCache.removeAt(i); +                continue; +            } +            // only do this if we didn't remove at, which continues loop by reducing size! +            i += 1; +        } + +        Log.d(Constants.TAG, "PassphraseCacheService Removing all cached-until-lock passphrases from memory!"); + +        updateService(); +    } +      private void updateService() {          if (mPassphraseCache.size() > 0) {              startForeground(Constants.Notification.PASSPHRASE_CACHE, getNotification()); @@ -483,7 +505,7 @@ public class PassphraseCacheService extends Service {          // Moves events into the big view          for (int i = 0; i < mPassphraseCache.size(); i++) { -            inboxStyle.addLine(mPassphraseCache.valueAt(i).getPrimaryUserID()); +            inboxStyle.addLine(mPassphraseCache.valueAt(i).mPrimaryUserId);          }          // Moves the big view style object into the notification object. @@ -516,6 +538,8 @@ public class PassphraseCacheService extends Service {          super.onCreate();          mContext = this;          Log.d(Constants.TAG, "PassphraseCacheService, onCreate()"); + +        registerReceiver();      }      @Override @@ -539,29 +563,34 @@ public class PassphraseCacheService extends Service {      private final IBinder mBinder = new PassphraseCacheBinder(); -    public class CachedPassphrase { -        private String primaryUserID; -        private Passphrase passphrase; - -        public CachedPassphrase(Passphrase passphrase, String primaryUserID) { -            setPassphrase(passphrase); -            setPrimaryUserID(primaryUserID); -        } +    private enum TimeoutMode { +        NEVER, TTL, LOCK +    } -        public String getPrimaryUserID() { -            return primaryUserID; +    private static class CachedPassphrase { +        private String mPrimaryUserId; +        private Passphrase mPassphrase; +        private TimeoutMode mTimeoutMode; +        private Long mTimeoutTime; + +        private CachedPassphrase(Passphrase passphrase, String primaryUserId, TimeoutMode timeoutMode, Long timeoutTime) { +            mPassphrase = passphrase; +            mPrimaryUserId = primaryUserId; +            mTimeoutMode = timeoutMode; +            mTimeoutTime = timeoutTime;          } -        public Passphrase getPassphrase() { -            return passphrase; +        static CachedPassphrase getPassphraseNoTimeout(Passphrase passphrase, String primaryUserId) { +            return new CachedPassphrase(passphrase, primaryUserId, TimeoutMode.NEVER, null);          } -        public void setPrimaryUserID(String primaryUserID) { -            this.primaryUserID = primaryUserID; +        static CachedPassphrase getPassphraseTtlTimeout(Passphrase passphrase, String primaryUserId, long timeoutTime) { +            return new CachedPassphrase(passphrase, primaryUserId, TimeoutMode.TTL, timeoutTime);          } -        public void setPassphrase(Passphrase passphrase) { -            this.passphrase = passphrase; +        static CachedPassphrase getPassphraseLock(Passphrase passphrase, String primaryUserId) { +            return new CachedPassphrase(passphrase, primaryUserId, TimeoutMode.LOCK, null);          }      } +  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java index 1f99836ea..429d7a7e5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/input/RequiredInputParcel.java @@ -28,6 +28,8 @@ public class RequiredInputParcel implements Parcelable {      private Long mMasterKeyId;      private Long mSubKeyId; +    public boolean mSkipCaching = false; +      private RequiredInputParcel(RequiredInputType type, byte[][] inputData,              int[] signAlgos, Date signatureTime, Long masterKeyId, Long subKeyId) {          mType = type; @@ -66,6 +68,7 @@ public class RequiredInputParcel implements Parcelable {          mSignatureTime = source.readInt() != 0 ? new Date(source.readLong()) : null;          mMasterKeyId = source.readInt() != 0 ? source.readLong() : null;          mSubKeyId = source.readInt() != 0 ? source.readLong() : null; +        mSkipCaching = source.readInt() != 0;      } @@ -171,6 +174,7 @@ public class RequiredInputParcel implements Parcelable {          } else {              dest.writeInt(0);          } +        dest.writeInt(mSkipCaching ? 1 : 0);      } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java index 25601d655..3ff3bfbe3 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/BackupRestoreFragment.java @@ -36,6 +36,7 @@ import android.view.ViewGroup;  import org.sufficientlysecure.keychain.R;  import org.sufficientlysecure.keychain.pgp.CanonicalizedSecretKey.SecretKeyType;  import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel;  import org.sufficientlysecure.keychain.ui.util.Notify;  import org.sufficientlysecure.keychain.util.FileHelper; @@ -157,7 +158,10 @@ public class BackupRestoreFragment extends Fragment {          Intent intent = new Intent(activity, PassphraseDialogActivity.class);          long masterKeyId = mIdsForRepeatAskPassphrase.get(mIndex++); -        intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, masterKeyId); +        RequiredInputParcel requiredInput = +                RequiredInputParcel.createRequiredDecryptPassphrase(masterKeyId, masterKeyId); +        requiredInput.mSkipCaching = true; +        intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput);          startActivityForResult(intent, REQUEST_REPEAT_PASSPHRASE);      } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java index c3a33fc92..2fcbbf6c2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/PassphraseDialogActivity.java @@ -44,6 +44,7 @@ import android.widget.Button;  import android.widget.EditText;  import android.widget.TextView;  import android.widget.Toast; +import android.widget.ViewAnimator;  import org.sufficientlysecure.keychain.Constants;  import org.sufficientlysecure.keychain.R; @@ -60,8 +61,10 @@ 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; +import org.sufficientlysecure.keychain.service.input.RequiredInputParcel.RequiredInputType;  import org.sufficientlysecure.keychain.ui.dialog.CustomAlertDialogBuilder;  import org.sufficientlysecure.keychain.ui.util.ThemeChanger; +import org.sufficientlysecure.keychain.ui.widget.CacheTTLSpinner;  import org.sufficientlysecure.keychain.util.Log;  import org.sufficientlysecure.keychain.util.Passphrase;  import org.sufficientlysecure.keychain.util.Preferences; @@ -77,14 +80,10 @@ public class PassphraseDialogActivity extends FragmentActivity {      public static final String RESULT_CRYPTO_INPUT = "result_data";      public static final String EXTRA_REQUIRED_INPUT = "required_input"; -    public static final String EXTRA_SUBKEY_ID = "secret_key_id";      public static final String EXTRA_CRYPTO_INPUT = "crypto_input";      // special extra for OpenPgpService      public static final String EXTRA_SERVICE_INTENT = "data"; -    private long mSubKeyId; - -    private CryptoInputParcel mCryptoInputParcel;      @Override      protected void onCreate(Bundle savedInstanceState) { @@ -99,62 +98,37 @@ public class PassphraseDialogActivity extends FragmentActivity {              );          } -        mCryptoInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); - -        if (mCryptoInputParcel == null) { -            // not all usages of PassphraseActivity are from CryptoInputOperation -            // NOTE: This CryptoInputParcel cannot be used for signing operations without setting -            // signature time -            mCryptoInputParcel = new CryptoInputParcel(); +        CryptoInputParcel cryptoInputParcel = getIntent().getParcelableExtra(EXTRA_CRYPTO_INPUT); +        if (cryptoInputParcel == null) { +            cryptoInputParcel = new CryptoInputParcel(); +            getIntent().putExtra(EXTRA_CRYPTO_INPUT, cryptoInputParcel);          }          // this activity itself has no content view (see manifest) +        RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT); +        if (requiredInput.mType != RequiredInputType.PASSPHRASE) { +            return; +        } -        if (getIntent().hasExtra(EXTRA_SUBKEY_ID)) { -            mSubKeyId = getIntent().getLongExtra(EXTRA_SUBKEY_ID, 0); -        } else { -            RequiredInputParcel requiredInput = getIntent().getParcelableExtra(EXTRA_REQUIRED_INPUT); -            switch (requiredInput.mType) { -                case PASSPHRASE_SYMMETRIC: { -                    mSubKeyId = Constants.key.symmetric; -                    break; -                } -                case BACKUP_CODE: { -                    mSubKeyId = Constants.key.backup_code; -                    break; -                } -                case PASSPHRASE: { - -                    // handle empty passphrases by directly returning an empty crypto input parcel -                    try { -                        CanonicalizedSecretKeyRing pubRing = -                                new ProviderHelper(this).getCanonicalizedSecretKeyRing( -                                        requiredInput.getMasterKeyId()); -                        // use empty passphrase for empty passphrase -                        if (pubRing.getSecretKey(requiredInput.getSubKeyId()).getSecretKeyType() == -                                SecretKeyType.PASSPHRASE_EMPTY) { -                            // also return passphrase back to activity -                            Intent returnIntent = new Intent(); -                            mCryptoInputParcel.mPassphrase = new Passphrase(""); -                            returnIntent.putExtra(RESULT_CRYPTO_INPUT, mCryptoInputParcel); -                            setResult(RESULT_OK, returnIntent); -                            finish(); -                            return; -                        } -                    } catch (NotFoundException e) { -                        Log.e(Constants.TAG, "Key not found?!", e); -                        setResult(RESULT_CANCELED); -                        finish(); -                        return; -                    } - -                    mSubKeyId = requiredInput.getSubKeyId(); -                    break; -                } -                default: { -                    throw new AssertionError("Unsupported required input type for PassphraseDialogActivity!"); -                } +        // handle empty passphrases by directly returning an empty crypto input parcel +        try { +            CanonicalizedSecretKeyRing pubRing = +                    new ProviderHelper(this).getCanonicalizedSecretKeyRing( +                            requiredInput.getMasterKeyId()); +            // use empty passphrase for empty passphrase +            if (pubRing.getSecretKey(requiredInput.getSubKeyId()).getSecretKeyType() == +                    SecretKeyType.PASSPHRASE_EMPTY) { +                // also return passphrase back to activity +                Intent returnIntent = new Intent(); +                cryptoInputParcel.mPassphrase = new Passphrase(""); +                returnIntent.putExtra(RESULT_CRYPTO_INPUT, cryptoInputParcel); +                setResult(RESULT_OK, returnIntent); +                finish();              } +        } catch (NotFoundException e) { +            Log.e(Constants.TAG, "Key not found?!", e); +            setResult(RESULT_CANCELED); +            finish();          }      } @@ -167,14 +141,8 @@ public class PassphraseDialogActivity extends FragmentActivity {           * encryption. Based on mSecretKeyId it asks for a passphrase to open a private key or it asks           * for a symmetric passphrase           */ - -        Intent serviceIntent = getIntent().getParcelableExtra(EXTRA_SERVICE_INTENT); -          PassphraseDialogFragment frag = new PassphraseDialogFragment(); -        Bundle args = new Bundle(); -        args.putLong(EXTRA_SUBKEY_ID, mSubKeyId); -        args.putParcelable(EXTRA_SERVICE_INTENT, serviceIntent); -        frag.setArguments(args); +        frag.setArguments(getIntent().getExtras());          frag.show(getSupportFragmentManager(), "passphraseDialog");      } @@ -191,14 +159,14 @@ public class PassphraseDialogActivity extends FragmentActivity {      public static class PassphraseDialogFragment extends DialogFragment implements TextView.OnEditorActionListener {          private EditText mPassphraseEditText;          private TextView mPassphraseText; -        private View mInput, mProgress;          private EditText[] mBackupCodeEditText;          private CanonicalizedSecretKeyRing mSecretRing = null;          private boolean mIsCancelled = false; -        private long mSubKeyId; +        private RequiredInputParcel mRequiredInput; -        private Intent mServiceIntent; +        private ViewAnimator mLayout; +        private CacheTTLSpinner mTimeToLiveSpinner;          @NonNull          @Override @@ -207,15 +175,14 @@ public class PassphraseDialogActivity extends FragmentActivity {              ContextThemeWrapper theme = ThemeChanger.getDialogThemeWrapper(activity); -            mSubKeyId = getArguments().getLong(EXTRA_SUBKEY_ID); -            mServiceIntent = getArguments().getParcelable(EXTRA_SERVICE_INTENT); +            mRequiredInput = getArguments().getParcelable(EXTRA_REQUIRED_INPUT);              CustomAlertDialogBuilder alert = new CustomAlertDialogBuilder(theme);              // No title, see http://www.google.com/design/spec/components/dialogs.html#dialogs-alerts              //alert.setTitle() -            if (mSubKeyId == Constants.key.backup_code) { +            if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {                  LayoutInflater inflater = LayoutInflater.from(theme);                  View view = inflater.inflate(R.layout.passphrase_dialog_backup_code, null);                  alert.setView(view); @@ -233,14 +200,19 @@ public class PassphraseDialogActivity extends FragmentActivity {                  return dialog;              } +            long subKeyId = mRequiredInput.getSubKeyId(); +              LayoutInflater inflater = LayoutInflater.from(theme); -            View view = inflater.inflate(R.layout.passphrase_dialog, null); -            alert.setView(view); +            mLayout = (ViewAnimator) inflater.inflate(R.layout.passphrase_dialog, null); +            alert.setView(mLayout); + +            mPassphraseText = (TextView) mLayout.findViewById(R.id.passphrase_text); +            mPassphraseEditText = (EditText) mLayout.findViewById(R.id.passphrase_passphrase); + +            View vTimeToLiveLayout = mLayout.findViewById(R.id.remember_layout); +            vTimeToLiveLayout.setVisibility(mRequiredInput.mSkipCaching ? View.GONE : View.VISIBLE); -            mPassphraseText = (TextView) view.findViewById(R.id.passphrase_text); -            mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); -            mInput = view.findViewById(R.id.input); -            mProgress = view.findViewById(R.id.progress); +            mTimeToLiveSpinner = (CacheTTLSpinner) mLayout.findViewById(R.id.ttl_spinner);              alert.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @@ -255,14 +227,14 @@ public class PassphraseDialogActivity extends FragmentActivity {              String message;              String hint; -            if (mSubKeyId == Constants.key.symmetric || mSubKeyId == Constants.key.none) { +            if (mRequiredInput.mType == RequiredInputType.PASSPHRASE_SYMMETRIC) {                  message = getString(R.string.passphrase_for_symmetric_encryption);                  hint = getString(R.string.label_passphrase);              } else {                  try {                      ProviderHelper helper = new ProviderHelper(activity);                      mSecretRing = helper.getCanonicalizedSecretKeyRing( -                            KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(mSubKeyId)); +                            KeychainContract.KeyRings.buildUnifiedKeyRingsFindBySubkeyUri(subKeyId));                      // yes the inner try/catch block is necessary, otherwise the final variable                      // above can't be statically verified to have been set in all cases because                      // the catch clause doesn't return. @@ -278,7 +250,7 @@ public class PassphraseDialogActivity extends FragmentActivity {                          userId = null;                      } -                    keyType = mSecretRing.getSecretKey(mSubKeyId).getSecretKeyType(); +                    keyType = mSecretRing.getSecretKey(subKeyId).getSecretKeyType();                      switch (keyType) {                          case PASSPHRASE:                              message = getString(R.string.passphrase_for, userId); @@ -301,7 +273,7 @@ public class PassphraseDialogActivity extends FragmentActivity {                  } catch (ProviderHelper.NotFoundException e) {                      alert.setTitle(R.string.title_key_not_found); -                    alert.setMessage(getString(R.string.key_not_found, mSubKeyId)); +                    alert.setMessage(getString(R.string.key_not_found, mRequiredInput.getSubKeyId()));                      alert.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {                          public void onClick(DialogInterface dialog, int which) {                              dismiss(); @@ -397,7 +369,7 @@ public class PassphraseDialogActivity extends FragmentActivity {                  public void onClick(View v) {                      final Passphrase passphrase; -                    if (mSubKeyId == Constants.key.backup_code) { +                    if (mRequiredInput.mType == RequiredInputType.BACKUP_CODE) {                          StringBuilder backupCodeInput = new StringBuilder(26);                          for (EditText editText : mBackupCodeEditText) {                              if (editText.getText().length() < 6) { @@ -413,23 +385,21 @@ public class PassphraseDialogActivity extends FragmentActivity {                          passphrase = new Passphrase(mPassphraseEditText);                      } -                    CryptoInputParcel cryptoInputParcel = -                            ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; +                    final int timeToLiveSeconds = mTimeToLiveSpinner.getSelectedTimeToLive();                      // Early breakout if we are dealing with a symmetric key                      if (mSecretRing == null) { -                        if (cryptoInputParcel.mCachePassphrase) { +                        if ( ! mRequiredInput.mSkipCaching) {                              PassphraseCacheService.addCachedPassphrase(getActivity(),                                      Constants.key.symmetric, Constants.key.symmetric, passphrase, -                                    getString(R.string.passp_cache_notif_pwd)); +                                    getString(R.string.passp_cache_notif_pwd), timeToLiveSeconds);                          }                          finishCaching(passphrase);                          return;                      } -                    mInput.setVisibility(View.INVISIBLE); -                    mProgress.setVisibility(View.VISIBLE); +                    mLayout.setDisplayedChild(1);                      positive.setEnabled(false);                      new AsyncTask<Void, Void, Boolean>() { @@ -443,7 +413,7 @@ public class PassphraseDialogActivity extends FragmentActivity {                                      // never mind                                  }                                  // make sure this unlocks -                                return mSecretRing.getSecretKey(mSubKeyId).unlock(passphrase); +                                return mSecretRing.getSecretKey(mRequiredInput.getSubKeyId()).unlock(passphrase);                              } catch (PgpGeneralException e) {                                  Toast.makeText(getActivity(), R.string.error_could_not_extract_private_key,                                          Toast.LENGTH_SHORT).show(); @@ -469,8 +439,7 @@ public class PassphraseDialogActivity extends FragmentActivity {                              if (!result) {                                  mPassphraseEditText.setText("");                                  mPassphraseEditText.setError(getString(R.string.wrong_passphrase)); -                                mInput.setVisibility(View.VISIBLE); -                                mProgress.setVisibility(View.INVISIBLE); +                                mLayout.setDisplayedChild(0);                                  positive.setEnabled(true);                                  return;                              } @@ -478,21 +447,18 @@ public class PassphraseDialogActivity extends FragmentActivity {                              // cache the new passphrase as specified in CryptoInputParcel                              Log.d(Constants.TAG, "Everything okay!"); -                            CryptoInputParcel cryptoInputParcel -                                    = ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; - -                            if (cryptoInputParcel.mCachePassphrase) { +                            if (mRequiredInput.mSkipCaching) { +                                Log.d(Constants.TAG, "Not caching entered passphrase!"); +                            } else {                                  Log.d(Constants.TAG, "Caching entered passphrase");                                  try {                                      PassphraseCacheService.addCachedPassphrase(getActivity(), -                                            mSecretRing.getMasterKeyId(), mSubKeyId, passphrase, -                                            mSecretRing.getPrimaryUserIdWithFallback()); +                                            mSecretRing.getMasterKeyId(), mRequiredInput.getSubKeyId(), passphrase, +                                            mSecretRing.getPrimaryUserIdWithFallback(), timeToLiveSeconds);                                  } catch (PgpKeyNotFoundException e) {                                      Log.e(Constants.TAG, "adding of a passphrase failed", e);                                  } -                            } else { -                                Log.d(Constants.TAG, "Not caching entered passphrase!");                              }                              finishCaching(passphrase); @@ -508,13 +474,14 @@ public class PassphraseDialogActivity extends FragmentActivity {                  return;              } -            CryptoInputParcel inputParcel = -                    ((PassphraseDialogActivity) getActivity()).mCryptoInputParcel; +            CryptoInputParcel inputParcel = getArguments().getParcelable(EXTRA_CRYPTO_INPUT); +            // noinspection ConstantConditions, we handle the non-null case in PassphraseDialogActivity.onCreate()              inputParcel.mPassphrase = passphrase; -            if (mServiceIntent != null) { -                CryptoInputParcelCacheService.addCryptoInputParcel(getActivity(), mServiceIntent, -                        inputParcel); -                getActivity().setResult(RESULT_OK, mServiceIntent); + +            Intent serviceIntent = getArguments().getParcelable(EXTRA_SERVICE_INTENT); +            if (serviceIntent != null) { +                CryptoInputParcelCacheService.addCryptoInputParcel(getActivity(), serviceIntent, inputParcel); +                getActivity().setResult(RESULT_OK, serviceIntent);              } else {                  // also return passphrase back to activity                  Intent returnIntent = new Intent(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java index cd754d60e..7666a230a 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsActivity.java @@ -18,6 +18,9 @@  package org.sufficientlysecure.keychain.ui; + +import java.util.List; +  import android.Manifest;  import android.accounts.Account;  import android.accounts.AccountManager; @@ -29,6 +32,7 @@ import android.content.Intent;  import android.content.pm.PackageManager;  import android.os.Build;  import android.os.Bundle; +import android.preference.CheckBoxPreference;  import android.preference.EditTextPreference;  import android.preference.ListPreference;  import android.preference.Preference; @@ -49,13 +53,10 @@ import org.sufficientlysecure.keychain.R;  import org.sufficientlysecure.keychain.compatibility.AppCompatPreferenceActivity;  import org.sufficientlysecure.keychain.ui.util.Notify;  import org.sufficientlysecure.keychain.ui.util.ThemeChanger; -import org.sufficientlysecure.keychain.ui.widget.IntegerListPreference;  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; @@ -103,6 +104,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {          Toolbar toolbar = (Toolbar) toolbarContainer.findViewById(R.id.toolbar);          toolbar.setTitle(R.string.title_preferences); +        // noinspection deprecation, TODO use alternative in API level 21          toolbar.setNavigationIcon(getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp));          toolbar.setNavigationOnClickListener(new View.OnClickListener() {              @Override @@ -195,23 +197,19 @@ public class SettingsActivity extends AppCompatPreferenceActivity {              // Load the preferences from an XML resource              addPreferencesFromResource(R.xml.passphrase_preferences); -            initializePassphraseCacheTtl( -                    (IntegerListPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_TTL)); -        } - -        private static void initializePassphraseCacheTtl( -                final IntegerListPreference passphraseCacheTtl) { -            passphraseCacheTtl.setValue("" + sPreferences.getPassphraseCacheTtl()); -            passphraseCacheTtl.setSummary(passphraseCacheTtl.getEntry()); -            passphraseCacheTtl -                    .setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { -                        public boolean onPreferenceChange(Preference preference, Object newValue) { -                            passphraseCacheTtl.setValue(newValue.toString()); -                            passphraseCacheTtl.setSummary(passphraseCacheTtl.getEntry()); -                            sPreferences.setPassphraseCacheTtl(Integer.parseInt(newValue.toString())); +            findPreference(Constants.Pref.PASSPHRASE_CACHE_TTLS) +                    .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { +                        public boolean onPreferenceClick(Preference preference) { +                            Intent intent = new Intent(getActivity(), SettingsCacheTTLActivity.class); +                            intent.putExtra(SettingsCacheTTLActivity.EXTRA_TTL_PREF, +                                    sPreferences.getPassphraseCacheTtl()); +                            startActivity(intent);                              return false;                          }                      }); + +            initializePassphraseCacheSubs( +                    (CheckBoxPreference) findPreference(Constants.Pref.PASSPHRASE_CACHE_SUBS));          }      } @@ -580,4 +578,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {                  || ExperimentalPrefsFragment.class.getName().equals(fragmentName)                  || super.isValidFragment(fragmentName);      } + +    private static void initializePassphraseCacheSubs(final CheckBoxPreference mPassphraseCacheSubs) { +        mPassphraseCacheSubs.setChecked(sPreferences.getPassphraseCacheSubs()); +        mPassphraseCacheSubs.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { +            public boolean onPreferenceChange(Preference preference, Object newValue) { +                mPassphraseCacheSubs.setChecked((Boolean) newValue); +                sPreferences.setPassphraseCacheSubs((Boolean) newValue); +                return false; +            } +        }); +    }  } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLActivity.java new file mode 100644 index 000000000..6c3d0fd1c --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLActivity.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@my.amazin.horse> + * + * 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 java.util.ArrayList; + +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.base.BaseActivity; +import org.sufficientlysecure.keychain.util.Preferences.CacheTTLPrefs; + + +public class SettingsCacheTTLActivity extends BaseActivity { + +    public static final String EXTRA_TTL_PREF = "ttl_pref"; + +    @Override +    public void onCreate(Bundle savedInstanceState) { +        super.onCreate(savedInstanceState); + +        Intent intent = getIntent(); +        CacheTTLPrefs ttlPrefs = (CacheTTLPrefs) intent.getSerializableExtra(EXTRA_TTL_PREF); +        loadFragment(savedInstanceState, ttlPrefs); + +        getSupportActionBar().setDisplayHomeAsUpEnabled(true); + +    } + +    @Override +    public boolean onOptionsItemSelected(MenuItem item) { +        switch (item.getItemId()) { +            case android.R.id.home: +                finish(); +                return true; +        } +        return super.onOptionsItemSelected(item); +    } + +    @Override +    protected void initLayout() { +        setContentView(R.layout.settings_cache_ttl); +    } + +    private void loadFragment(Bundle savedInstanceState, CacheTTLPrefs ttlPrefs) { +        // However, if we're being restored from a previous state, +        // then we don't need to do anything and should return or else +        // we could end up with overlapping fragments. +        if (savedInstanceState != null) { +            return; +        } + +        SettingsCacheTTLFragment fragment = SettingsCacheTTLFragment.newInstance(ttlPrefs); + +        // Add the fragment to the 'fragment_container' FrameLayout +        // NOTE: We use commitAllowingStateLoss() to prevent weird crashes! +        getSupportFragmentManager().beginTransaction() +                .replace(R.id.settings_cache_ttl_fragment, fragment) +                .commitAllowingStateLoss(); +        // do it immediately! +        getSupportFragmentManager().executePendingTransactions(); +    } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java new file mode 100644 index 000000000..1c769e7a2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/SettingsCacheTTLFragment.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@my.amazin.horse> + * + * 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 java.util.ArrayList; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.util.Notify; +import org.sufficientlysecure.keychain.ui.util.Notify.Style; +import org.sufficientlysecure.keychain.ui.util.recyclerview.DividerItemDecoration; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.Preferences.CacheTTLPrefs; + + +public class SettingsCacheTTLFragment extends Fragment { + +    public static final String ARG_TTL_PREFS = "ttl_prefs"; + +    private CacheTTLListAdapter mAdapter; + +    public static SettingsCacheTTLFragment newInstance(CacheTTLPrefs ttlPrefs) { +        Bundle args = new Bundle(); +        args.putSerializable(ARG_TTL_PREFS, ttlPrefs); + +        SettingsCacheTTLFragment fragment = new SettingsCacheTTLFragment(); +        fragment.setArguments(args); + +        return fragment; +    } + +    @Override +    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle +            savedInstanceState) { + +        return inflater.inflate(R.layout.settings_cache_ttl_fragment, null); +    } + +    @Override +    public void onViewCreated(View view, Bundle savedInstanceState) { +        super.onViewCreated(view, savedInstanceState); + +        CacheTTLPrefs prefs = (CacheTTLPrefs) getArguments().getSerializable(ARG_TTL_PREFS); + +        mAdapter = new CacheTTLListAdapter(prefs); + +        RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.cache_ttl_recycler_view); +        recyclerView.setHasFixedSize(true); +        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); +        recyclerView.setAdapter(mAdapter); +        recyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), DividerItemDecoration.VERTICAL_LIST)); + + +    } + +    private void savePreference() { +        FragmentActivity activity = getActivity(); +        if (activity == null) { +            return; +        } +        CacheTTLPrefs prefs = mAdapter.getPrefs(); +        Preferences.getPreferences(activity).setPassphraseCacheTtl(prefs); +    } + +    public class CacheTTLListAdapter extends RecyclerView.Adapter<CacheTTLListAdapter.ViewHolder> { + +        private final ArrayList<Boolean> mPositionIsChecked; + +        public CacheTTLListAdapter(CacheTTLPrefs prefs) { +            this.mPositionIsChecked = new ArrayList<>(); +            for (int ttlTime : CacheTTLPrefs.CACHE_TTLS) { +                mPositionIsChecked.add(prefs.ttlTimes.contains(ttlTime)); +            } + +        } + +        public CacheTTLPrefs getPrefs() { +            ArrayList<String> ttls = new ArrayList<>(); +            for (int i = 0; i < mPositionIsChecked.size(); i++) { +                if (mPositionIsChecked.get(i)) { +                    ttls.add(Integer.toString(CacheTTLPrefs.CACHE_TTLS.get(i))); +                } +            } +            return new CacheTTLPrefs(ttls); +        } + +        @Override +        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { +            View view = LayoutInflater.from(parent.getContext()) +                    .inflate(R.layout.settings_cache_ttl_item, parent, false); +            return new ViewHolder(view); +        } + +        @Override +        public void onBindViewHolder(final ViewHolder holder, int position) { +            holder.bind(position); +        } + +        @Override +        public int getItemCount() { +            return mPositionIsChecked.size(); +        } + +        public class ViewHolder extends RecyclerView.ViewHolder { + +            CheckBox mChecked; +            TextView mTitle; + +            public ViewHolder(View itemView) { +                super(itemView); +                mChecked = (CheckBox) itemView.findViewById(R.id.ttl_selected); +                mTitle = (TextView) itemView.findViewById(R.id.ttl_title); + +                itemView.setOnClickListener(new OnClickListener() { +                    @Override +                    public void onClick(View v) { +                        mChecked.performClick(); +                    } +                }); +            } + +            public void bind(final int position) { + +                int ttl = CacheTTLPrefs.CACHE_TTLS.get(position); +                boolean isChecked = mPositionIsChecked.get(position); + +                mTitle.setText(CacheTTLPrefs.CACHE_TTL_NAMES.get(ttl)); +                // avoid some ui flicker by skipping unnecessary updates +                if (mChecked.isChecked() != isChecked) { +                    mChecked.setChecked(isChecked); +                } + +                mChecked.setOnClickListener(new OnClickListener() { +                    @Override +                    public void onClick(View v) { +                        setTtlChecked(position); +                        savePreference(); +                    } +                }); + +            } + +            private void setTtlChecked(int position) { +                boolean isChecked = mPositionIsChecked.get(position); +                int checkedItems = countCheckedItems(); + +                boolean isLastChecked = isChecked && checkedItems == 1; +                boolean isOneTooMany = !isChecked && checkedItems >= 3; +                if (isLastChecked) { +                    Notify.create(getActivity(), R.string.settings_cache_ttl_at_least_one, Style.ERROR).show(); +                } else if (isOneTooMany) { +                    Notify.create(getActivity(), R.string.settings_cache_ttl_max_three, Style.ERROR).show(); +                } else { +                    mPositionIsChecked.set(position, !isChecked); +                } +                notifyItemChanged(position); +            } + +            private int countCheckedItems() { +                int result = 0; +                for (boolean isChecked : mPositionIsChecked) { +                    if (isChecked) { +                        result += 1; +                    } +                } +                return result; +            } + +        } + +    } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index e379831c1..67487f2ca 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -82,6 +82,7 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper;  import org.sufficientlysecure.keychain.provider.ProviderHelper.NotFoundException;  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.BaseNfcActivity;  import org.sufficientlysecure.keychain.ui.base.CryptoOperationHelper; @@ -531,7 +532,10 @@ public class ViewKeyActivity extends BaseNfcActivity implements          if (keyHasPassphrase()) {              Intent intent = new Intent(this, PassphraseDialogActivity.class); -            intent.putExtra(PassphraseDialogActivity.EXTRA_SUBKEY_ID, mMasterKeyId); +            RequiredInputParcel requiredInput = +                    RequiredInputParcel.createRequiredDecryptPassphrase(mMasterKeyId, mMasterKeyId); +            requiredInput.mSkipCaching = true; +            intent.putExtra(PassphraseDialogActivity.EXTRA_REQUIRED_INPUT, requiredInput);              startActivityForResult(intent, requestCode);          } else {              startBackupActivity(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CacheTTLSpinner.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CacheTTLSpinner.java new file mode 100644 index 000000000..efaabbad2 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/widget/CacheTTLSpinner.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 Vincent Breitmoser <v.breitmoser@my.amazin.horse> + * + * 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.widget; + + +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.support.v4.widget.SimpleCursorAdapter; +import android.support.v7.widget.AppCompatSpinner; +import android.util.AttributeSet; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Preferences; +import org.sufficientlysecure.keychain.util.Preferences.CacheTTLPrefs; + + +public class CacheTTLSpinner extends AppCompatSpinner { + +    public CacheTTLSpinner(Context context, AttributeSet attrs) { +        super(context, attrs); +        initView(context); +    } + +    public CacheTTLSpinner(Context context, AttributeSet attrs, int defStyle) { +        super(context, attrs, defStyle); +        initView(context); +    } + +    private void initView(Context context) { + +        CacheTTLPrefs prefs = Preferences.getPreferences(context).getPassphraseCacheTtl(); +        MatrixCursor  cursor = new MatrixCursor(new String[] { "_id", "TTL", "description" }, 5); +        int i = 0; +        for (int ttl : CacheTTLPrefs.CACHE_TTLS) { +            if ( ! prefs.ttlTimes.contains(ttl)) { +                continue; +            } +            cursor.addRow(new Object[] { i++, ttl, getContext().getString(CacheTTLPrefs.CACHE_TTL_NAMES.get(ttl)) }); +        } + +        setAdapter(new SimpleCursorAdapter(getContext(), R.layout.simple_item, cursor, +                new String[] { "description" }, new int[] { R.id.simple_item_text }, 0)); +    } + +    public int getSelectedTimeToLive() { +        int selectedItemPosition = getSelectedItemPosition(); +        Object item = getAdapter().getItem(selectedItemPosition); +        return ((Cursor) item).getInt(1); +    } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java index ce81bbcac..75a7dcaba 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/Preferences.java @@ -18,9 +18,23 @@  package org.sufficientlysecure.keychain.util; + +import java.io.Serializable; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.Vector; + +import android.annotation.SuppressLint;  import android.content.Context;  import android.content.SharedPreferences; -  import android.os.Parcel;  import android.os.Parcelable;  import android.preference.PreferenceManager; @@ -28,17 +42,13 @@ import android.support.annotation.NonNull;  import org.sufficientlysecure.keychain.Constants;  import org.sufficientlysecure.keychain.Constants.Pref; +import org.sufficientlysecure.keychain.R;  import org.sufficientlysecure.keychain.service.KeyserverSyncAdapterService; -import java.net.Proxy; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.ListIterator; -import java.util.Vector; -  /**   * Singleton Implementation of a Preference Helper   */ +@SuppressLint("CommitPrefEdits")  public class Preferences {      private static Preferences sPreferences;      private SharedPreferences mSharedPreferences; @@ -90,22 +100,6 @@ public class Preferences {          editor.commit();      } -    public long getPassphraseCacheTtl() { -        int ttl = mSharedPreferences.getInt(Constants.Pref.PASSPHRASE_CACHE_TTL, 180); -        // fix the value if it was set to "never" in previous versions, which currently is not -        // supported -        if (ttl == 0) { -            ttl = 180; -        } -        return (long) ttl; -    } - -    public void setPassphraseCacheTtl(int value) { -        SharedPreferences.Editor editor = mSharedPreferences.edit(); -        editor.putInt(Constants.Pref.PASSPHRASE_CACHE_TTL, value); -        editor.commit(); -    } -      public boolean getPassphraseCacheSubs() {          return mSharedPreferences.getBoolean(Pref.PASSPHRASE_CACHE_SUBS, false);      } @@ -143,7 +137,7 @@ public class Preferences {      public String[] getKeyServers() {          String rawData = mSharedPreferences.getString(Constants.Pref.KEY_SERVERS,                  Constants.Defaults.KEY_SERVERS); -        if (rawData.equals("")) { +        if ("".equals(rawData)) {              return new String[0];          }          Vector<String> servers = new Vector<>(); @@ -308,6 +302,64 @@ public class Preferences {      } +    public CacheTTLPrefs getPassphraseCacheTtl() { +        Set<String> pref = mSharedPreferences.getStringSet(Constants.Pref.PASSPHRASE_CACHE_TTLS, null); +        if (pref == null) { +            return CacheTTLPrefs.getDefault(); +        } +        return new CacheTTLPrefs(pref); +    } + +    public void setPassphraseCacheTtl(CacheTTLPrefs prefs) { +        SharedPreferences.Editor editor = mSharedPreferences.edit(); +        editor.putStringSet(Constants.Pref.PASSPHRASE_CACHE_TTLS, prefs.getStringSet()); +        editor.commit(); +    } + +    public static class CacheTTLPrefs implements Serializable { +        public static final Map<Integer,Integer> CACHE_TTL_NAMES; +        public static final ArrayList<Integer> CACHE_TTLS; +        static { +            HashMap<Integer,Integer> cacheTtlNames = new HashMap<>(); +            cacheTtlNames.put(0, R.string.cache_ttl_lock_screen); +            cacheTtlNames.put(60 * 5, R.string.cache_ttl_five_minutes); +            cacheTtlNames.put(60 * 60, R.string.cache_ttl_one_hour); +            cacheTtlNames.put(60 * 60 * 3, R.string.cache_ttl_three_hours); +            cacheTtlNames.put(60 * 60 * 24, R.string.cache_ttl_one_day); +            cacheTtlNames.put(60 * 60 * 24 * 3, R.string.cache_ttl_three_days); +            CACHE_TTL_NAMES = Collections.unmodifiableMap(cacheTtlNames); + +            CACHE_TTLS = new ArrayList<>(CacheTTLPrefs.CACHE_TTL_NAMES.keySet()); +            Collections.sort(CACHE_TTLS); +        } + +        public HashSet<Integer> ttlTimes; + +        public CacheTTLPrefs(Collection<String> ttlStrings) { +            ttlTimes = new HashSet<>(); +            for (String ttlString : ttlStrings) { +                ttlTimes.add(Integer.parseInt(ttlString)); +            } +        } + +        public HashSet<String> getStringSet() { +            HashSet<String> ttlTimeStrings = new HashSet<>(); +            for (Integer ttlTime : ttlTimes) { +                ttlTimeStrings.add(Integer.toString(ttlTime)); +            } +            return ttlTimeStrings; +        } + +        public static CacheTTLPrefs getDefault() { +            ArrayList<String> ttlStrings = new ArrayList<>(); +            ttlStrings.add(Integer.toString(60 * 5)); +            ttlStrings.add(Integer.toString(60 * 60)); +            ttlStrings.add(Integer.toString(60 * 60 * 24)); +            return new CacheTTLPrefs(ttlStrings); +        } + +    } +      // cloud prefs      public CloudSearchPrefs getCloudSearchPrefs() { @@ -402,15 +454,19 @@ public class Preferences {                          if (server == null) {                              continue;                          } -                        if (server.equals("pool.sks-keyservers.net")) { -                            // use HKPS! -                            it.set("hkps://hkps.pool.sks-keyservers.net"); -                        } else if (server.equals("pgp.mit.edu")) { -                            // use HKPS! -                            it.set("hkps://pgp.mit.edu"); -                        } else if (server.equals("subkeys.pgp.net")) { -                            // remove, because often down and no HKPS! -                            it.remove(); +                        switch (server) { +                            case "pool.sks-keyservers.net": +                                // use HKPS! +                                it.set("hkps://hkps.pool.sks-keyservers.net"); +                                break; +                            case "pgp.mit.edu": +                                // use HKPS! +                                it.set("hkps://pgp.mit.edu"); +                                break; +                            case "subkeys.pgp.net": +                                // remove, because often down and no HKPS! +                                it.remove(); +                                break;                          }                      } diff --git a/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml b/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml index ab1bf3d4a..3245fd28b 100644 --- a/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml +++ b/OpenKeychain/src/main/res/layout/encrypt_asymmetric_fragment.xml @@ -91,6 +91,7 @@                  android:layout_width="wrap_content"                  android:layout_height="wrap_content"                  android:textAppearance="?android:attr/textAppearanceMedium" +                android:textColor="@color/md_black_1000"                  android:text="@string/label_asymmetric_from"                  android:paddingRight="8dp"/> diff --git a/OpenKeychain/src/main/res/layout/passphrase_dialog.xml b/OpenKeychain/src/main/res/layout/passphrase_dialog.xml index a2e6af27c..7252effad 100644 --- a/OpenKeychain/src/main/res/layout/passphrase_dialog.xml +++ b/OpenKeychain/src/main/res/layout/passphrase_dialog.xml @@ -1,15 +1,17 @@  <?xml version="1.0" encoding="utf-8"?> -<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" +<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator +    xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:custom="http://schemas.android.com/apk/res-auto"      android:layout_width="match_parent"      android:layout_height="wrap_content" +    xmlns:tools="http://schemas.android.com/tools"      android:paddingTop="16dp" -    android:paddingBottom="16dp"      android:paddingLeft="24dp"      android:paddingRight="24dp" -    android:orientation="vertical"> +    android:orientation="vertical" +    custom:initialView="0">      <LinearLayout -        android:id="@+id/input"          android:layout_width="match_parent"          android:layout_height="wrap_content"          android:orientation="vertical"> @@ -31,10 +33,33 @@              android:ems="10"              android:layout_gravity="center_horizontal" /> +        <LinearLayout +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_gravity="center" +            android:gravity="center_vertical" +            android:layout_margin="8dp" +            android:id="@+id/remember_layout"> + +            <!-- paddingBottom for spinner alignment --> +            <TextView +                android:layout_width="wrap_content" +                android:layout_height="wrap_content" +                android:text="@string/remember" +                android:paddingBottom="1dp" +                android:textAppearance="@android:style/TextAppearance.Medium" /> + +            <org.sufficientlysecure.keychain.ui.widget.CacheTTLSpinner +                android:id="@+id/ttl_spinner" +                android:layout_width="wrap_content" +                android:layout_height="wrap_content"> +            </org.sufficientlysecure.keychain.ui.widget.CacheTTLSpinner> + +        </LinearLayout> +      </LinearLayout>      <LinearLayout -        android:id="@+id/progress"          android:layout_centerInParent="true"          android:layout_width="match_parent"          android:layout_height="wrap_content" @@ -57,4 +82,4 @@      </LinearLayout> -</RelativeLayout>
\ No newline at end of file +</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/settings_cache_ttl.xml b/OpenKeychain/src/main/res/layout/settings_cache_ttl.xml new file mode 100644 index 000000000..25ac33c5c --- /dev/null +++ b/OpenKeychain/src/main/res/layout/settings_cache_ttl.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:orientation="vertical"> + +    <include +        android:id="@+id/toolbar_include" +        layout="@layout/toolbar_standalone" /> + +    <LinearLayout +        android:layout_width="match_parent" +        android:layout_height="wrap_content" +        android:orientation="horizontal"> + +        <TextView +            android:layout_width="wrap_content" +            android:layout_height="wrap_content" +            android:layout_margin="8dp" +            android:textAppearance="?android:attr/textAppearanceMedium" +            android:text="@string/settings_cache_select_three"/> + +    </LinearLayout> + +    <View style="@style/Divider"/> + +    <FrameLayout +        android:id="@+id/settings_cache_ttl_fragment" +        android:layout_width="match_parent" +        android:layout_height="match_parent" +        android:orientation="vertical" /> + +</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/settings_cache_ttl_fragment.xml b/OpenKeychain/src/main/res/layout/settings_cache_ttl_fragment.xml new file mode 100644 index 000000000..4a34bc5bc --- /dev/null +++ b/OpenKeychain/src/main/res/layout/settings_cache_ttl_fragment.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> + +<android.support.v7.widget.RecyclerView +        xmlns:android="http://schemas.android.com/apk/res/android" +        android:id="@+id/cache_ttl_recycler_view" +        android:layout_width="match_parent" +        android:layout_height="match_parent" />
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/settings_cache_ttl_item.xml b/OpenKeychain/src/main/res/layout/settings_cache_ttl_item.xml new file mode 100644 index 000000000..c0470288f --- /dev/null +++ b/OpenKeychain/src/main/res/layout/settings_cache_ttl_item.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout +    xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:tools="http://schemas.android.com/tools" +    android:layout_width="match_parent" +    android:layout_height="wrap_content" +    android:minHeight="?listPreferredItemHeight" +    android:orientation="horizontal" +    android:gravity="center_vertical" +    android:background="?selectableItemBackground"> + +    <CheckBox +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:id="@+id/ttl_selected" +        android:layout_margin="8dp" +        /> + +    <TextView +        android:layout_width="0dp" +        android:layout_height="wrap_content" +        android:id="@+id/ttl_title" +        android:layout_weight="1" +        android:textAppearance="?android:attr/textAppearanceMedium" +        android:textStyle="bold" +        tools:text="One Hour" +        /> + +</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/simple_item.xml b/OpenKeychain/src/main/res/layout/simple_item.xml new file mode 100644 index 000000000..45033a6e0 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/simple_item.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout +    xmlns:android="http://schemas.android.com/apk/res/android" +    xmlns:tools="http://schemas.android.com/tools" +    android:orientation="vertical" +    android:layout_width="match_parent" +    android:layout_height="match_parent" +    android:gravity="center_vertical"> + +    <TextView +        android:layout_width="wrap_content" +        android:layout_height="wrap_content" +        android:id="@+id/simple_item_text" +        android:textAppearance="@android:style/TextAppearance.Medium" +        android:padding="8dp" +        tools:text="itemtext" /> +</LinearLayout>
\ No newline at end of file diff --git a/OpenKeychain/src/main/res/values/arrays.xml b/OpenKeychain/src/main/res/values/arrays.xml index 393a1e091..584d773b7 100644 --- a/OpenKeychain/src/main/res/values/arrays.xml +++ b/OpenKeychain/src/main/res/values/arrays.xml @@ -1,34 +1,6 @@  <?xml version="1.0" encoding="utf-8"?>  <resources> -    <string-array name="passphrase_cache_ttl_entries" translatable="false"> -        <item>@string/choice_15secs</item> -        <item>@string/choice_1min</item> -        <item>@string/choice_3mins</item> -        <item>@string/choice_5mins</item> -        <item>@string/choice_10mins</item> -        <item>@string/choice_20mins</item> -        <item>@string/choice_40mins</item> -        <item>@string/choice_1hour</item> -        <item>@string/choice_2hours</item> -        <item>@string/choice_4hours</item> -        <item>@string/choice_8hours</item> -        <item>@string/choice_forever</item> -    </string-array> -    <string-array name="passphrase_cache_ttl_values" translatable="false"> -        <item>15</item> -        <item>60</item> -        <item>180</item> -        <item>300</item> -        <item>600</item> -        <item>1200</item> -        <item>2400</item> -        <item>3600</item> -        <item>7200</item> -        <item>14400</item> -        <item>28800</item> -        <item>-1</item> -    </string-array>      <string-array name="pref_proxy_type_entries" translatable="false">          <item>@string/pref_proxy_type_choice_http</item>          <item>@string/pref_proxy_type_choice_socks</item> diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index afda3c3c3..3c77d82dd 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -18,6 +18,7 @@      <string name="title_preferences">"Settings"</string>      <string name="title_api_registered_apps">"Apps"</string>      <string name="title_key_server_preference">"OpenPGP keyservers"</string> +    <string name="title_cache_ttl_preference">"Customize 'Remember' Choices"</string>      <string name="title_change_passphrase">"Change Password"</string>      <string name="title_share_fingerprint_with">"Share fingerprint with…"</string>      <string name="title_share_key">"Share key with…"</string> @@ -157,7 +158,7 @@      <string name="label_encryption_algorithm">"Encryption algorithm"</string>      <string name="label_hash_algorithm">"Hash algorithm"</string>      <string name="label_symmetric">"Encrypt with password"</string> -    <string name="label_passphrase_cache_ttl">"Remember time"</string> +    <string name="label_passphrase_cache_ttl">"Customize 'Remember' Choices"</string>      <string name="label_passphrase_cache_subs">"Remember passwords by subkey"</string>      <string name="label_message_compression">"Text compression"</string>      <string name="label_file_compression">"File compression"</string> @@ -1709,5 +1710,15 @@      <string name="title_edit_identities">"Edit Identities"</string>      <string name="title_edit_subkeys">"Edit Subkeys"</string>      <string name="btn_search_for_query">"Search for\n'%s'"</string> +    <string name="cache_ttl_lock_screen">"until Screen Off"</string> +    <string name="cache_ttl_five_minutes">"for Five Minutes"</string> +    <string name="cache_ttl_one_hour">"for One Hour"</string> +    <string name="cache_ttl_three_hours">"for Three Hours"</string> +    <string name="cache_ttl_one_day">"for One Day"</string> +    <string name="cache_ttl_three_days">"for Three Days"</string> +    <string name="settings_cache_select_three">"Pick up to three."</string> +    <string name="settings_cache_ttl_at_least_one">"At least one item must be selected!"</string> +    <string name="settings_cache_ttl_max_three">"Can\'t select more than three items!"</string> +    <string name="remember">"Remember"</string>  </resources> diff --git a/OpenKeychain/src/main/res/xml/passphrase_preferences.xml b/OpenKeychain/src/main/res/xml/passphrase_preferences.xml index 75f293c43..25bdf3a3d 100644 --- a/OpenKeychain/src/main/res/xml/passphrase_preferences.xml +++ b/OpenKeychain/src/main/res/xml/passphrase_preferences.xml @@ -1,8 +1,6 @@  <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> -    <org.sufficientlysecure.keychain.ui.widget.IntegerListPreference -        android:entries="@array/passphrase_cache_ttl_entries" -        android:entryValues="@array/passphrase_cache_ttl_values" -        android:key="passphraseCacheTtl" +    <PreferenceScreen +        android:key="passphraseCacheTtls"          android:persistent="false"          android:title="@string/label_passphrase_cache_ttl" />      <CheckBoxPreference  | 
