diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java new file mode 100644 index 00000000..6e8d3d51 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java @@ -0,0 +1,5 @@ +package com.etesync.syncadapter.journalmanager; + +public class Constants { + public final static int CURRENT_VERSION = 2; +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java index d365d9aa..267f0524 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java @@ -1,8 +1,12 @@ package com.etesync.syncadapter.journalmanager; +import android.support.annotation.NonNull; import android.util.Base64; +import com.etesync.syncadapter.App; + import org.apache.commons.codec.Charsets; +import org.apache.commons.lang3.ArrayUtils; import org.spongycastle.crypto.BufferedBlockCipher; import org.spongycastle.crypto.CipherParameters; import org.spongycastle.crypto.InvalidCipherTextException; @@ -21,7 +25,7 @@ import org.spongycastle.util.encoders.Hex; import java.security.SecureRandom; import java.util.Arrays; -import com.etesync.syncadapter.App; +import lombok.Getter; public class Crypto { public static String deriveKey(String salt, String password) { @@ -32,12 +36,24 @@ public class Crypto { public static class CryptoManager { private SecureRandom _random = null; + @Getter + private final byte version; private final byte[] cipherKey; private final byte[] hmacKey; - public CryptoManager(String keyBase64, String salt) { - byte[] derivedKey; // FIXME use salt = hmac256(salt.getBytes(Charsets.UTF_8), Base64.decode(keyBase64, Base64.NO_WRAP)); - derivedKey = Base64.decode(keyBase64, Base64.NO_WRAP); + public CryptoManager(int version, @NonNull String keyBase64, @NonNull String salt) throws Exceptions.IntegrityException { + byte[] derivedKey; + if (version > Byte.MAX_VALUE) { + throw new Exceptions.IntegrityException("Version is out of range."); + } else if (version > Constants.CURRENT_VERSION) { + throw new RuntimeException("Journal version is newer than expected."); + } else if (version == 1) { + derivedKey = Base64.decode(keyBase64, Base64.NO_WRAP); + } else { + derivedKey = hmac256(salt.getBytes(Charsets.UTF_8), Base64.decode(keyBase64, Base64.NO_WRAP)); + } + + this.version = (byte) version; cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey); hmacKey = hmac256("hmac".getBytes(Charsets.UTF_8), derivedKey); } @@ -101,7 +117,12 @@ public class Crypto { } byte[] hmac(byte[] data) { - return hmac256(hmacKey, data); + if (version == 1) { + return hmac256(hmacKey, data); + } else { + // Starting from version 2 we hmac the version too. + return hmac256(hmacKey, ArrayUtils.add(data, version)); + } } private SecureRandom getRandom() { diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java index 9110124f..8a780e23 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -10,6 +10,9 @@ import java.util.UUID; import com.etesync.syncadapter.App; import com.etesync.syncadapter.GsonHelper; + +import lombok.Getter; +import lombok.Setter; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -46,7 +49,7 @@ public class JournalManager extends BaseManager { List ret = GsonHelper.gson.fromJson(body.charStream(), journalType); for (Journal journal : ret) { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(keyBase64, journal.getUuid()); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), keyBase64, journal.getUuid()); journal.processFromJson(); journal.verify(crypto); } @@ -88,6 +91,10 @@ public class JournalManager extends BaseManager { } public static class Journal extends Base { + @Setter + @Getter + private int version = -1; + final private transient int hmacSize = 256 / 8; // hmac256 in bytes private transient byte[] hmac = null; @@ -99,6 +106,7 @@ public class JournalManager extends BaseManager { public Journal(Crypto.CryptoManager crypto, String content, String uid) { super(crypto, content, uid); hmac = calculateHmac(crypto); + version = crypto.getVersion(); } private void processFromJson() { diff --git a/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java b/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java index 26d0dc1a..49745588 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java +++ b/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java @@ -10,6 +10,8 @@ package com.etesync.syncadapter.model; import android.content.ContentValues; +import com.etesync.syncadapter.journalmanager.Constants; +import com.etesync.syncadapter.journalmanager.JournalManager; import com.etesync.syncadapter.model.ServiceDB.Collections; import com.google.gson.GsonBuilder; import com.google.gson.annotations.Expose; @@ -22,6 +24,7 @@ import lombok.ToString; public class CollectionInfo implements Serializable { @Deprecated public long id; + public Long serviceID; public enum Type { @@ -29,6 +32,10 @@ public class CollectionInfo implements Serializable { CALENDAR } + // FIXME: Shouldn't be exposed, as it's already saved in the journal. We just expose it for when we save for db. + @Expose + public int version = -1; + @Expose public Type type; @@ -52,6 +59,7 @@ public class CollectionInfo implements Serializable { public boolean selected; public CollectionInfo() { + version = Constants.CURRENT_VERSION; } public static CollectionInfo defaultForServiceType(Type service) { @@ -70,6 +78,11 @@ public class CollectionInfo implements Serializable { return info; } + public void updateFromJournal(JournalManager.Journal journal) { + url = journal.getUuid(); + version = journal.getVersion(); + } + public boolean isOfTypeService(String service) { return service.equals(type.toString()); } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.java index bae5942d..8d091566 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarSyncManager.java @@ -24,6 +24,7 @@ import com.etesync.syncadapter.App; import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.InvalidAccountException; import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.JournalEntryManager; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.SyncEntry; @@ -44,7 +45,7 @@ public class CalendarSyncManager extends SyncManager { final private HttpUrl remote; - public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar, HttpUrl remote) throws InvalidAccountException { + public CalendarSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult result, LocalCalendar calendar, HttpUrl remote) throws InvalidAccountException, Exceptions.IntegrityException { super(context, account, settings, extras, authority, result, calendar.getName(), CollectionInfo.Type.CALENDAR); localCollection = calendar; this.remote = remote; diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.java index 340be48f..336a11fd 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncManager.java @@ -32,6 +32,7 @@ import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.HttpClient; import com.etesync.syncadapter.InvalidAccountException; import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.JournalEntryManager; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.SyncEntry; @@ -59,7 +60,7 @@ public class ContactsSyncManager extends SyncManager { final private ContentProviderClient provider; final private HttpUrl remote; - public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, HttpUrl principal, CollectionInfo info) throws InvalidAccountException { + public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, HttpUrl principal, CollectionInfo info) throws InvalidAccountException, Exceptions.IntegrityException { super(context, account, settings, extras, authority, result, info.url, CollectionInfo.Type.ADDRESS_BOOK); this.provider = provider; this.remote = principal; diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java index 0b1dfd6b..3d2a3110 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -14,12 +14,9 @@ import android.app.PendingIntent; import android.app.Service; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SyncResult; -import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -33,7 +30,6 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -50,7 +46,6 @@ import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.JournalManager; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; import com.etesync.syncadapter.model.ServiceDB; import com.etesync.syncadapter.ui.PermissionsActivity; @@ -161,9 +156,10 @@ public abstract class SyncAdapterService extends Service { List collections = new LinkedList<>(); for (JournalManager.Journal journal : journalsManager.getJournals(settings.password())) { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(settings.password(), journal.getUuid()); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUuid()); CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto)); - info.url = journal.getUuid(); + info.updateFromJournal(journal); + if (info.type.equals(serviceType)) { collections.add(info); } @@ -172,7 +168,7 @@ public abstract class SyncAdapterService extends Service { if (collections.isEmpty()) { CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType); info.url = JournalManager.Journal.genUid(); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(settings.password(), info.url); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.url); JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.url); journalsManager.putJournal(journal); collections.add(info); diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java index 945d8e0d..6874df8b 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java @@ -39,6 +39,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.UUID; import java.util.logging.Level; @@ -95,7 +96,7 @@ abstract public class SyncManager { private List localDeleted; private LocalResource[] localDirty; - public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String journalUid, CollectionInfo.Type serviceType) throws InvalidAccountException { + public SyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, SyncResult syncResult, String journalUid, CollectionInfo.Type serviceType) throws InvalidAccountException, Exceptions.IntegrityException { this.context = context; this.account = account; this.settings = settings; @@ -114,7 +115,8 @@ abstract public class SyncManager { notificationManager = new NotificationHelper(context, journalUid, notificationId()); notificationManager.cancel(); - crypto = new Crypto.CryptoManager(settings.password(), journalUid); + App.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version)); + crypto = new Crypto.CryptoManager(info.version, settings.password(), journalUid); } protected abstract int notificationId(); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java index c6139655..00bbe28b 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.java @@ -22,6 +22,7 @@ import android.view.View; import android.widget.EditText; import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.model.CollectionInfo; import org.apache.commons.lang3.StringUtils; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java index 4adefa27..d21dd623 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java @@ -158,11 +158,11 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal); if (info.url == null) { info.url = JournalManager.Journal.genUid(); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(settings.password(), info.url); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.url); JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.url); journalManager.putJournal(journal); } else { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(settings.password(), info.url); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.url); JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.url); journalManager.updateJournal(journal); } @@ -179,6 +179,8 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa return e; } catch (InvalidAccountException e) { return e; + } catch (Exceptions.IntegrityException e) { + return e; } finally { dbHelper.close(); } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java index 0073a994..cf14920e 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java @@ -120,7 +120,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa HttpUrl principal = HttpUrl.get(settings.getUri()); JournalManager journalManager = new JournalManager(HttpClient.create(getContext(), account), principal); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(settings.password(), collectionInfo.url); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.url); journalManager.deleteJournal(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.url)); JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.url); @@ -128,7 +128,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa data.update(journalEntity); return null; - } catch (Exceptions.HttpException e) { + } catch (Exceptions.HttpException|Exceptions.IntegrityException e) { return e; } catch (InvalidAccountException e) { return e;