mirror of
https://github.com/etesync/android
synced 2025-02-02 19:01:06 +00:00
JournalManager: add version info to Journals and use it in crypto
The crypto class now behaves differently depending on the version of the journal. The current difference is in the key derivation, and that the new version of the crypto also hmacs the version automatically whenever it hmacs anything. The versioning was added for better future-proofing of the code. The derivation change was done because before we were creating the same password for all of the journals, now we do it per-journal. This means that we can, if needed in the future use this password as the journal password when sharing journals without compromising the security of the rest of the journals.
This commit is contained in:
parent
7357447786
commit
cf805d4e72
@ -0,0 +1,5 @@
|
||||
package com.etesync.syncadapter.journalmanager;
|
||||
|
||||
public class Constants {
|
||||
public final static int CURRENT_VERSION = 2;
|
||||
}
|
@ -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() {
|
||||
|
@ -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<Journal> 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() {
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<CollectionInfo> 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);
|
||||
|
@ -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<LocalResource> 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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user