diff --git a/app/schemas/com.todoroo.astrid.dao.Database/58.json b/app/schemas/com.todoroo.astrid.dao.Database/58.json index edc00eb49..f686a6971 100644 --- a/app/schemas/com.todoroo.astrid.dao.Database/58.json +++ b/app/schemas/com.todoroo.astrid.dao.Database/58.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 58, - "identityHash": "119477b5c2bd3389e53fef40e4844e01", + "identityHash": "dad699968028cb1d91c793e29b73ceb9", "entities": [ { "tableName": "notification", @@ -761,7 +761,7 @@ "notNull": true }, { - "fieldPath": "account", + "fieldPath": "calendar", "columnName": "calendar", "affinity": "TEXT", "notNull": false @@ -814,7 +814,7 @@ }, { "tableName": "caldav_account", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT, `iv` BLOB)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uuid` TEXT, `name` TEXT, `url` TEXT, `username` TEXT, `password` TEXT)", "fields": [ { "fieldPath": "id", @@ -851,12 +851,6 @@ "columnName": "password", "affinity": "TEXT", "notNull": false - }, - { - "fieldPath": "iv", - "columnName": "iv", - "affinity": "BLOB", - "notNull": false } ], "primaryKey": { @@ -871,7 +865,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"119477b5c2bd3389e53fef40e4844e01\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"dad699968028cb1d91c793e29b73ceb9\")" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java index 65d95738e..e6f4be85a 100644 --- a/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java +++ b/app/src/main/java/com/todoroo/andlib/utility/AndroidUtilities.java @@ -179,6 +179,10 @@ public class AndroidUtilities { return !atLeastJellybean(); } + public static boolean preMarshmallow() { + return !atLeastMarshmallow(); + } + public static boolean preOreo() { return !atLeastOreo(); } diff --git a/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java index 7828eb61c..04bc5b4f7 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java @@ -335,7 +335,7 @@ public class FilterAdapter extends ArrayAdapter { if (!inventory.hasPro()) { add( new NavigationDrawerAction( - activity.getResources().getString(R.string.subscribe_to_pro), + activity.getResources().getString(R.string.upgrade_to_pro), R.drawable.ic_attach_money_black_24dp, new Intent(activity, PurchaseActivity.class), REQUEST_PURCHASE)); diff --git a/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java b/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java index 60503dbe0..6aeef3ab7 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java +++ b/app/src/main/java/org/tasks/caldav/CaldavAccountSettingsActivity.java @@ -1,6 +1,7 @@ package org.tasks.caldav; import static android.text.TextUtils.isEmpty; +import static com.todoroo.andlib.utility.AndroidUtilities.preMarshmallow; import android.app.ProgressDialog; import android.content.Context; @@ -14,10 +15,7 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import android.view.inputmethod.InputMethodManager; import android.widget.LinearLayout; -import at.bitfire.dav4android.DavResource; -import at.bitfire.dav4android.PropertyCollection; import at.bitfire.dav4android.exception.HttpException; -import at.bitfire.dav4android.property.DisplayName; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnFocusChange; @@ -28,7 +26,6 @@ import java.net.ConnectException; import java.net.IDN; import java.net.URI; import java.net.URISyntaxException; -import java.util.List; import javax.inject.Inject; import org.tasks.R; import org.tasks.analytics.Tracker; @@ -39,8 +36,10 @@ import org.tasks.dialogs.DialogBuilder; import org.tasks.injection.ActivityComponent; import org.tasks.injection.ThemedInjectingAppCompatActivity; import org.tasks.preferences.Preferences; +import org.tasks.security.Encryption; import org.tasks.sync.SyncAdapters; import org.tasks.ui.DisplayableException; +import org.tasks.ui.MenuColorizer; import timber.log.Timber; public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActivity @@ -54,10 +53,14 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv @Inject CaldavDao caldavDao; @Inject SyncAdapters syncAdapters; @Inject TaskDeleter taskDeleter; + @Inject Encryption encryption; @BindView(R.id.root_layout) LinearLayout root; + @BindView(R.id.name) + TextInputEditText name; + @BindView(R.id.url) TextInputEditText url; @@ -67,6 +70,9 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv @BindView(R.id.password) TextInputEditText password; + @BindView(R.id.name_layout) + TextInputLayout nameLayout; + @BindView(R.id.url_layout) TextInputLayout urlLayout; @@ -92,7 +98,13 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv caldavAccount = getIntent().getParcelableExtra(EXTRA_CALDAV_DATA); if (savedInstanceState == null) { + if (caldavAccount == null) { + if (preMarshmallow()) { + passwordLayout.setError(getString(R.string.encryption_warning)); + } + } if (caldavAccount != null) { + name.setText(caldavAccount.getName()); url.setText(caldavAccount.getUrl()); user.setText(caldavAccount.getUsername()); if (!isEmpty(caldavAccount.getPassword())) { @@ -118,15 +130,21 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv toolbar.inflateMenu(R.menu.menu_caldav_account_settings); toolbar.setOnMenuItemClickListener(this); toolbar.showOverflowMenu(); + MenuColorizer.colorToolbar(this, toolbar); if (caldavAccount == null) { toolbar.getMenu().findItem(R.id.remove).setVisible(false); - url.requestFocus(); + name.requestFocus(); InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(url, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT); } } + @OnTextChanged(R.id.name) + void onNameChanged(CharSequence text) { + nameLayout.setError(null); + } + @OnTextChanged(R.id.url) void onUrlChanged(CharSequence text) { urlLayout.setError(null); @@ -149,7 +167,7 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv password.setText(""); } } else { - if (isEmpty(password.getText()) && !isEmpty(caldavAccount.getPassword())) { + if (isEmpty(password.getText()) && caldavAccount != null) { password.setText(PASSWORD_MASK); } } @@ -160,6 +178,10 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv component.inject(this); } + private String getNewName() { + return name.getText().toString().trim(); + } + private String getNewURL() { return url.getText().toString().trim(); } @@ -168,18 +190,36 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv return user.getText().toString().trim(); } + private boolean passwordChanged() { + return caldavAccount == null || !PASSWORD_MASK.equals(password.getText().toString().trim()); + } + private String getNewPassword() { String input = password.getText().toString().trim(); - return PASSWORD_MASK.equals(input) ? caldavAccount.getPassword() : input; + return PASSWORD_MASK.equals(input) + ? encryption.decrypt(caldavAccount.getPassword()) + : input; } private void save() { + String name = getNewName(); String username = getNewUsername(); String url = getNewURL(); String password = getNewPassword(); boolean failed = false; + if (isEmpty(name)) { + nameLayout.setError(getString(R.string.name_cannot_be_empty)); + failed = true; + } else { + CaldavAccount accountByName = caldavDao.getAccountByName(name); + if (accountByName != null && !accountByName.equals(caldavAccount)) { + nameLayout.setError(getString(R.string.duplicate_name)); + failed = true; + } + } + if (isEmpty(url)) { urlLayout.setError(getString(R.string.url_required)); failed = true; @@ -255,7 +295,7 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv CaldavAccount newAccount = new CaldavAccount(); newAccount.setUrl(principal); newAccount.setUsername(getNewUsername()); - newAccount.setPassword(getNewPassword()); + newAccount.setPassword(encryption.encrypt(getNewPassword())); newAccount.setUuid(UUIDHelper.newUUID()); newAccount.setId(caldavDao.insert(newAccount)); @@ -264,9 +304,12 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv } private void updateAccount(String principal) { + caldavAccount.setName(getNewName()); caldavAccount.setUrl(principal); caldavAccount.setUsername(getNewUsername()); - caldavAccount.setPassword(getNewPassword()); + if (passwordChanged()) { + caldavAccount.setPassword(encryption.encrypt(getNewPassword())); + } caldavDao.update(caldavAccount); setResult(RESULT_OK); @@ -305,21 +348,24 @@ public class CaldavAccountSettingsActivity extends ThemedInjectingAppCompatActiv private boolean hasChanges() { if (caldavAccount == null) { - return !isEmpty(getNewPassword()) || !isEmpty(getNewURL()) || !isEmpty(getNewUsername()); + return !isEmpty(getNewName()) + || !isEmpty(getNewPassword()) + || !isEmpty(getNewURL()) + || !isEmpty(getNewUsername()); } - return needsValidation(); + return needsValidation() || !getNewName().equals(caldavAccount.getName()); } private boolean needsValidation() { return !getNewURL().equals(caldavAccount.getUrl()) || !getNewUsername().equals(caldavAccount.getUsername()) - || !getNewPassword().equals(caldavAccount.getPassword()); + || passwordChanged(); } @Override public void finish() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(url.getWindowToken(), 0); + imm.hideSoftInputFromWindow(name.getWindowToken(), 0); super.finish(); } diff --git a/app/src/main/java/org/tasks/caldav/CaldavClient.java b/app/src/main/java/org/tasks/caldav/CaldavClient.java index 3cf04a968..208d9e000 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClient.java +++ b/app/src/main/java/org/tasks/caldav/CaldavClient.java @@ -27,6 +27,8 @@ import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import org.tasks.R; +import org.tasks.data.CaldavAccount; +import org.tasks.security.Encryption; import org.tasks.ui.DisplayableException; import timber.log.Timber; @@ -35,7 +37,14 @@ class CaldavClient { private final DavResource davResource; private HttpUrl httpUrl; - public CaldavClient(String url, String username, String password) { + CaldavClient(CaldavAccount caldavAccount, Encryption encryption) { + this( + caldavAccount.getUrl(), + caldavAccount.getUsername(), + encryption.decrypt(caldavAccount.getPassword())); + } + + CaldavClient(String url, String username, String password) { BasicDigestAuthHandler basicDigestAuthHandler = new BasicDigestAuthHandler(null, username, password); OkHttpClient httpClient = diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java index c21b59ec6..273d1fe59 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java @@ -24,6 +24,7 @@ import at.bitfire.dav4android.property.GetETag; import at.bitfire.ical4android.InvalidCalendarException; import at.bitfire.ical4android.iCalendar; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.io.CharStreams; import com.todoroo.andlib.utility.DateUtilities; @@ -55,6 +56,7 @@ import org.tasks.data.CaldavCalendar; import org.tasks.data.CaldavDao; import org.tasks.data.CaldavTask; import org.tasks.injection.ForApplication; +import org.tasks.security.Encryption; import timber.log.Timber; public class CaldavSynchronizer { @@ -69,6 +71,7 @@ public class CaldavSynchronizer { private final LocalBroadcastManager localBroadcastManager; private final TaskCreator taskCreator; private final TaskDeleter taskDeleter; + private final Encryption encryption; private final Context context; @Inject @@ -78,25 +81,31 @@ public class CaldavSynchronizer { TaskDao taskDao, LocalBroadcastManager localBroadcastManager, TaskCreator taskCreator, - TaskDeleter taskDeleter) { + TaskDeleter taskDeleter, + Encryption encryption) { this.context = context; this.caldavDao = caldavDao; this.taskDao = taskDao; this.localBroadcastManager = localBroadcastManager; this.taskCreator = taskCreator; this.taskDeleter = taskDeleter; + this.encryption = encryption; } public void sync() { // required for dav4android (ServiceLoader) Thread.currentThread().setContextClassLoader(context.getClassLoader()); for (CaldavAccount account : caldavDao.getAccounts()) { - CaldavClient caldavClient = - new CaldavClient(account.getUrl(), account.getUsername(), account.getPassword()); + if (isNullOrEmpty(account.getPassword())) { + Timber.e("Missing password for %s", account); + continue; + } + CaldavClient caldavClient = new CaldavClient(account, encryption); List resources = caldavClient.getCalendars(); Set urls = newHashSet(transform(resources, c -> c.getLocation().toString())); Timber.d("Found calendars: %s", urls); - for (CaldavCalendar deleted : caldavDao.findDeletedCalendars(account.getUuid(), newArrayList(urls))) { + for (CaldavCalendar deleted : + caldavDao.findDeletedCalendars(account.getUuid(), newArrayList(urls))) { taskDeleter.markDeleted(caldavDao.getTasksByCalendar(deleted.getUuid())); caldavDao.deleteTasksForCalendar(deleted.getUuid()); caldavDao.delete(deleted); @@ -115,22 +124,15 @@ public class CaldavSynchronizer { calendar.setId(caldavDao.insert(calendar)); localBroadcastManager.broadcastRefreshList(); } - String ctag = properties.get(GetCTag.class).getCTag(); - if (calendar.getCtag() == null || !calendar.getCtag().equals(ctag)) { - sync(account, calendar); - } + sync(account, calendar); } } } private void sync(CaldavAccount account, CaldavCalendar caldavCalendar) { - if (isNullOrEmpty(account.getPassword())) { - Timber.e("Missing password for %s", caldavCalendar); - return; - } Timber.d("sync(%s)", caldavCalendar); BasicDigestAuthHandler basicDigestAuthHandler = - new BasicDigestAuthHandler(null, account.getUsername(), account.getPassword()); + new BasicDigestAuthHandler(null, account.getUsername(), encryption.decrypt(account.getPassword())); OkHttpClient httpClient = new OkHttpClient() .newBuilder() @@ -173,7 +175,7 @@ public class CaldavSynchronizer { Iterable changed = filter( - davCalendar.getMembers(), + ImmutableSet.copyOf(davCalendar.getMembers()), vCard -> { GetETag eTag = (GetETag) vCard.getProperties().get(GetETag.NAME); if (eTag == null || isNullOrEmpty(eTag.getETag())) { diff --git a/app/src/main/java/org/tasks/dashclock/DashClockExtension.java b/app/src/main/java/org/tasks/dashclock/DashClockExtension.java index e7f6a06ca..a46a1748c 100644 --- a/app/src/main/java/org/tasks/dashclock/DashClockExtension.java +++ b/app/src/main/java/org/tasks/dashclock/DashClockExtension.java @@ -87,8 +87,8 @@ public class DashClockExtension extends com.google.android.apps.dashclock.api.Da new ExtensionData() .visible(true) .icon(R.drawable.ic_check_white_24dp) - .status(getString(R.string.subscribe_to_pro)) - .expandedTitle(getString(R.string.subscribe_to_pro)) + .status(getString(R.string.upgrade_to_pro)) + .expandedTitle(getString(R.string.upgrade_to_pro)) .clickIntent(new Intent(this, DashClockSettings.class))); } } diff --git a/app/src/main/java/org/tasks/data/CaldavAccount.java b/app/src/main/java/org/tasks/data/CaldavAccount.java index ea05de25d..3d6a3e15a 100644 --- a/app/src/main/java/org/tasks/data/CaldavAccount.java +++ b/app/src/main/java/org/tasks/data/CaldavAccount.java @@ -46,9 +46,6 @@ public class CaldavAccount implements Parcelable { @ColumnInfo(name = "password") private transient String password = ""; - @ColumnInfo(name = "iv") - private transient byte[] iv = null; - public CaldavAccount() {} @Ignore @@ -59,7 +56,6 @@ public class CaldavAccount implements Parcelable { url = source.readString(); username = source.readString(); password = source.readString(); - iv = source.createByteArray(); } public long getId() { @@ -110,14 +106,6 @@ public class CaldavAccount implements Parcelable { this.password = password; } - public byte[] getIv() { - return iv; - } - - public void setIv(byte[] iv) { - this.iv = iv; - } - @Override public String toString() { return "CaldavAccount{" + @@ -127,7 +115,6 @@ public class CaldavAccount implements Parcelable { ", url='" + url + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + - ", iv=" + Arrays.toString(iv) + '}'; } @@ -157,10 +144,7 @@ public class CaldavAccount implements Parcelable { if (username != null ? !username.equals(that.username) : that.username != null) { return false; } - if (password != null ? !password.equals(that.password) : that.password != null) { - return false; - } - return Arrays.equals(iv, that.iv); + return password != null ? password.equals(that.password) : that.password == null; } @Override @@ -171,7 +155,6 @@ public class CaldavAccount implements Parcelable { result = 31 * result + (url != null ? url.hashCode() : 0); result = 31 * result + (username != null ? username.hashCode() : 0); result = 31 * result + (password != null ? password.hashCode() : 0); - result = 31 * result + Arrays.hashCode(iv); return result; } @@ -188,6 +171,5 @@ public class CaldavAccount implements Parcelable { dest.writeString(url); dest.writeString(username); dest.writeString(password); - dest.writeByteArray(iv); } } diff --git a/app/src/main/java/org/tasks/data/CaldavDao.java b/app/src/main/java/org/tasks/data/CaldavDao.java index 3825b063c..eed21d3f7 100644 --- a/app/src/main/java/org/tasks/data/CaldavDao.java +++ b/app/src/main/java/org/tasks/data/CaldavDao.java @@ -90,4 +90,7 @@ public interface CaldavDao { @Query("SELECT * FROM caldav_calendar WHERE account = :account AND url = :url LIMIT 1") CaldavCalendar getCalendarByUrl(String account, String url); + + @Query("SELECT * FROM caldav_account WHERE name = :name COLLATE NOCASE LIMIT 1") + CaldavAccount getAccountByName(String name); } diff --git a/app/src/main/java/org/tasks/security/EncryptedString.java b/app/src/main/java/org/tasks/security/EncryptedString.java deleted file mode 100644 index 40e78fe89..000000000 --- a/app/src/main/java/org/tasks/security/EncryptedString.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.tasks.security; - -public class EncryptedString { - - private final String value; - private final byte[] iv; - - public EncryptedString(String value, byte[] iv) { - this.value = value; - this.iv = iv; - } - - public String getValue() { - return value; - } - - public byte[] getIv() { - return iv; - } -} diff --git a/app/src/main/java/org/tasks/security/Encryption.java b/app/src/main/java/org/tasks/security/Encryption.java index 1462a55ad..21f974417 100644 --- a/app/src/main/java/org/tasks/security/Encryption.java +++ b/app/src/main/java/org/tasks/security/Encryption.java @@ -1,15 +1,8 @@ package org.tasks.security; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - public interface Encryption { - EncryptedString encrypt(String text) - throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException; + String encrypt(String text); - String decrypt(EncryptedString encryptedString) - throws IOException, BadPaddingException, IllegalBlockSizeException; + String decrypt(String text); } diff --git a/app/src/main/java/org/tasks/security/KeyStoreEncryption.java b/app/src/main/java/org/tasks/security/KeyStoreEncryption.java index d5c496001..620d27ff4 100644 --- a/app/src/main/java/org/tasks/security/KeyStoreEncryption.java +++ b/app/src/main/java/org/tasks/security/KeyStoreEncryption.java @@ -5,8 +5,10 @@ import android.os.Build.VERSION_CODES; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyProperties; import android.support.annotation.RequiresApi; +import android.util.Base64; import java.io.IOException; -import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStore; @@ -14,8 +16,10 @@ import java.security.KeyStore.Entry; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; +import java.security.SecureRandom; import java.security.UnrecoverableEntryException; import java.security.cert.CertificateException; +import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -24,13 +28,16 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.inject.Inject; +import timber.log.Timber; @RequiresApi(api = VERSION_CODES.M) public class KeyStoreEncryption implements Encryption { private static final String ANDROID_KEYSTORE = "AndroidKeyStore"; private static final String ALIAS = "passwords"; - private static final String ENCODING = "UTF-8"; + private static final Charset ENCODING = StandardCharsets.UTF_8; + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 16; private KeyStore keyStore; @@ -45,28 +52,40 @@ public class KeyStoreEncryption implements Encryption { } @Override - public EncryptedString encrypt(String text) - throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, null); - return new EncryptedString( - new String(cipher.doFinal(text.getBytes(ENCODING)), ENCODING), cipher.getIV()); + public String encrypt(String text) { + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, iv); + try { + byte[] output = cipher.doFinal(text.getBytes(ENCODING)); + byte[] result = new byte[iv.length + output.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(output, 0, result, iv.length, output.length); + return Base64.encodeToString(result, Base64.DEFAULT); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Timber.e(e); + return null; + } } @Override - public String decrypt(EncryptedString encryptedString) - throws IOException, BadPaddingException, IllegalBlockSizeException { - Cipher cipher = getCipher(Cipher.DECRYPT_MODE, encryptedString.getIv()); - return new String(cipher.doFinal(encryptedString.getValue().getBytes(ENCODING)), ENCODING); + public String decrypt(String text) { + byte[] decoded = Base64.decode(text, Base64.DEFAULT); + byte[] iv = Arrays.copyOfRange(decoded, 0, GCM_IV_LENGTH); + Cipher cipher = getCipher(Cipher.DECRYPT_MODE, iv); + try { + byte[] decrypted = cipher.doFinal(decoded, GCM_IV_LENGTH, decoded.length - GCM_IV_LENGTH); + return new String(decrypted, ENCODING); + } catch (IllegalBlockSizeException | BadPaddingException e) { + Timber.e(e); + return null; + } } private Cipher getCipher(int cipherMode, byte[] iv) { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); - if (cipherMode == Cipher.ENCRYPT_MODE) { - cipher.init(cipherMode, getSecretKey()); - } else { - cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(128, iv)); - } + cipher.init(cipherMode, getSecretKey(), new GCMParameterSpec(GCM_TAG_LENGTH * Byte.SIZE, iv)); return cipher; } catch (NoSuchAlgorithmException | NoSuchPaddingException @@ -95,6 +114,7 @@ public class KeyStoreEncryption implements Encryption { new KeyGenParameterSpec.Builder( ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setRandomizedEncryptionRequired(false) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .build()); return keyGenerator.generateKey(); diff --git a/app/src/main/java/org/tasks/security/NoEncryption.java b/app/src/main/java/org/tasks/security/NoEncryption.java index eabdbbc3b..897290c8e 100644 --- a/app/src/main/java/org/tasks/security/NoEncryption.java +++ b/app/src/main/java/org/tasks/security/NoEncryption.java @@ -1,21 +1,14 @@ package org.tasks.security; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; - public class NoEncryption implements Encryption { @Override - public EncryptedString encrypt(String text) - throws UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { - return new EncryptedString(text, null); + public String encrypt(String text) { + return text; } @Override - public String decrypt(EncryptedString encryptedString) - throws IOException, BadPaddingException, IllegalBlockSizeException { - return encryptedString.getValue(); + public String decrypt(String text) { + return text; } } diff --git a/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java b/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java index 4164146fa..efa1e676e 100644 --- a/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java +++ b/app/src/main/java/org/tasks/sync/SynchronizationPreferences.java @@ -65,6 +65,7 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity { for (CaldavAccount caldavAccount : caldavDao.getAccounts()) { Preference accountPreferences = new Preference(this); accountPreferences.setTitle(caldavAccount.getName()); + accountPreferences.setSummary(caldavAccount.getUrl()); accountPreferences.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(this, CaldavAccountSettingsActivity.class); intent.putExtra(CaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, caldavAccount); @@ -73,12 +74,14 @@ public class SynchronizationPreferences extends InjectingPreferenceActivity { }); caldavPreferences.addPreference(accountPreferences); } - findPreference(getString(R.string.add_account)).setOnPreferenceClickListener( - preference -> { - startActivityForResult(new Intent(this, CaldavAccountSettingsActivity.class), - REQUEST_CALDAV_SETTINGS); - return false; - }); + Preference addCaldavAccount = new Preference(this); + addCaldavAccount.setTitle(R.string.add_account); + addCaldavAccount.setOnPreferenceClickListener(preference -> { + startActivityForResult(new Intent(this, CaldavAccountSettingsActivity.class), + REQUEST_CALDAV_SETTINGS); + return false; + }); + caldavPreferences.addPreference(addCaldavAccount); final CheckBoxPreference gtaskPreference = (CheckBoxPreference) findPreference(getString(R.string.sync_gtasks)); diff --git a/app/src/main/res/layout/activity_caldav_account_settings.xml b/app/src/main/res/layout/activity_caldav_account_settings.xml index fff8470ba..96f1678fc 100644 --- a/app/src/main/res/layout/activity_caldav_account_settings.xml +++ b/app/src/main/res/layout/activity_caldav_account_settings.xml @@ -20,6 +20,20 @@ android:focusableInTouchMode="true" android:orientation="vertical"> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 343f2be3c..659303deb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -752,6 +752,7 @@ File %1$s contained %2$s.\n\n Improve Tasks Send anonymous usage statistics and crash reports to help improve Tasks. No personal data will be collected. Tag already exists + Duplicate name Name cannot be empty Username required Password required @@ -882,7 +883,7 @@ File %1$s contained %2$s.\n\n Connection failed Only on unmetered connections Upgrade - Subscribe to pro + Upgrade to pro Refresh purchases Subscribed Subscribe @@ -898,5 +899,6 @@ File %1$s contained %2$s.\n\n Multiple Google Task accounts Tasker plugins Dashclock extension + Passwords are stored in plain text on devices running Android 5 or below. This is a security concern if your device has been rooted. diff --git a/app/src/main/res/xml/preferences_synchronization.xml b/app/src/main/res/xml/preferences_synchronization.xml index 615e37d49..265b8a3fc 100644 --- a/app/src/main/res/xml/preferences_synchronization.xml +++ b/app/src/main/res/xml/preferences_synchronization.xml @@ -24,11 +24,7 @@ - - + android:title="@string/CalDAV"/>