From 89731519e9467e4565c6bb4fb6bf8f97439189d4 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 20 Apr 2017 19:10:02 +0100 Subject: [PATCH 01/23] Account view: cleanup and share collection list items and adapter. They were redundant and needed some cleanup, now they are better, and shared between calendar and contact. --- .../syncadapter/ui/AccountActivity.java | 58 +++++-------------- .../main/res/layout/account_carddav_item.xml | 45 -------------- ...v_item.xml => account_collection_item.xml} | 27 +++++---- 3 files changed, 29 insertions(+), 101 deletions(-) delete mode 100644 app/src/main/res/layout/account_carddav_item.xml rename app/src/main/res/layout/{account_caldav_item.xml => account_collection_item.xml} (71%) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index d18fc7c6..cba38ac2 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -231,7 +231,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu listCardDAV.setEnabled(!info.carddav.refreshing); listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1); - AddressBookAdapter adapter = new AddressBookAdapter(this); + CollectionListAdapter adapter = new CollectionListAdapter(this); adapter.addAll(info.carddav.collections); listCardDAV.setAdapter(adapter); listCardDAV.setOnItemClickListener(onItemClickListener); @@ -247,7 +247,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu listCalDAV.setEnabled(!info.caldav.refreshing); listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1); - final CalendarAdapter adapter = new CalendarAdapter(this); + final CollectionListAdapter adapter = new CollectionListAdapter(this); adapter.addAll(info.caldav.collections); listCalDAV.setAdapter(adapter); listCalDAV.setOnItemClickListener(onItemClickListener); @@ -352,15 +352,15 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu /* LIST ADAPTERS */ - public static class AddressBookAdapter extends ArrayAdapter { - public AddressBookAdapter(Context context) { - super(context, R.layout.account_carddav_item); + public static class CollectionListAdapter extends ArrayAdapter { + public CollectionListAdapter(Context context) { + super(context, R.layout.account_collection_item); } @Override public View getView(int position, View v, ViewGroup parent) { if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.account_carddav_item, parent, false); + v = LayoutInflater.from(getContext()).inflate(R.layout.account_collection_item, parent, false); final CollectionInfo info = getItem(position); @@ -375,45 +375,19 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu tv.setText(info.description); } - tv = (TextView)v.findViewById(R.id.read_only); - tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE); - - return v; - } - } - - public static class CalendarAdapter extends ArrayAdapter { - public CalendarAdapter(Context context) { - super(context, R.layout.account_caldav_item); - } - - @Override - public View getView(final int position, View v, ViewGroup parent) { - if (v == null) - v = LayoutInflater.from(getContext()).inflate(R.layout.account_caldav_item, parent, false); - - final CollectionInfo info = getItem(position); - - View vColor = v.findViewById(R.id.color); - if (info.color != null) { - vColor.setBackgroundColor(info.color); + final View vColor = v.findViewById(R.id.color); + if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { + vColor.setVisibility(View.GONE); } else { - vColor.setBackgroundColor(LocalCalendar.defaultColor); + if (info.color != null) { + vColor.setBackgroundColor(info.color); + } else { + vColor.setBackgroundColor(LocalCalendar.defaultColor); + } } - TextView tv = (TextView)v.findViewById(R.id.title); - tv.setText(TextUtils.isEmpty(info.displayName) ? info.uid : info.displayName); - - tv = (TextView)v.findViewById(R.id.description); - if (TextUtils.isEmpty(info.description)) - tv.setVisibility(View.GONE); - else { - tv.setVisibility(View.VISIBLE); - tv.setText(info.description); - } - - tv = (TextView)v.findViewById(R.id.read_only); - tv.setVisibility(info.readOnly ? View.VISIBLE : View.GONE); + View readOnly = v.findViewById(R.id.read_only); + readOnly.setVisibility(info.readOnly ? View.VISIBLE : View.GONE); return v; } diff --git a/app/src/main/res/layout/account_carddav_item.xml b/app/src/main/res/layout/account_carddav_item.xml deleted file mode 100644 index ee0ff6d3..00000000 --- a/app/src/main/res/layout/account_carddav_item.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/account_caldav_item.xml b/app/src/main/res/layout/account_collection_item.xml similarity index 71% rename from app/src/main/res/layout/account_caldav_item.xml rename to app/src/main/res/layout/account_collection_item.xml index 8dc9a303..c0184338 100644 --- a/app/src/main/res/layout/account_caldav_item.xml +++ b/app/src/main/res/layout/account_collection_item.xml @@ -1,5 +1,4 @@ - - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal" + android:padding="8dp"> + tools:text="Title" /> + tools:text="Description" /> - + android:src="@drawable/ic_remove_circle_dark" /> + tools:background="@color/green700" /> \ No newline at end of file From e836b4c716db7fb78ad2f69a05a0dda68dac5cc8 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 13:33:17 +0100 Subject: [PATCH 02/23] Crypto: Add basic asymmetric encryption methods --- .../syncadapter/journalmanager/Crypto.java | 89 ++++++++++++++++++- .../journalmanager/EncryptionTest.java | 17 ++++ 2 files changed, 103 insertions(+), 3 deletions(-) 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 fa08028f..b6120c70 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java @@ -7,11 +7,18 @@ import com.etesync.syncadapter.utils.Base64; import org.apache.commons.codec.Charsets; import org.apache.commons.lang3.ArrayUtils; +import org.spongycastle.asn1.pkcs.PrivateKeyInfo; +import org.spongycastle.asn1.x509.SubjectPublicKeyInfo; +import org.spongycastle.crypto.AsymmetricBlockCipher; +import org.spongycastle.crypto.AsymmetricCipherKeyPair; import org.spongycastle.crypto.BufferedBlockCipher; import org.spongycastle.crypto.CipherParameters; import org.spongycastle.crypto.InvalidCipherTextException; import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.encodings.OAEPEncoding; import org.spongycastle.crypto.engines.AESEngine; +import org.spongycastle.crypto.engines.RSAEngine; +import org.spongycastle.crypto.generators.RSAKeyPairGenerator; import org.spongycastle.crypto.generators.SCrypt; import org.spongycastle.crypto.macs.HMac; import org.spongycastle.crypto.modes.CBCBlockCipher; @@ -20,12 +27,21 @@ import org.spongycastle.crypto.paddings.PKCS7Padding; import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.spongycastle.crypto.params.KeyParameter; import org.spongycastle.crypto.params.ParametersWithIV; +import org.spongycastle.crypto.params.RSAKeyGenerationParameters; +import org.spongycastle.crypto.util.PrivateKeyFactory; +import org.spongycastle.crypto.util.PrivateKeyInfoFactory; +import org.spongycastle.crypto.util.PublicKeyFactory; +import org.spongycastle.crypto.util.SubjectPublicKeyInfoFactory; import org.spongycastle.util.encoders.Hex; +import java.io.IOException; +import java.math.BigInteger; import java.security.SecureRandom; import java.util.Arrays; +import lombok.AccessLevel; import lombok.Getter; +import lombok.RequiredArgsConstructor; public class Crypto { public static String deriveKey(String salt, String password) { @@ -34,6 +50,73 @@ public class Crypto { return Base64.encodeToString(SCrypt.generate(password.getBytes(Charsets.UTF_8), salt.getBytes(Charsets.UTF_8), 16384, 8, 1, keySize), Base64.NO_WRAP); } + public static AsymmetricKeyPair generateKeyPair() { + RSAKeyPairGenerator keyPairGenerator = new RSAKeyPairGenerator(); + keyPairGenerator.init(new RSAKeyGenerationParameters(BigInteger.valueOf(65537), new SecureRandom(), 2048, 160)); + AsymmetricCipherKeyPair keyPair = keyPairGenerator.generateKeyPair(); + try { + PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.getPrivate()); + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(keyPair.getPublic()); + return new AsymmetricKeyPair(privateKeyInfo.getEncoded(), publicKeyInfo.getEncoded()); + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } + + @RequiredArgsConstructor + public static class AsymmetricKeyPair { + @Getter(AccessLevel.PUBLIC) + private final byte[] privateKey; + @Getter(AccessLevel.PUBLIC) + private final byte[] publicKey; + } + + public static class AsymmetricCryptoManager { + private final AsymmetricKeyPair keyPair; + + public AsymmetricCryptoManager(AsymmetricKeyPair keyPair) { + this.keyPair = keyPair; + } + + public byte[] encrypt(byte[] pubkey, byte[] content) { + AsymmetricBlockCipher cipher = new RSAEngine(); + cipher = new OAEPEncoding(cipher); + try { + cipher.init(true, PublicKeyFactory.createKey(pubkey)); + return cipher.processBlock(content, 0, content.length); + } catch (IOException e) { + e.printStackTrace(); + } catch (InvalidCipherTextException e) { + e.printStackTrace(); + App.log.severe("Invalid ciphertext: " + Base64.encodeToString(content, Base64.NO_WRAP)); + } + + return null; + } + + public byte[] decrypt(byte[] cipherText) { + AsymmetricBlockCipher cipher = new RSAEngine(); + cipher = new OAEPEncoding(cipher); + try { + cipher.init(false, PrivateKeyFactory.createKey(keyPair.getPrivateKey())); + return cipher.processBlock(cipherText, 0, cipherText.length); + } catch (IOException e) { + e.printStackTrace(); + } catch (InvalidCipherTextException e) { + e.printStackTrace(); + App.log.severe("Invalid ciphertext: " + Base64.encodeToString(cipherText, Base64.NO_WRAP)); + } + + return null; + } + + public static byte[] getKeyFingerprint(byte[] pubkey) { + return sha256(pubkey); + } + } + public static class CryptoManager { final static int HMAC_SIZE = 256 / 8; // hmac256 in bytes @@ -146,15 +229,15 @@ public class Crypto { } static String sha256(String base) { - return sha256(base.getBytes(Charsets.UTF_8)); + return toHex(sha256(base.getBytes(Charsets.UTF_8))); } - private static String sha256(byte[] base) { + private static byte[] sha256(byte[] base) { SHA256Digest digest = new SHA256Digest(); digest.update(base, 0, base.length); byte[] ret = new byte[digest.getDigestSize()]; digest.doFinal(ret, 0); - return toHex(ret); + return ret; } static String toHex(byte[] bytes) { diff --git a/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java b/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java index 01883101..b0d65764 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java @@ -14,9 +14,11 @@ import org.apache.commons.codec.Charsets; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.spongycastle.util.encoders.Hex; import java.io.IOException; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; public class EncryptionTest { @@ -67,4 +69,19 @@ public class EncryptionTest { public void testCryptoVersionOutOfRange() throws Exceptions.IntegrityException, Exceptions.VersionTooNewException { new Crypto.CryptoManager(999, Helpers.keyBase64, "TestSaltShouldBeJournalId"); } + + @Test + public void testAsymCrypto() throws Exceptions.IntegrityException, Exceptions.GenericCryptoException { + Crypto.AsymmetricKeyPair keyPair = Crypto.generateKeyPair(); + Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair); + + byte[] clearText = "This Is Some Test Cleartext.".getBytes(Charsets.UTF_8); + byte[] cipher = cryptoManager.encrypt(keyPair.getPublicKey(), clearText); + byte[] clearText2 = cryptoManager.decrypt(cipher); + assertArrayEquals(clearText, clearText2); + + // Mostly for coverage. Make sure it's the expected sha256 value. + assertEquals("ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", + Hex.toHexString(Crypto.AsymmetricCryptoManager.getKeyFingerprint("a".getBytes(Charsets.UTF_8))).toLowerCase()); + } } From 11e37dbd1ea28f2a9ec7d7d361b5b5e73c790349 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 12 Apr 2017 11:54:41 +0100 Subject: [PATCH 03/23] Journalmanager: add API to interact with the UserInfo This is where the keypair is stored on the server. Both the public facing public key, and the encrypted private key --- .../journalmanager/JournalManager.java | 6 +- .../journalmanager/UserInfoManager.java | 150 ++++++++++++++++++ .../journalmanager/ServiceTest.java | 36 +++++ 3 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java 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 d542fb7e..2fc076f1 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -1,16 +1,16 @@ package com.etesync.syncadapter.journalmanager; +import com.etesync.syncadapter.App; +import com.etesync.syncadapter.GsonHelper; import com.google.gson.reflect.TypeToken; import org.spongycastle.util.Arrays; +import java.io.IOException; import java.lang.reflect.Type; import java.util.List; import java.util.UUID; -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.GsonHelper; - import lombok.Getter; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java new file mode 100644 index 00000000..49487290 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java @@ -0,0 +1,150 @@ +package com.etesync.syncadapter.journalmanager; + +import com.etesync.syncadapter.GsonHelper; + +import org.apache.commons.codec.Charsets; +import org.spongycastle.util.Arrays; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import lombok.Getter; +import lombok.Setter; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +import static com.etesync.syncadapter.journalmanager.Crypto.CryptoManager.HMAC_SIZE; +import static com.etesync.syncadapter.journalmanager.Crypto.toHex; + +public class UserInfoManager extends BaseManager { + public UserInfoManager(OkHttpClient httpClient, HttpUrl remote) { + this.remote = remote.newBuilder() + .addPathSegments("api/v1/user") + .addPathSegment("") + .build(); + + this.client = httpClient; + } + + public UserInfo get(Crypto.CryptoManager cryptoManager, String owner) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { + HttpUrl remote = this.remote.newBuilder().addPathSegment(owner).addPathSegment("").build(); + Request request = new Request.Builder() + .get() + .url(remote) + .build(); + + Response response; + try { + response = newCall(request); + } catch (Exceptions.HttpException e) { + if (e.status == HttpURLConnection.HTTP_NOT_FOUND) { + return null; + } else { + throw e; + } + } + + ResponseBody body = response.body(); + UserInfo ret = GsonHelper.gson.fromJson(body.charStream(), UserInfo.class); + ret.verify(cryptoManager); + ret.setOwner(owner); + + return ret; + } + + public void delete(UserInfo userInfo) throws Exceptions.HttpException { + HttpUrl remote = this.remote.newBuilder().addPathSegment(userInfo.getOwner()).addPathSegment("").build(); + Request request = new Request.Builder() + .delete() + .url(remote) + .build(); + + newCall(request); + } + + public void create(UserInfo userInfo) throws Exceptions.HttpException { + RequestBody body = RequestBody.create(JSON, userInfo.toJson()); + + Request request = new Request.Builder() + .post(body) + .url(remote) + .build(); + + newCall(request); + } + + public void update(UserInfo userInfo) throws Exceptions.HttpException { + HttpUrl remote = this.remote.newBuilder().addPathSegment(userInfo.getOwner()).addPathSegment("").build(); + RequestBody body = RequestBody.create(JSON, userInfo.toJson()); + + Request request = new Request.Builder() + .put(body) + .url(remote) + .build(); + + newCall(request); + } + + public static class UserInfo { + @Setter + @Getter + private transient String owner; + @Getter + private byte version; + @Getter + private byte[] pubkey; + private byte[] content; + + public byte[] getContent(Crypto.CryptoManager crypto) { + byte[] content = Arrays.copyOfRange(this.content, HMAC_SIZE, this.content.length); + return crypto.decrypt(content); + } + + void setContent(Crypto.CryptoManager crypto, byte[] rawContent) { + byte[] content = crypto.encrypt(rawContent); + this.content = Arrays.concatenate(calculateHmac(crypto, content), content); + } + + void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { + if (this.content == null) { + // Nothing to verify. + return; + } + + byte[] hmac = Arrays.copyOfRange(this.content, 0, HMAC_SIZE); + byte[] content = Arrays.copyOfRange(this.content, HMAC_SIZE, this.content.length); + + byte[] correctHash = calculateHmac(crypto, content); + if (!Arrays.areEqual(hmac, correctHash)) { + throw new Exceptions.IntegrityException("Bad HMAC. " + toHex(hmac) + " != " + toHex(correctHash)); + } + } + + private byte[] calculateHmac(Crypto.CryptoManager crypto, byte[] content) { + return crypto.hmac(Arrays.concatenate(content, pubkey)); + } + + private UserInfo() { + } + + public UserInfo(Crypto.CryptoManager crypto, String owner, byte[] pubkey, byte[] content) { + this.owner = owner; + this.pubkey = pubkey; + version = crypto.getVersion(); + setContent(crypto, content); + } + + public static UserInfo generate(Crypto.CryptoManager cryptoManager, String owner) throws IOException { + Crypto.AsymmetricKeyPair keyPair = Crypto.generateKeyPair(); + return new UserInfo(cryptoManager, owner, keyPair.getPublicKey(), keyPair.getPrivateKey()); + } + + String toJson() { + return GsonHelper.gson.toJson(this, getClass()); + } + } +} diff --git a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java index 626bbf9a..5b4d03c7 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java @@ -12,6 +12,7 @@ import com.etesync.syncadapter.App; import com.etesync.syncadapter.HttpClient; import com.etesync.syncadapter.model.CollectionInfo; +import org.apache.commons.codec.Charsets; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -30,7 +31,9 @@ import okio.BufferedSink; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; public class ServiceTest { private OkHttpClient httpClient; @@ -196,4 +199,37 @@ public class ServiceTest { } assertNotNull(caught); } + + + @Test + public void testUserInfo() throws IOException, Exceptions.HttpException, Exceptions.GenericCryptoException, Exceptions.IntegrityException { + Crypto.CryptoManager cryptoManager = new Crypto.CryptoManager(Constants.CURRENT_VERSION, Helpers.keyBase64, "userInfo"); + UserInfoManager.UserInfo userInfo, userInfo2; + UserInfoManager manager = new UserInfoManager(httpClient, remote); + + // Get when there's nothing + userInfo = manager.get(cryptoManager, Helpers.USER); + assertNull(userInfo); + + // Create + userInfo = UserInfoManager.UserInfo.generate(cryptoManager, Helpers.USER); + manager.create(userInfo); + + // Get + userInfo2 = manager.get(cryptoManager, Helpers.USER); + assertNotNull(userInfo2); + assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager)); + + // Update + userInfo.setContent(cryptoManager, "test".getBytes(Charsets.UTF_8)); + manager.update(userInfo); + userInfo2 = manager.get(cryptoManager, Helpers.USER); + assertNotNull(userInfo2); + assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager)); + + // Delete + manager.delete(userInfo); + userInfo = manager.get(cryptoManager, Helpers.USER); + assertNull(userInfo); + } } From efe832ddb45a4462313c64c2b61f72a65587004f Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 12 Apr 2017 17:23:14 +0100 Subject: [PATCH 04/23] Journalmanager: Add api for the members endpoint. This API controls the members of a journal, that is, access control. --- .../journalmanager/JournalManager.java | 59 +++++++++++++++++++ .../journalmanager/ServiceTest.java | 26 +++++++- 2 files changed, 84 insertions(+), 1 deletion(-) 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 2fc076f1..de5b2095 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -26,6 +26,8 @@ import static com.etesync.syncadapter.journalmanager.Crypto.toHex; public class JournalManager extends BaseManager { final static private Type journalType = new TypeToken>() { }.getType(); + final static private Type memberType = new TypeToken>() { + }.getType(); public JournalManager(OkHttpClient httpClient, HttpUrl remote) { @@ -90,6 +92,43 @@ public class JournalManager extends BaseManager { newCall(request); } + private HttpUrl getMemberRemote(Journal journal) { + return this.remote.resolve(journal.getUid() + "/members/"); + } + + public List listMembers(Journal journal) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { + Request request = new Request.Builder() + .get() + .url(getMemberRemote(journal)) + .build(); + + Response response = newCall(request); + ResponseBody body = response.body(); + return GsonHelper.gson.fromJson(body.charStream(), memberType); + } + + public void deleteMember(Journal journal, Member member) throws Exceptions.HttpException { + RequestBody body = RequestBody.create(JSON, member.toJson()); + + Request request = new Request.Builder() + .delete(body) + .url(getMemberRemote(journal)) + .build(); + + newCall(request); + } + + public void addMember(Journal journal, Member member) throws Exceptions.HttpException { + RequestBody body = RequestBody.create(JSON, member.toJson()); + + Request request = new Request.Builder() + .post(body) + .url(getMemberRemote(journal)) + .build(); + + newCall(request); + } + public static class Journal extends Base { @Getter private int version = -1; @@ -140,4 +179,24 @@ public class JournalManager extends BaseManager { return ret; } } + + public static class Member { + @Getter + private String user; + @Getter + private byte[] key; + + @SuppressWarnings("unused") + private Member() { + } + + public Member(String user, byte[] encryptedKey) { + this.user = user; + this.key = encryptedKey; + } + + String toJson() { + return GsonHelper.gson.toJson(this, getClass()); + } + } } diff --git a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java index 5b4d03c7..b98bdf8b 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java @@ -31,7 +31,6 @@ import okio.BufferedSink; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -232,4 +231,29 @@ public class ServiceTest { userInfo = manager.get(cryptoManager, Helpers.USER); assertNull(userInfo); } + + + @Test + public void testJournalMember() throws IOException, Exceptions.HttpException, Exceptions.GenericCryptoException, Exceptions.IntegrityException { + JournalManager journalManager = new JournalManager(httpClient, remote); + CollectionInfo info = CollectionInfo.defaultForServiceType(CollectionInfo.Type.ADDRESS_BOOK); + info.uid = JournalManager.Journal.genUid(); + info.displayName = "Test"; + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, Helpers.keyBase64, info.uid); + JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); + journalManager.putJournal(journal); + + assertEquals(journalManager.listMembers(journal).size(), 0); + + // Test inviting ourselves + JournalManager.Member member = new JournalManager.Member(Helpers.USER, "test".getBytes(Charsets.UTF_8)); + journalManager.addMember(journal, member); + + assertEquals(journalManager.listMembers(journal).size(), 1); + + // Uninviting ourselves + journalManager.deleteMember(journal, member); + + assertEquals(journalManager.listMembers(journal).size(), 0); + } } From a4a32045e8777d6ff45626297e18497e09f92e36 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 12 Apr 2017 17:51:08 +0100 Subject: [PATCH 05/23] Journal: get and persist owner and key. The server was changed so the owner of the journal, and the encrypted key (if a shared journal) would be exposed. This change fetches it, and saves it. --- .../java/com/etesync/syncadapter/App.java | 2 +- .../journalmanager/JournalManager.java | 6 ++++ .../syncadapter/model/JournalModel.java | 11 ++++++- .../syncadapter/SyncAdapterService.java | 33 ++++++++++--------- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/App.java b/app/src/main/java/com/etesync/syncadapter/App.java index e567f09c..3b956d7b 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.java +++ b/app/src/main/java/com/etesync/syncadapter/App.java @@ -227,7 +227,7 @@ public class App extends Application { public EntityDataStore getData() { if (dataStore == null) { // override onUpgrade to handle migrating to a new version - DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 1); + DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 2); Configuration configuration = source.getConfiguration(); dataStore = new EntityDataStore<>(configuration); } 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 de5b2095..f190b449 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -130,6 +130,12 @@ public class JournalManager extends BaseManager { } public static class Journal extends Base { + @Getter + private String owner; + + @Getter + private byte[] key; + @Getter private int version = -1; diff --git a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java index f4320bb1..96f8b415 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java +++ b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java @@ -30,6 +30,10 @@ public class JournalModel { @Convert(CollectionInfoConverter.class) CollectionInfo info; + String owner; + + byte[] encryptedKey; + long service; boolean deleted; @@ -51,10 +55,15 @@ public class JournalModel { this.service = info.serviceID; } + public static List getJournals(EntityDataStore data, long service) { + return data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(service).and(JournalEntity.DELETED.eq(false))).get().toList(); + } + public static List getCollections(EntityDataStore data, long service) { List ret = new LinkedList<>(); - for (JournalEntity journal : data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(service).and(JournalEntity.DELETED.eq(false))).get()) { + List journals = getJournals(data, service); + for (JournalEntity journal : journals) { // FIXME: For some reason this isn't always being called, manually do it here. journal.afterLoad(); ret.add(journal.getInfo()); 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 85269e78..f639615e 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -28,6 +28,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.util.Pair; import java.util.HashMap; import java.util.LinkedList; @@ -153,7 +154,7 @@ public abstract class SyncAdapterService extends Service { AccountSettings settings = new AccountSettings(context, account); JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); - List collections = new LinkedList<>(); + List> journals = new LinkedList<>(); for (JournalManager.Journal journal : journalsManager.getJournals(settings.password())) { Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid()); @@ -161,22 +162,22 @@ public abstract class SyncAdapterService extends Service { info.updateFromJournal(journal); if (info.type.equals(serviceType)) { - collections.add(info); + journals.add(new Pair<>(journal, info)); } } - if (collections.isEmpty()) { + if (journals.isEmpty()) { CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType); info.uid = JournalManager.Journal.genUid(); Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); journalsManager.putJournal(journal); - collections.add(info); + journals.add(new Pair<>(journal, info)); } db.beginTransactionNonExclusive(); try { - saveCollections(db, collections); + saveCollections(db, journals); db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -186,30 +187,32 @@ public abstract class SyncAdapterService extends Service { } } - private void saveCollections(SQLiteDatabase db, Iterable collections) { + private void saveCollections(SQLiteDatabase db, Iterable> journals) { Long service = dbHelper.getService(db, account, serviceType.toString()); EntityDataStore data = ((App) context.getApplicationContext()).getData(); - Map existing = new HashMap<>(); - List existingList = JournalEntity.getCollections(data, service); - for (CollectionInfo info : existingList) { - existing.put(info.uid, info); + Map existing = new HashMap<>(); + for (JournalEntity journalEntity : JournalEntity.getJournals(data, service)) { + existing.put(journalEntity.getUid(), journalEntity); } - for (CollectionInfo collection : collections) { - App.log.log(Level.FINE, "Saving collection", collection.uid); + for (Pair pair : journals) { + JournalManager.Journal journal = pair.first; + CollectionInfo collection = pair.second; + App.log.log(Level.FINE, "Saving collection", journal.getUid()); collection.serviceID = service; JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, collection); + journalEntity.setOwner(journal.getOwner()); + journalEntity.setEncryptedKey(journal.getKey()); data.upsert(journalEntity); existing.remove(collection.uid); } - for (CollectionInfo collection : existing.values()) { - App.log.log(Level.FINE, "Deleting collection", collection.uid); + for (JournalEntity journalEntity : existing.values()) { + App.log.log(Level.FINE, "Deleting collection", journalEntity.getUid()); - JournalEntity journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(collection.uid)).limit(1).get().first(); journalEntity.setDeleted(true); data.update(journalEntity); } From 88ceeaa2a5a79ab5376b2c5f48c28071a7aa0937 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 00:04:35 +0100 Subject: [PATCH 06/23] Entry and journal: fix uniqueness to be composited, and not just by uid. Before this change, uid was unique on its own, this was wrong, because due to shared journals, we can have the same journal in two accounts, and we can thus have both journal and entry UIDs more than once. This fixes the constraint to be unique for journal, uid, and service, uid combinations. This is currently disabled for journals because of a bug in requery. --- .../com/etesync/syncadapter/model/JournalModel.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java index 96f8b415..1e1aaca2 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java +++ b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java @@ -15,16 +15,19 @@ import io.requery.ManyToOne; import io.requery.Persistable; import io.requery.PostLoad; import io.requery.ReferentialAction; +import io.requery.Table; import io.requery.sql.EntityDataStore; public class JournalModel { + // FIXME: Add unique constraint on the uid + service combination. Can't do it at the moment because requery is broken. @Entity + @Table(name = "Journal") public static abstract class Journal { @Key @Generated int id; - @Column(length = 64, unique = true, nullable = false) + @Column(length = 64, nullable = false) String uid; @Convert(CollectionInfoConverter.class) @@ -34,6 +37,7 @@ public class JournalModel { byte[] encryptedKey; + @Index(value = "uid_unique") long service; boolean deleted; @@ -102,18 +106,20 @@ public class JournalModel { } @Entity + @Table(name = "Entry", uniqueIndexes = "entry_unique_together") public static abstract class Entry { @Key @Generated int id; - @Column(length = 64, unique = true, nullable = false) + @Index("entry_unique_together") + @Column(length = 64, nullable = false) String uid; @Convert(SyncEntryConverter.class) SyncEntry content; - @Index("journal_index") + @Index("entry_unique_together") @ForeignKey(update = ReferentialAction.CASCADE) @ManyToOne Journal journal; From 8b79529a9453a253b9ccf41705ae69cc9f58f395 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 10:17:58 +0100 Subject: [PATCH 07/23] Bump version. --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 43bcd491..f009e076 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ android { minSdkVersion 16 targetSdkVersion 25 - versionCode 10 + versionCode 11 buildConfigField "long", "buildTime", System.currentTimeMillis() + "L" buildConfigField "boolean", "customCerts", "true" @@ -26,7 +26,7 @@ android { productFlavors { standard { - versionName "0.13.0" + versionName "0.14.0" } } From e2f206e02e71e53caf5b7b5aad25c6105ad37d41 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 10:48:40 +0100 Subject: [PATCH 08/23] Services: Move to a requery model instead of raw SQL and improve models. Having it in raw sql was slowing down development, and was error-prone. It's much cleaner now, easier to handle, and enables us to develop faster. In this change I also fixed the fetching of journals to be by service and id, not just id, because just id is not guaranteed to be unique. --- .../syncadapter/AccountUpdateService.java | 39 +++-------- .../java/com/etesync/syncadapter/App.java | 33 ++++++++- .../syncadapter/PackageChangedReceiver.java | 28 +------- .../syncadapter/model/CollectionInfo.java | 10 ++- .../syncadapter/model/JournalModel.java | 49 +++++++++---- .../etesync/syncadapter/model/ServiceDB.java | 59 +--------------- .../CalendarsSyncAdapterService.java | 68 ++++++++----------- .../ContactsSyncAdapterService.java | 10 +-- .../syncadapter/SyncAdapterService.java | 18 ++--- .../syncadapter/syncadapter/SyncManager.java | 7 +- .../syncadapter/ui/AccountActivity.java | 28 +++----- .../ui/CreateCollectionFragment.java | 25 ++----- .../syncadapter/ui/DebugInfoActivity.java | 9 ++- .../ui/DeleteCollectionFragment.java | 2 +- .../ui/EditCollectionActivity.java | 2 +- .../ui/ViewCollectionActivity.java | 6 +- .../ui/journalviewer/ListEntriesFragment.java | 2 +- .../ui/setup/SetupEncryptionFragment.java | 29 ++++---- 18 files changed, 182 insertions(+), 242 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/AccountUpdateService.java b/app/src/main/java/com/etesync/syncadapter/AccountUpdateService.java index af5d6a31..4f6fb65d 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountUpdateService.java +++ b/app/src/main/java/com/etesync/syncadapter/AccountUpdateService.java @@ -13,17 +13,12 @@ import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.app.Service; import android.content.Intent; -import android.database.Cursor; import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; import android.os.Binder; import android.os.IBinder; import android.support.annotation.NonNull; -import android.text.TextUtils; -import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceDB.OpenHelper; -import com.etesync.syncadapter.model.ServiceDB.Services; +import com.etesync.syncadapter.model.ServiceEntity; import java.lang.ref.WeakReference; import java.util.HashSet; @@ -105,31 +100,17 @@ public class AccountUpdateService extends Service { void cleanupAccounts() { App.log.info("Cleaning up orphaned accounts"); - final OpenHelper dbHelper = new OpenHelper(this); - try { - SQLiteDatabase db = dbHelper.getWritableDatabase(); + List sqlAccountNames = new LinkedList<>(); + AccountManager am = AccountManager.get(this); + for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE)) + sqlAccountNames.add(account.name); - List sqlAccountNames = new LinkedList<>(); - AccountManager am = AccountManager.get(this); - for (Account account : am.getAccountsByType(Constants.ACCOUNT_TYPE)) - sqlAccountNames.add(DatabaseUtils.sqlEscapeString(account.name)); + EntityDataStore data = ((App) getApplication()).getData(); - EntityDataStore data = ((App) getApplication()).getData(); - - if (sqlAccountNames.isEmpty()) { - data.delete(JournalEntity.class).get().value(); - db.delete(Services._TABLE, null, null); - } else { - Cursor cur = db.query(Services._TABLE, new String[]{Services.ID}, Services.ACCOUNT_NAME + " NOT IN (" + TextUtils.join(",", sqlAccountNames) + ")", null, null, null, null); - cur.moveToFirst(); - while(!cur.isAfterLast()) { - data.delete(JournalEntity.class).where(JournalEntity.SERVICE.eq(cur.getLong(0))).get().value(); - cur.moveToNext(); - } - db.delete(Services._TABLE, Services.ACCOUNT_NAME + " NOT IN (" + TextUtils.join(",", sqlAccountNames) + ")", null); - } - } finally { - dbHelper.close(); + if (sqlAccountNames.isEmpty()) { + data.delete(ServiceEntity.class).get().value(); + } else { + data.delete(ServiceEntity.class).where(ServiceEntity.ACCOUNT.notIn(sqlAccountNames)).get().value(); } } } diff --git a/app/src/main/java/com/etesync/syncadapter/App.java b/app/src/main/java/com/etesync/syncadapter/App.java index 3b956d7b..b0433329 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.java +++ b/app/src/main/java/com/etesync/syncadapter/App.java @@ -40,8 +40,10 @@ import com.etesync.syncadapter.log.LogcatHandler; import com.etesync.syncadapter.log.PlainTextFormatter; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; +import com.etesync.syncadapter.model.JournalModel; import com.etesync.syncadapter.model.Models; import com.etesync.syncadapter.model.ServiceDB; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.Settings; import com.etesync.syncadapter.resource.LocalAddressBook; import com.etesync.syncadapter.resource.LocalCalendar; @@ -227,7 +229,7 @@ public class App extends Application { public EntityDataStore getData() { if (dataStore == null) { // override onUpgrade to handle migrating to a new version - DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 2); + DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 3); Configuration configuration = source.getConfiguration(); dataStore = new EntityDataStore<>(configuration); } @@ -257,7 +259,7 @@ public class App extends Application { List collections = readCollections(dbHelper); for (CollectionInfo info : collections) { - JournalEntity journalEntity = new JournalEntity(info); + JournalEntity journalEntity = new JournalEntity(data, info); data.insert(journalEntity); } @@ -291,6 +293,12 @@ public class App extends Application { if (fromVersion < 10) { HintManager.setHintSeen(this, AccountsActivity.HINT_ACCOUNT_ADD, true); } + + if (fromVersion < 11) { + ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(this); + + migrateServices(dbHelper); + } } public static class AppUpdatedReceiver extends BroadcastReceiver { @@ -321,4 +329,25 @@ public class App extends Application { } return collections; } + + public void migrateServices(ServiceDB.OpenHelper dbHelper) { + @Cleanup SQLiteDatabase db = dbHelper.getReadableDatabase(); + EntityDataStore data = this.getData(); + @Cleanup Cursor cursor = db.query(ServiceDB.Services._TABLE, null, null, null, null, null, null); + while (cursor.moveToNext()) { + ContentValues values = new ContentValues(); + DatabaseUtils.cursorRowToContentValues(cursor, values); + ServiceEntity service = new ServiceEntity(); + service.setAccount(values.getAsString(ServiceDB.Services.ACCOUNT_NAME)); + service.setType(CollectionInfo.Type.valueOf(values.getAsString(ServiceDB.Services.SERVICE))); + data.insert(service); + + for (JournalEntity journalEntity : data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(values.getAsLong(ServiceDB.Services.ID))).get()) { + journalEntity.setServiceModel(service); + data.update(journalEntity); + } + } + + db.delete(ServiceDB.Services._TABLE, null, null); + } } diff --git a/app/src/main/java/com/etesync/syncadapter/PackageChangedReceiver.java b/app/src/main/java/com/etesync/syncadapter/PackageChangedReceiver.java index f0ffce08..4eac385d 100644 --- a/app/src/main/java/com/etesync/syncadapter/PackageChangedReceiver.java +++ b/app/src/main/java/com/etesync/syncadapter/PackageChangedReceiver.java @@ -8,22 +8,13 @@ package com.etesync.syncadapter; -import android.accounts.Account; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.Bundle; import android.support.annotation.NonNull; -import com.etesync.syncadapter.model.ServiceDB; -import com.etesync.syncadapter.model.ServiceDB.Services; import com.etesync.syncadapter.resource.LocalTaskList; -import at.bitfire.ical4android.TaskProvider; -import lombok.Cleanup; public class PackageChangedReceiver extends BroadcastReceiver { @@ -40,24 +31,7 @@ public class PackageChangedReceiver extends BroadcastReceiver { App.log.info("Package (un)installed; OpenTasks provider now available = " + tasksInstalled); // check all accounts and (de)activate OpenTasks if a CalDAV service is defined - @Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(context); - SQLiteDatabase db = dbHelper.getReadableDatabase(); - - @Cleanup Cursor cursor = db.query(Services._TABLE, new String[] { Services.ACCOUNT_NAME }, - Services.SERVICE + "=?", new String[] { Services.SERVICE_CALDAV }, null, null, null); - while (cursor.moveToNext()) { - Account account = new Account(cursor.getString(0), Constants.ACCOUNT_TYPE); - - if (tasksInstalled) { - if (ContentResolver.getIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority) <= 0) { - ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 1); - ContentResolver.setSyncAutomatically(account, TaskProvider.ProviderName.OpenTasks.authority, true); - ContentResolver.addPeriodicSync(account, TaskProvider.ProviderName.OpenTasks.authority, new Bundle(), Constants.DEFAULT_SYNC_INTERVAL); - } - } else - ContentResolver.setIsSyncable(account, TaskProvider.ProviderName.OpenTasks.authority, 0); - - } + // FIXME: Do something if we ever bring back tasks. } } 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 7b3729da..1e7b0abb 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java +++ b/app/src/main/java/com/etesync/syncadapter/model/CollectionInfo.java @@ -18,6 +18,8 @@ import com.google.gson.annotations.Expose; import java.io.Serializable; +import io.requery.Persistable; +import io.requery.sql.EntityDataStore; import lombok.ToString; @ToString(exclude = {"id"}) @@ -25,7 +27,7 @@ public class CollectionInfo implements Serializable { @Deprecated public long id; - public Long serviceID; + public int serviceID; public enum Type { ADDRESS_BOOK, @@ -80,7 +82,7 @@ public class CollectionInfo implements Serializable { public static CollectionInfo fromDB(ContentValues values) { CollectionInfo info = new CollectionInfo(); info.id = values.getAsLong(Collections.ID); - info.serviceID = values.getAsLong(Collections.SERVICE_ID); + info.serviceID = values.getAsInteger(Collections.SERVICE_ID); info.uid = values.getAsString(Collections.URL); info.readOnly = values.getAsInteger(Collections.READ_ONLY) != 0; @@ -95,6 +97,10 @@ public class CollectionInfo implements Serializable { return info; } + public ServiceEntity getServiceEntity(EntityDataStore data) { + return data.findByKey(ServiceEntity.class, serviceID); + } + public static CollectionInfo fromJson(String json) { return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, CollectionInfo.class); } diff --git a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java index 1e1aaca2..ad383ff1 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java +++ b/app/src/main/java/com/etesync/syncadapter/model/JournalModel.java @@ -37,14 +37,19 @@ public class JournalModel { byte[] encryptedKey; - @Index(value = "uid_unique") + + @Deprecated long service; + @ForeignKey(update = ReferentialAction.CASCADE) + @ManyToOne + Service serviceModel; + boolean deleted; @PostLoad void afterLoad() { - this.info.serviceID = service; + this.info.serviceID = this.serviceModel.id; this.info.uid = uid; } @@ -52,21 +57,21 @@ public class JournalModel { this.deleted = false; } - public Journal(CollectionInfo info) { + public Journal(EntityDataStore data, CollectionInfo info) { this(); this.info = info; this.uid = info.uid; - this.service = info.serviceID; + this.serviceModel = info.getServiceEntity(data); } - public static List getJournals(EntityDataStore data, long service) { - return data.select(JournalEntity.class).where(JournalEntity.SERVICE.eq(service).and(JournalEntity.DELETED.eq(false))).get().toList(); + public static List getJournals(EntityDataStore data, ServiceEntity serviceEntity) { + return data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.DELETED.eq(false))).get().toList(); } - public static List getCollections(EntityDataStore data, long service) { + public static List getCollections(EntityDataStore data, ServiceEntity serviceEntity) { List ret = new LinkedList<>(); - List journals = getJournals(data, service); + List journals = getJournals(data, serviceEntity); for (JournalEntity journal : journals) { // FIXME: For some reason this isn't always being called, manually do it here. journal.afterLoad(); @@ -76,8 +81,8 @@ public class JournalModel { return ret; } - public static JournalEntity fetch(EntityDataStore data, String url) { - JournalEntity ret = data.select(JournalEntity.class).where(JournalEntity.UID.eq(url)).limit(1).get().firstOrNull(); + public static JournalEntity fetch(EntityDataStore data, ServiceEntity serviceEntity, String uid) { + JournalEntity ret = data.select(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(serviceEntity).and(JournalEntity.UID.eq(uid))).limit(1).get().firstOrNull(); if (ret != null) { // FIXME: For some reason this isn't always being called, manually do it here. ret.afterLoad(); @@ -86,9 +91,9 @@ public class JournalModel { } public static JournalEntity fetchOrCreate(EntityDataStore data, CollectionInfo collection) { - JournalEntity journalEntity = fetch(data, collection.uid); + JournalEntity journalEntity = fetch(data, collection.getServiceEntity(data), collection.uid); if (journalEntity == null) { - journalEntity = new JournalEntity(collection); + journalEntity = new JournalEntity(data, collection); } else { journalEntity.setInfo(collection); } @@ -125,6 +130,26 @@ public class JournalModel { Journal journal; } + + @Entity + @Table(name = "Service", uniqueIndexes = "service_unique_together") + public static abstract class Service { + @Key + @Generated + int id; + + @Index(value = "service_unique_together") + @Column(nullable = false) + String account; + + @Index(value = "service_unique_together") + CollectionInfo.Type type; + + public static ServiceEntity fetch(EntityDataStore data, String account, CollectionInfo.Type type) { + return data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account).and(ServiceEntity.TYPE.eq(type))).limit(1).get().firstOrNull(); + } + } + static class CollectionInfoConverter implements Converter { @Override public Class getMappedType() { diff --git a/app/src/main/java/com/etesync/syncadapter/model/ServiceDB.java b/app/src/main/java/com/etesync/syncadapter/model/ServiceDB.java index 83e95b50..196b35ae 100644 --- a/app/src/main/java/com/etesync/syncadapter/model/ServiceDB.java +++ b/app/src/main/java/com/etesync/syncadapter/model/ServiceDB.java @@ -33,17 +33,13 @@ public class ServiceDB { VALUE = "value"; } + @Deprecated public static class Services { public static final String _TABLE = "services", ID = "_id", ACCOUNT_NAME = "accountName", SERVICE = "service"; - - // allowed values for SERVICE column - public static final String - SERVICE_CALDAV = CollectionInfo.Type.CALENDAR.toString(), - SERVICE_CARDDAV = CollectionInfo.Type.ADDRESS_BOOK.toString(); } @Deprecated @@ -90,15 +86,8 @@ public class ServiceDB { db.execSQL("CREATE TABLE " + Settings._TABLE + "(" + Settings.NAME + " TEXT NOT NULL," + Settings.VALUE + " TEXT NOT NULL" + - ")"); + ")"); db.execSQL("CREATE UNIQUE INDEX settings_name ON " + Settings._TABLE + " (" + Settings.NAME + ")"); - - db.execSQL("CREATE TABLE " + Services._TABLE + "(" + - Services.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - Services.ACCOUNT_NAME + " TEXT NOT NULL," + - Services.SERVICE + " TEXT NOT NULL" + - ")"); - db.execSQL("CREATE UNIQUE INDEX services_account ON " + Services._TABLE + " (" + Services.ACCOUNT_NAME + "," + Services.SERVICE + ")"); } @Override @@ -112,7 +101,7 @@ public class ServiceDB { db.beginTransactionNonExclusive(); // iterate through all tables - @Cleanup Cursor cursorTables = db.query("sqlite_master", new String[] { "name" }, "type='table'", null, null, null, null); + @Cleanup Cursor cursorTables = db.query("sqlite_master", new String[]{"name"}, "type='table'", null, null, null, null); while (cursorTables.moveToNext()) { String table = cursorTables.getString(0); sb.append(table).append("\n"); @@ -153,47 +142,5 @@ public class ServiceDB { } db.endTransaction(); } - - @NonNull - public Account getServiceAccount(SQLiteDatabase db, long service) { - @Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.ACCOUNT_NAME}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null); - if (cursor.moveToNext()) { - return new Account(cursor.getString(0), Constants.ACCOUNT_TYPE); - } else - throw new IllegalArgumentException("Service not found"); - } - - @NonNull - public String getServiceType(SQLiteDatabase db, long service) { - @Cleanup Cursor cursor = db.query(Services._TABLE, new String[]{Services.SERVICE}, Services.ID + "=?", new String[]{String.valueOf(service)}, null, null, null); - if (cursor.moveToNext()) - return cursor.getString(0); - else - throw new IllegalArgumentException("Service not found"); - } - - @Nullable - public Long getService(@NonNull SQLiteDatabase db, @NonNull Account account, String service) { - @Cleanup Cursor c = db.query(Services._TABLE, new String[]{Services.ID}, - Services.ACCOUNT_NAME + "=? AND " + Services.SERVICE + "=?", new String[]{account.name, service}, null, null, null); - if (c.moveToNext()) - return c.getLong(0); - else - return null; - } - - @Nullable - public Long getService(@NonNull Account account, String service) { - @Cleanup SQLiteDatabase db = getReadableDatabase(); - return getService(db, account, service); - } } - - - public static void onRenameAccount(@NonNull SQLiteDatabase db, @NonNull String oldName, @NonNull String newName) { - ContentValues values = new ContentValues(1); - values.put(Services.ACCOUNT_NAME, newName); - db.update(Services._TABLE, values, Services.ACCOUNT_NAME + "=?", new String[] { oldName }); - } - } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java index 1d9126ed..cd4ba3a7 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java @@ -14,7 +14,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SyncResult; -import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.os.Bundle; import android.provider.CalendarContract; @@ -27,8 +26,8 @@ import com.etesync.syncadapter.R; import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceDB; -import com.etesync.syncadapter.model.ServiceDB.Services; +import com.etesync.syncadapter.model.JournalModel; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.ui.DebugInfoActivity; @@ -109,47 +108,40 @@ public class CalendarsSyncAdapterService extends SyncAdapterService { } private void updateLocalCalendars(ContentProviderClient provider, Account account, AccountSettings settings) throws CalendarStorageException { - ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); - try { - // enumerate remote and local calendars - SQLiteDatabase db = dbHelper.getReadableDatabase(); - Long service = dbHelper.getService(db, account, Services.SERVICE_CALDAV); + EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); + ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.CALENDAR); - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - Map remote = new HashMap<>(); - List remoteCollections = JournalEntity.getCollections(data, service); - for (CollectionInfo info : remoteCollections) { - remote.put(info.uid, info); - } + Map remote = new HashMap<>(); + List remoteCollections = JournalEntity.getCollections(data, service); + for (CollectionInfo info : remoteCollections) { + remote.put(info.uid, info); + } - LocalCalendar[] local = (LocalCalendar[])LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null); + LocalCalendar[] local = (LocalCalendar[]) LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null); - boolean updateColors = settings.getManageCalendarColors(); + boolean updateColors = settings.getManageCalendarColors(); - // delete obsolete local calendar - for (LocalCalendar calendar : local) { - String url = calendar.getName(); - if (!remote.containsKey(url)) { - App.log.fine("Deleting obsolete local calendar " + url); - calendar.delete(); - } else { - // remote CollectionInfo found for this local collection, update data - CollectionInfo info = remote.get(url); - App.log.fine("Updating local calendar " + url + " with " + info); - calendar.update(info, updateColors); - // we already have a local calendar for this remote collection, don't take into consideration anymore - remote.remove(url); - } - } - - // create new local calendars - for (String url : remote.keySet()) { + // delete obsolete local calendar + for (LocalCalendar calendar : local) { + String url = calendar.getName(); + if (!remote.containsKey(url)) { + App.log.fine("Deleting obsolete local calendar " + url); + calendar.delete(); + } else { + // remote CollectionInfo found for this local collection, update data CollectionInfo info = remote.get(url); - App.log.info("Adding local calendar list " + info); - LocalCalendar.create(account, provider, info); + App.log.fine("Updating local calendar " + url + " with " + info); + calendar.update(info, updateColors); + // we already have a local calendar for this remote collection, don't take into consideration anymore + remote.remove(url); } - } finally { - dbHelper.close(); + } + + // create new local calendars + for (String url : remote.keySet()) { + CollectionInfo info = remote.get(url); + App.log.info("Adding local calendar list " + info); + LocalCalendar.create(account, provider, info); } } } diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java index fc2f55cf..74d8ead0 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java @@ -14,7 +14,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SyncResult; -import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import com.etesync.syncadapter.AccountSettings; @@ -26,7 +25,9 @@ import com.etesync.syncadapter.R; import com.etesync.syncadapter.journalmanager.Exceptions; 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.model.ServiceEntity; import com.etesync.syncadapter.ui.DebugInfoActivity; import java.util.logging.Level; @@ -65,11 +66,12 @@ public class ContactsSyncAdapterService extends SyncAdapterService { new RefreshCollections(account, CollectionInfo.Type.ADDRESS_BOOK).run(); - SQLiteDatabase db = dbHelper.getReadableDatabase(); - Long service = dbHelper.getService(db, account, ServiceDB.Services.SERVICE_CARDDAV); + EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); + + ServiceEntity service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.ADDRESS_BOOK); + if (service != null) { HttpUrl principal = HttpUrl.get(settings.getUri()); - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); CollectionInfo info = JournalEntity.getCollections(data, service).get(0); try { ContactsSyncManager syncManager = new ContactsSyncManager(getContext(), account, settings, extras, authority, provider, syncResult, principal, info); 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 f639615e..25bd22bb 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -47,7 +47,9 @@ 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.model.ServiceEntity; import com.etesync.syncadapter.ui.PermissionsActivity; import io.requery.Persistable; @@ -175,22 +177,16 @@ public abstract class SyncAdapterService extends Service { journals.add(new Pair<>(journal, info)); } - db.beginTransactionNonExclusive(); - try { - saveCollections(db, journals); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } + saveCollections(journals); } finally { dbHelper.close(); } } - private void saveCollections(SQLiteDatabase db, Iterable> journals) { - Long service = dbHelper.getService(db, account, serviceType.toString()); - + private void saveCollections(Iterable> journals) { EntityDataStore data = ((App) context.getApplicationContext()).getData(); + ServiceEntity service = JournalModel.Service.fetch(data, account.name, serviceType); + Map existing = new HashMap<>(); for (JournalEntity journalEntity : JournalEntity.getJournals(data, service)) { existing.put(journalEntity.getUid(), journalEntity); @@ -201,7 +197,7 @@ public abstract class SyncAdapterService extends Service { CollectionInfo collection = pair.second; App.log.log(Level.FINE, "Saving collection", journal.getUid()); - collection.serviceID = service; + collection.serviceID = service.getId(); JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, collection); journalEntity.setOwner(journal.getOwner()); journalEntity.setEncryptedKey(journal.getKey()); 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 cf07d4a0..4db3a398 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java @@ -27,6 +27,8 @@ import com.etesync.syncadapter.journalmanager.JournalEntryManager; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.EntryEntity; import com.etesync.syncadapter.model.JournalEntity; +import com.etesync.syncadapter.model.JournalModel; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.model.SyncEntry; import com.etesync.syncadapter.resource.LocalCollection; import com.etesync.syncadapter.resource.LocalResource; @@ -108,7 +110,8 @@ abstract public class SyncManager { httpClient = HttpClient.create(context, account); data = ((App) context.getApplicationContext()).getData(); - info = JournalEntity.fetch(data, journalUid).getInfo(); + ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, serviceType); + info = JournalEntity.fetch(data, serviceEntity, journalUid).getInfo(); // dismiss previous error notifications notificationManager = new NotificationHelper(context, journalUid, notificationId()); @@ -232,7 +235,7 @@ abstract public class SyncManager { private JournalEntity getJournalEntity() { if (_journalEntity == null) - _journalEntity = data.select(JournalEntity.class).where(JournalEntity.UID.eq(journal.getUid())).limit(1).get().first(); + _journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), journal.getUid()); return _journalEntity; } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index cba38ac2..4ccfc867 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -24,8 +24,6 @@ import android.content.Intent; import android.content.Loader; import android.content.ServiceConnection; import android.content.SyncStatusObserver; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; @@ -57,8 +55,7 @@ import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.R; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.ServiceDB.OpenHelper; -import com.etesync.syncadapter.model.ServiceDB.Services; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.utils.HintManager; import com.etesync.syncadapter.utils.ShowcaseBuilder; @@ -71,7 +68,6 @@ import at.bitfire.cert4android.CustomCertManager; import at.bitfire.ical4android.TaskProvider; import io.requery.Persistable; import io.requery.sql.EntityDataStore; -import lombok.Cleanup; import tourguide.tourguide.ToolTip; import static android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE; @@ -318,31 +314,23 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu public AccountInfo loadInBackground() { AccountInfo info = new AccountInfo(); - @Cleanup OpenHelper dbHelper = new OpenHelper(getContext()); - SQLiteDatabase db = dbHelper.getReadableDatabase(); EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - @Cleanup Cursor cursor = db.query( - Services._TABLE, - new String[] { Services.ID, Services.SERVICE }, - Services.ACCOUNT_NAME + "=?", new String[] { account.name }, - null, null, null); - - while (cursor.moveToNext()) { - long id = cursor.getLong(0); - String service = cursor.getString(1); - if (Services.SERVICE_CARDDAV.equals(service)) { + for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { + long id = serviceEntity.getId(); + CollectionInfo.Type service = serviceEntity.getType(); + if (service.equals(CollectionInfo.Type.ADDRESS_BOOK)) { info.carddav = new AccountInfo.ServiceInfo(); info.carddav.id = id; info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY); - info.carddav.collections = JournalEntity.getCollections(data, id); - } else if (Services.SERVICE_CALDAV.equals(service)) { + info.carddav.collections = JournalEntity.getCollections(data, serviceEntity); + } else if (service.equals(CollectionInfo.Type.CALENDAR)) { info.caldav = new AccountInfo.ServiceInfo(); info.caldav.id = id; info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority); - info.caldav.collections = JournalEntity.getCollections(data, id); + info.caldav.collections = JournalEntity.getCollections(data, serviceEntity); } } return info; 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 4e86c044..fdaa671e 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.java @@ -35,7 +35,9 @@ 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.model.ServiceEntity; import io.requery.Persistable; import io.requery.sql.EntityDataStore; @@ -125,31 +127,21 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa @Override public Exception loadInBackground() { - ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); - try { String authority = null; - // now insert collection into database: - SQLiteDatabase db = dbHelper.getWritableDatabase(); + + EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); // 1. find service ID - String serviceType; if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { - serviceType = ServiceDB.Services.SERVICE_CARDDAV; authority = ContactsContract.AUTHORITY; } else if (info.type == CollectionInfo.Type.CALENDAR) { - serviceType = ServiceDB.Services.SERVICE_CALDAV; authority = CalendarContract.AUTHORITY; } else { throw new IllegalArgumentException("Collection must be an address book or calendar"); } - @Cleanup Cursor c = db.query(ServiceDB.Services._TABLE, new String[]{ServiceDB.Services.ID}, - ServiceDB.Services.ACCOUNT_NAME + "=? AND " + ServiceDB.Services.SERVICE + "=?", - new String[]{account.name, serviceType}, null, null, null - ); - if (!c.moveToNext()) - throw new IllegalStateException(); - long serviceID = c.getLong(0); + + ServiceEntity serviceEntity = JournalModel.Service.fetch(data, account.name, info.type); AccountSettings settings = new AccountSettings(getContext(), account); HttpUrl principal = HttpUrl.get(settings.getUri()); @@ -167,8 +159,7 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa } // 2. add collection to service - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); - info.serviceID = serviceID; + info.serviceID = serviceEntity.getId(); JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, info); data.upsert(journalEntity); @@ -180,8 +171,6 @@ public class CreateCollectionFragment extends DialogFragment implements LoaderMa return e; } catch (Exceptions.IntegrityException|Exceptions.GenericCryptoException e) { return e; - } finally { - dbHelper.close(); } return null; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java index 25178b54..f26eaeba 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java @@ -49,6 +49,7 @@ import com.etesync.syncadapter.journalmanager.Exceptions.HttpException; import com.etesync.syncadapter.model.EntryEntity; import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.ServiceDB; +import com.etesync.syncadapter.model.ServiceEntity; import io.requery.Persistable; import io.requery.sql.EntityDataStore; @@ -246,8 +247,14 @@ public class DebugInfoActivity extends AppCompatActivity implements LoaderManage dbHelper.dump(report); report.append("\n"); - report.append("JOURNALS DUMP\n"); + report.append("SERVICES DUMP\n"); EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); + for (ServiceEntity serviceEntity : data.select(ServiceEntity.class).get()) { + report.append(serviceEntity.toString() + "\n"); + } + report.append("\n"); + + report.append("JOURNALS DUMP\n"); List journals = data.select(JournalEntity.class).where(JournalEntity.DELETED.eq(false)).get().toList(); for (JournalEntity journal : journals) { report.append(journal.toString() + "\n"); 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 fcf93e60..88cc560f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/DeleteCollectionFragment.java @@ -123,7 +123,7 @@ public class DeleteCollectionFragment extends DialogFragment implements LoaderMa Crypto.CryptoManager crypto = new Crypto.CryptoManager(collectionInfo.version, settings.password(), collectionInfo.uid); journalManager.deleteJournal(new JournalManager.Journal(crypto, collectionInfo.toJson(), collectionInfo.uid)); - JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.uid); + JournalEntity journalEntity = JournalEntity.fetch(data, collectionInfo.getServiceEntity(data), collectionInfo.uid); journalEntity.setDeleted(true); data.update(journalEntity); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java index ac96e87e..20908bb7 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/EditCollectionActivity.java @@ -78,7 +78,7 @@ public class EditCollectionActivity extends CreateCollectionActivity { public void onDeleteCollection(MenuItem item) { EntityDataStore data = ((App) getApplication()).getData(); - int journalCount = data.count(JournalEntity.class).where(JournalEntity.SERVICE.eq(info.serviceID)).get().value(); + int journalCount = data.count(JournalEntity.class).where(JournalEntity.SERVICE_MODEL.eq(info.getServiceEntity(data))).get().value(); if (journalCount < 2) { new AlertDialog.Builder(this) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index 29c9206f..a859a9ca 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -27,6 +27,8 @@ import com.etesync.syncadapter.R; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.EntryEntity; import com.etesync.syncadapter.model.JournalEntity; +import com.etesync.syncadapter.model.JournalModel; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalAddressBook; import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.ui.importlocal.ImportActivity; @@ -63,7 +65,7 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh public void refresh() { EntityDataStore data = ((App) getApplicationContext()).getData(); - final JournalEntity journalEntity = JournalEntity.fetch(data, info.uid); + final JournalEntity journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); if ((journalEntity == null) || journalEntity.isDeleted()) { finish(); return; @@ -173,7 +175,7 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh protected Long doInBackground(Void... aVoids) { EntityDataStore data = ((App) getApplicationContext()).getData(); - final JournalEntity journalEntity = JournalEntity.fetch(data, info.uid); + final JournalEntity journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); entryCount = data.count(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).get().value(); long count; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java index c525245a..87feec28 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/journalviewer/ListEntriesFragment.java @@ -137,7 +137,7 @@ public class ListEntriesFragment extends ListFragment implements AdapterView.OnI @Override protected List doInBackground(Void... voids) { - journalEntity = JournalModel.Journal.fetch(data, info.uid); + journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid); return data.select(EntryEntity.class).where(EntryEntity.JOURNAL.eq(journalEntity)).orderBy(EntryEntity.ID.desc()).get().toList(); } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java index cf1cafa8..1b4d920f 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java @@ -14,7 +14,6 @@ import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ContentResolver; -import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; @@ -26,8 +25,6 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.Loader; -import java.util.logging.Level; - import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.App; import com.etesync.syncadapter.Constants; @@ -37,8 +34,12 @@ import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.ServiceDB; +import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalTaskList; import com.etesync.syncadapter.ui.setup.BaseConfigurationFinder.Configuration; + +import java.util.logging.Level; + import at.bitfire.ical4android.TaskProvider; import io.requery.Persistable; import io.requery.sql.EntityDataStore; @@ -140,7 +141,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan if (config.cardDAV != null) { // insert CardDAV service - insertService(db, accountName, ServiceDB.Services.SERVICE_CARDDAV, config.cardDAV); + insertService(db, accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV); // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); @@ -150,7 +151,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan if (config.calDAV != null) { // insert CalDAV service - insertService(db, accountName, ServiceDB.Services.SERVICE_CALDAV, config.calDAV); + insertService(db, accountName, CollectionInfo.Type.CALENDAR, config.calDAV); // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); @@ -172,22 +173,20 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan return true; } - protected long insertService(SQLiteDatabase db, String accountName, String service, BaseConfigurationFinder.Configuration.ServiceInfo info) { - ContentValues values = new ContentValues(); + protected void insertService(SQLiteDatabase db, String accountName, CollectionInfo.Type serviceType, BaseConfigurationFinder.Configuration.ServiceInfo info) { + EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); // insert service - values.put(ServiceDB.Services.ACCOUNT_NAME, accountName); - values.put(ServiceDB.Services.SERVICE, service); - long serviceID = db.insertWithOnConflict(ServiceDB.Services._TABLE, null, values, SQLiteDatabase.CONFLICT_REPLACE); - EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); + ServiceEntity serviceEntity = new ServiceEntity(); + serviceEntity.setAccount(accountName); + serviceEntity.setType(serviceType); + data.upsert(serviceEntity); // insert collections for (CollectionInfo collection : info.collections.values()) { - collection.serviceID = serviceID; - JournalEntity journalEntity = new JournalEntity(collection); + collection.serviceID = serviceEntity.getId(); + JournalEntity journalEntity = new JournalEntity(data, collection); data.insert(journalEntity); } - - return serviceID; } } From ae08510729155e7769fcadf2e67c874875652f60 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 20 Apr 2017 14:57:52 +0100 Subject: [PATCH 09/23] Requery: Fix database to have the correct constraints (on upgrade). Requery doesn't automatically update column constraints, and there was an issue with it applying indexes before adding the new columns which was also causing troubles. This commit, while ugly, just manually updates the database using raw SQL to what we expect it to be. --- .../java/com/etesync/syncadapter/App.java | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/App.java b/app/src/main/java/com/etesync/syncadapter/App.java index b0433329..478de93d 100644 --- a/app/src/main/java/com/etesync/syncadapter/App.java +++ b/app/src/main/java/com/etesync/syncadapter/App.java @@ -40,7 +40,6 @@ import com.etesync.syncadapter.log.LogcatHandler; import com.etesync.syncadapter.log.PlainTextFormatter; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; import com.etesync.syncadapter.model.Models; import com.etesync.syncadapter.model.ServiceDB; import com.etesync.syncadapter.model.ServiceEntity; @@ -70,9 +69,9 @@ import at.bitfire.ical4android.CalendarStorageException; import at.bitfire.vcard4android.ContactsStorageException; import io.requery.Persistable; import io.requery.android.sqlite.DatabaseSource; +import io.requery.meta.EntityModel; import io.requery.sql.Configuration; import io.requery.sql.EntityDataStore; -import io.requery.sql.TableCreationMode; import lombok.Cleanup; import lombok.Getter; import okhttp3.internal.tls.OkHostnameVerifier; @@ -229,13 +228,43 @@ public class App extends Application { public EntityDataStore getData() { if (dataStore == null) { // override onUpgrade to handle migrating to a new version - DatabaseSource source = new DatabaseSource(this, Models.DEFAULT, 3); + DatabaseSource source = new MyDatabaseSource(this, Models.DEFAULT, 3); Configuration configuration = source.getConfiguration(); dataStore = new EntityDataStore<>(configuration); } return dataStore; } + private static class MyDatabaseSource extends DatabaseSource { + MyDatabaseSource(Context context, EntityModel entityModel, int version) { + super(context, entityModel, version); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + super.onUpgrade(db, oldVersion, newVersion); + + if (oldVersion < 3) { + db.execSQL("PRAGMA foreign_keys=OFF;"); + + db.execSQL("CREATE TABLE new_Journal (id integer primary key autoincrement not null, deleted boolean not null, encryptedKey varbinary(255), info varchar(255), owner varchar(255), service integer, serviceModel integer, uid varchar(64) not null, foreign key (serviceModel) references Service (id) on delete cascade);"); + db.execSQL("CREATE TABLE new_Entry (id integer primary key autoincrement not null, content varchar(255), journal integer, uid varchar(64) not null, foreign key (journal) references Journal (id) on delete cascade);"); + + db.execSQL("INSERT INTO new_Journal SELECT id, deleted, encryptedKey, info, owner, service, serviceModel, uid from Journal;"); + db.execSQL("INSERT INTO new_Entry SELECT id, content, journal, uid from Entry;"); + + db.execSQL("DROP TABLE Journal;"); + db.execSQL("DROP TABLE Entry;"); + db.execSQL("ALTER TABLE new_Journal RENAME TO Journal;"); + db.execSQL("ALTER TABLE new_Entry RENAME TO Entry;"); + // Add back indexes + db.execSQL("CREATE UNIQUE INDEX journal_unique_together on Journal (serviceModel, uid);"); + db.execSQL("CREATE UNIQUE INDEX entry_unique_together on Entry (journal, uid);"); + db.execSQL("PRAGMA foreign_keys=ON;"); + } + } + } + // update from previous account settings private final static String PREF_VERSION = "version"; From 43803b6d3e8257f46b1b93d625f972597a6f4ef7 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 12 Apr 2017 16:04:16 +0100 Subject: [PATCH 10/23] AccountSettings: Add a keypair setting. This is used for storing the asymmetric key pair. --- .../etesync/syncadapter/AccountSettings.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/AccountSettings.java b/app/src/main/java/com/etesync/syncadapter/AccountSettings.java index 35baa4b1..814b96b8 100644 --- a/app/src/main/java/com/etesync/syncadapter/AccountSettings.java +++ b/app/src/main/java/com/etesync/syncadapter/AccountSettings.java @@ -21,6 +21,9 @@ import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.etesync.syncadapter.journalmanager.Crypto; +import com.etesync.syncadapter.utils.Base64; + import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; @@ -36,6 +39,8 @@ public class AccountSettings { KEY_URI = "uri", KEY_USERNAME = "user_name", KEY_TOKEN = "auth_token", + KEY_ASYMMETRIC_PRIVATE_KEY = "asymmetric_private_key", + KEY_ASYMMETRIC_PUBLIC_KEY = "asymmetric_public_key", KEY_WIFI_ONLY = "wifi_only", // sync on WiFi only (default: false) KEY_WIFI_ONLY_SSID = "wifi_only_ssid"; // restrict sync to specific WiFi SSID @@ -122,6 +127,23 @@ public class AccountSettings { accountManager.setUserData(account, KEY_TOKEN, token); } + + public Crypto.AsymmetricKeyPair getKeyPair() { + if (accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY) != null) { + byte[] pubkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY), Base64.NO_WRAP); + byte[] privkey = Base64.decode(accountManager.getUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY), Base64.NO_WRAP); + + return new Crypto.AsymmetricKeyPair(privkey, pubkey); + } + + return null; + } + + public void setKeyPair(@NonNull Crypto.AsymmetricKeyPair keyPair) { + accountManager.setUserData(account, KEY_ASYMMETRIC_PUBLIC_KEY, Base64.encodeToString(keyPair.getPublicKey(), Base64.NO_WRAP)); + accountManager.setUserData(account, KEY_ASYMMETRIC_PRIVATE_KEY, Base64.encodeToString(keyPair.getPrivateKey(), Base64.NO_WRAP)); + } + public String username() { return accountManager.getUserData(account, KEY_USERNAME); } From beccb339049a6f5148805a15298161c81a892534 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 13:29:05 +0100 Subject: [PATCH 11/23] Journal: use journal keys if available. If a journal has a key set to it (usually used for shared journals), use it instead of the symmetric key. The key of the journal is asymmetrically encrypted using our keypair. --- .../syncadapter/journalmanager/Crypto.java | 20 +++++++++++++++---- .../journalmanager/JournalManager.java | 6 ++---- .../syncadapter/SyncAdapterService.java | 12 +++++++++-- .../syncadapter/syncadapter/SyncManager.java | 10 ++++++++-- .../journalmanager/ServiceTest.java | 11 ++++++---- 5 files changed, 43 insertions(+), 16 deletions(-) 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 b6120c70..92246cca 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java @@ -123,8 +123,21 @@ public class Crypto { private SecureRandom _random = null; @Getter private final byte version; - private final byte[] cipherKey; - private final byte[] hmacKey; + private byte[] cipherKey; + private byte[] hmacKey; + + private void setDerivedKey(byte[] derivedKey) { + cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey); + hmacKey = hmac256("hmac".getBytes(Charsets.UTF_8), derivedKey); + } + + public CryptoManager(int version, AsymmetricKeyPair keyPair, byte[] encryptedKey) { + Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair); + byte[] derivedKey = cryptoManager.decrypt(encryptedKey); + + this.version = (byte) version; + setDerivedKey(derivedKey); + } public CryptoManager(int version, @NonNull String keyBase64, @NonNull String salt) throws Exceptions.IntegrityException, Exceptions.VersionTooNewException { byte[] derivedKey; @@ -139,8 +152,7 @@ public class Crypto { } this.version = (byte) version; - cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey); - hmacKey = hmac256("hmac".getBytes(Charsets.UTF_8), derivedKey); + setDerivedKey(derivedKey); } private static final int blockSize = 16; // AES's block size in bytes 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 f190b449..522f311a 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -40,7 +40,7 @@ public class JournalManager extends BaseManager { this.client = httpClient; } - public List getJournals(String keyBase64) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { + public List getJournals() throws Exceptions.HttpException { Request request = new Request.Builder() .get() .url(remote) @@ -51,9 +51,7 @@ public class JournalManager extends BaseManager { List ret = GsonHelper.gson.fromJson(body.charStream(), journalType); for (Journal journal : ret) { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), keyBase64, journal.getUid()); journal.processFromJson(); - journal.verify(crypto); } return ret; @@ -157,7 +155,7 @@ public class JournalManager extends BaseManager { setContent(Arrays.copyOfRange(getContent(), HMAC_SIZE, getContent().length)); } - void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { + public void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { if (hmac == null) { throw new Exceptions.IntegrityException("HMAC is null!"); } 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 25bd22bb..abd4f902 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -158,8 +158,16 @@ public abstract class SyncAdapterService extends Service { List> journals = new LinkedList<>(); - for (JournalManager.Journal journal : journalsManager.getJournals(settings.password())) { - Crypto.CryptoManager crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid()); + for (JournalManager.Journal journal : journalsManager.getJournals()) { + Crypto.CryptoManager crypto; + if (journal.getKey() != null) { + crypto = new Crypto.CryptoManager(journal.getVersion(), settings.getKeyPair(), journal.getKey()); + } else { + crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid()); + } + + journal.verify(crypto); + CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto)); info.updateFromJournal(journal); 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 4db3a398..f7481f0c 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java @@ -33,6 +33,7 @@ import com.etesync.syncadapter.model.SyncEntry; import com.etesync.syncadapter.resource.LocalCollection; import com.etesync.syncadapter.resource.LocalResource; import com.etesync.syncadapter.ui.DebugInfoActivity; +import com.etesync.syncadapter.utils.Base64; import org.apache.commons.collections4.ListUtils; @@ -118,7 +119,12 @@ abstract public class SyncManager { notificationManager.cancel(); App.log.info(String.format(Locale.getDefault(), "Syncing collection %s (version: %d)", journalUid, info.version)); - crypto = new Crypto.CryptoManager(info.version, settings.password(), journalUid); + + if (getJournalEntity().getEncryptedKey() != null) { + crypto = new Crypto.CryptoManager(info.version, settings.getKeyPair(), getJournalEntity().getEncryptedKey()); + } else { + crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); + } } protected abstract int notificationId(); @@ -235,7 +241,7 @@ abstract public class SyncManager { private JournalEntity getJournalEntity() { if (_journalEntity == null) - _journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), journal.getUid()); + _journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid); return _journalEntity; } diff --git a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java index b98bdf8b..a905856d 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java @@ -93,7 +93,7 @@ public class ServiceTest { } assertNotNull(caught); - List journals = journalManager.getJournals(Helpers.keyBase64); + List journals = journalManager.getJournals(); assertEquals(journals.size(), 1); CollectionInfo info2 = CollectionInfo.fromJson(journals.get(0).getContent(crypto)); assertEquals(info2.displayName, info.displayName); @@ -103,7 +103,7 @@ public class ServiceTest { journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); journalManager.updateJournal(journal); - journals = journalManager.getJournals(Helpers.keyBase64); + journals = journalManager.getJournals(); assertEquals(journals.size(), 1); info2 = CollectionInfo.fromJson(journals.get(0).getContent(crypto)); assertEquals(info2.displayName, info.displayName); @@ -111,7 +111,7 @@ public class ServiceTest { // Delete journal journalManager.deleteJournal(journal); - journals = journalManager.getJournals(Helpers.keyBase64); + journals = journalManager.getJournals(); assertEquals(journals.size(), 0); // Bad HMAC @@ -124,7 +124,10 @@ public class ServiceTest { try { caught = null; - journalManager.getJournals(Helpers.keyBase64); + for (JournalManager.Journal journal1 : journalManager.getJournals()) { + Crypto.CryptoManager crypto1 = new Crypto.CryptoManager(info.version, Helpers.keyBase64, journal1.getUid()); + journal1.verify(crypto1); + } } catch (Exceptions.IntegrityException e) { caught = e; } From a57936982d71723dbcfba1a3c1104eb14f8bc992 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 16:26:12 +0100 Subject: [PATCH 12/23] Add a fragment to setup user info. This is used to create a keypair and put it on the server if one doesn't exist, and fetch it and save it locally if one does. It's currently called from the account activity. --- .../syncadapter/ui/AccountActivity.java | 5 + .../ui/setup/SetupUserInfoFragment.java | 142 ++++++++++++++++++ app/src/main/res/values/strings.xml | 4 + 3 files changed, 151 insertions(+) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index 4ccfc867..a9eb0f4d 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -57,6 +57,7 @@ import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalCalendar; +import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment; import com.etesync.syncadapter.utils.HintManager; import com.etesync.syncadapter.utils.ShowcaseBuilder; @@ -113,6 +114,10 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu .playOn(tbCardDAV); HintManager.setHintSeen(this, HINT_VIEW_COLLECTION, true); } + + if (!SetupUserInfoFragment.hasUserInfo(this, account)) { + SetupUserInfoFragment.newInstance(account).show(getSupportFragmentManager(), null); + } } @Override diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java new file mode 100644 index 00000000..eda1db14 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java @@ -0,0 +1,142 @@ +package com.etesync.syncadapter.ui.setup; + +import android.accounts.Account; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v7.app.AlertDialog; +import android.widget.TextView; + +import com.etesync.syncadapter.AccountSettings; +import com.etesync.syncadapter.App; +import com.etesync.syncadapter.HttpClient; +import com.etesync.syncadapter.InvalidAccountException; +import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Crypto; +import com.etesync.syncadapter.journalmanager.UserInfoManager; + +import lombok.RequiredArgsConstructor; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +import static com.etesync.syncadapter.Constants.KEY_ACCOUNT; + +public class SetupUserInfoFragment extends DialogFragment { + private Account account; + private AccountSettings settings; + + public static SetupUserInfoFragment newInstance(Account account) { + SetupUserInfoFragment frag = new SetupUserInfoFragment(); + Bundle args = new Bundle(1); + args.putParcelable(KEY_ACCOUNT, account); + frag.setArguments(args); + return frag; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + ProgressDialog progress = new ProgressDialog(getActivity()); + progress.setTitle(R.string.login_encryption_setup_title); + progress.setMessage(getString(R.string.login_encryption_setup)); + progress.setIndeterminate(true); + progress.setCanceledOnTouchOutside(false); + setCancelable(false); + return progress; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + account = getArguments().getParcelable(KEY_ACCOUNT); + + try { + settings = new AccountSettings(getContext(), account); + } catch (Exception e) { + e.printStackTrace(); + } + + new SetupUserInfo().execute(account); + } + + public static boolean hasUserInfo(Context context, Account account) { + AccountSettings settings; + try { + settings = new AccountSettings(context, account); + } catch (InvalidAccountException e) { + e.printStackTrace(); + return false; + } + return settings.getKeyPair() != null; + } + + protected class SetupUserInfo extends AsyncTask { + ProgressDialog progressDialog; + + @Override + protected void onPreExecute() { + progressDialog = (ProgressDialog) getDialog(); + } + + @Override + protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) { + try { + OkHttpClient httpClient = HttpClient.create(getContext(), account); + + Crypto.CryptoManager cryptoManager = new Crypto.CryptoManager(com.etesync.syncadapter.journalmanager.Constants.CURRENT_VERSION, settings.password(), "userInfo"); + UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri())); + UserInfoManager.UserInfo userInfo = userInfoManager.get(cryptoManager, account.name); + + if (userInfo == null) { + App.log.info("Creating userInfo for " + account.name); + userInfo = UserInfoManager.UserInfo.generate(cryptoManager, account.name); + userInfoManager.create(userInfo); + } else { + App.log.info("Fetched userInfo for " + account.name); + } + + Crypto.AsymmetricKeyPair keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); + + return new SetupUserInfoResult(keyPair, null); + } catch (Exception e) { + e.printStackTrace(); + return new SetupUserInfoResult(null, e); + } + } + + @Override + protected void onPostExecute(SetupUserInfoResult result) { + if (result.exception == null) { + settings.setKeyPair(result.keyPair); + } else { + Dialog dialog = new AlertDialog.Builder(getActivity()) + .setTitle(R.string.login_encryption_error_title) + .setIcon(R.drawable.ic_error_dark) + .setMessage(result.exception.getLocalizedMessage()) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // dismiss + } + }) + .create(); + dialog.show(); + } + + dismissAllowingStateLoss(); + } + + @RequiredArgsConstructor + class SetupUserInfoResult { + final Crypto.AsymmetricKeyPair keyPair; + final Exception exception; + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc2f23b4..737018d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,6 +140,10 @@ Setting up encryption Please wait, setting up encryption… + + Encryption Error + + Import Import Failed From 4c6176a6f4e68c1f888f8d50bed2e495b678f858 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Apr 2017 16:01:04 +0100 Subject: [PATCH 13/23] Fetch userinfo on account creation. We need the keypair to access shared journals, so we need to make sure to fetch it at the moment we create the local account, which is what this commit does. --- .../com/etesync/syncadapter/HttpClient.java | 4 +++ .../ui/setup/BaseConfigurationFinder.java | 2 ++ .../ui/setup/SetupEncryptionFragment.java | 29 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/HttpClient.java b/app/src/main/java/com/etesync/syncadapter/HttpClient.java index a0e277e0..5fd080a8 100644 --- a/app/src/main/java/com/etesync/syncadapter/HttpClient.java +++ b/app/src/main/java/com/etesync/syncadapter/HttpClient.java @@ -74,6 +74,10 @@ public class HttpClient { return create(context, App.log); } + public static OkHttpClient create(@Nullable Context context, String authToken) { + return create(context, App.log, Constants.serviceUrl.getHost(), authToken); + } + private static OkHttpClient.Builder defaultBuilder(@Nullable Context context, @NonNull final Logger logger) { OkHttpClient.Builder builder = client.newBuilder(); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java index 33200efe..23110052 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/BaseConfigurationFinder.java @@ -19,6 +19,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import com.etesync.syncadapter.HttpClient; +import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.journalmanager.Exceptions; import com.etesync.syncadapter.journalmanager.JournalAuthenticator; import com.etesync.syncadapter.log.StringHandler; @@ -99,6 +100,7 @@ public class BaseConfigurationFinder { public final String userName, authtoken; public String rawPassword; public String password; + public Crypto.AsymmetricKeyPair keyPair; public final ServiceInfo cardDAV; public final ServiceInfo calDAV; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java index 1b4d920f..253f095e 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java @@ -28,9 +28,12 @@ import android.support.v4.content.Loader; import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.App; 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.Crypto; +import com.etesync.syncadapter.journalmanager.Exceptions; +import com.etesync.syncadapter.journalmanager.UserInfoManager; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.ServiceDB; @@ -44,6 +47,8 @@ import at.bitfire.ical4android.TaskProvider; import io.requery.Persistable; import io.requery.sql.EntityDataStore; import lombok.Cleanup; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; public class SetupEncryptionFragment extends DialogFragment implements LoaderManager.LoaderCallbacks { private static final String KEY_CONFIG = "config"; @@ -114,6 +119,27 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan @Override public Configuration loadInBackground() { config.password = Crypto.deriveKey(config.userName, config.rawPassword); + + try { + Crypto.CryptoManager cryptoManager; + OkHttpClient httpClient = HttpClient.create(getContext(), config.authtoken); + + UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(config.url)); + UserInfoManager.UserInfo userInfo = userInfoManager.get(config.userName); + if (userInfo != null) { + App.log.info("Fetched userInfo for " + config.userName); + cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), config.password, "userInfo"); + userInfo.verify(cryptoManager); + config.keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); + } + } catch (Exceptions.HttpException e) { + e.printStackTrace(); + } catch (Exceptions.IntegrityException e) { + e.printStackTrace(); + } catch (Exceptions.VersionTooNewException e) { + e.printStackTrace(); + } + return config; } } @@ -138,6 +164,9 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan AccountSettings settings = new AccountSettings(getContext(), account); settings.setAuthToken(config.authtoken); + if (config.keyPair != null) { + settings.setKeyPair(config.keyPair); + } if (config.cardDAV != null) { // insert CardDAV service From 4246ae7edeac93a6c38e1350967a2154958e3a46 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 17:28:22 +0100 Subject: [PATCH 14/23] Add a way to view own fingerprint. This adds a menu option from the account page to view your own keypair (to compare when sharing). --- .../syncadapter/ui/AccountActivity.java | 30 +++++++++++++++++++ .../main/res/drawable/ic_fingerprint_dark.xml | 5 ++++ app/src/main/res/menu/activity_account.xml | 5 ++++ app/src/main/res/values/strings.xml | 3 ++ 4 files changed, 43 insertions(+) create mode 100644 app/src/main/res/drawable/ic_fingerprint_dark.xml diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index a9eb0f4d..c89096e9 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -49,10 +49,12 @@ import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; +import com.etesync.syncadapter.AccountSettings; import com.etesync.syncadapter.AccountUpdateService; import com.etesync.syncadapter.App; import com.etesync.syncadapter.Constants; import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.JournalEntity; import com.etesync.syncadapter.model.ServiceEntity; @@ -61,6 +63,8 @@ import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment; import com.etesync.syncadapter.utils.HintManager; import com.etesync.syncadapter.utils.ShowcaseBuilder; +import org.spongycastle.util.encoders.Hex; + import java.io.IOException; import java.util.List; import java.util.logging.Level; @@ -167,6 +171,19 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu }) .show(); break; + case R.id.show_fingerprint: + AlertDialog dialog = new AlertDialog.Builder(AccountActivity.this) + .setIcon(R.drawable.ic_fingerprint_dark) + .setTitle(R.string.show_fingperprint_title) + .setMessage(getFormattedFingerprint()) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).create(); + dialog.show(); + break; default: return super.onOptionsItemSelected(item); } @@ -196,6 +213,19 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu } }; + private String getFormattedFingerprint() { + AccountSettings settings = null; + try { + settings = new AccountSettings(this, account); + byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(settings.getKeyPair().getPublicKey()); + String fingerprintString = Hex.toHexString(fingerprint).toLowerCase(); + return fingerprintString.replaceAll("(.{4})", "$1 "); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + /* LOADERS AND LOADED DATA */ protected static class AccountInfo { diff --git a/app/src/main/res/drawable/ic_fingerprint_dark.xml b/app/src/main/res/drawable/ic_fingerprint_dark.xml new file mode 100644 index 00000000..ec952639 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_dark.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/menu/activity_account.xml b/app/src/main/res/menu/activity_account.xml index d591ff6f..96193221 100644 --- a/app/src/main/res/menu/activity_account.xml +++ b/app/src/main/res/menu/activity_account.xml @@ -20,6 +20,11 @@ android:title="@string/account_settings" app:showAsAction="ifRoom"/> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 737018d7..e5a1f667 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,6 +90,7 @@ Synchronizing now Account settings Delete account + Show Fingerprint Really delete account? All local copies of address books, calendars and task lists will be deleted. Create new calendar @@ -97,6 +98,8 @@ Deleting the last collection is not allowed, please create a new one if you\'d like to delete this one. You can click on an item to view the collection. From there you can view the journal, import, and much more... + My Fingerprint + Change Journal In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\". From 656dad3615526b1768147bbde3dd535db91cc067 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 17:35:09 +0100 Subject: [PATCH 15/23] Add UI to add/remove/list journal members. Only owners of a journal are allowed to control and view its members. --- app/src/main/AndroidManifest.xml | 1 + .../syncadapter/journalmanager/Crypto.java | 15 +- .../journalmanager/JournalManager.java | 6 + .../syncadapter/ui/AccountActivity.java | 4 +- .../syncadapter/ui/AddMemberFragment.java | 174 ++++++++++++++++++ .../ui/CollectionMembersActivity.java | 153 +++++++++++++++ .../ui/CollectionMembersListFragment.java | 164 +++++++++++++++++ .../syncadapter/ui/RemoveMemberFragment.java | 116 ++++++++++++ .../ui/ViewCollectionActivity.java | 18 ++ .../main/res/drawable/ic_account_add_dark.xml | 10 + app/src/main/res/drawable/ic_members_dark.xml | 5 + app/src/main/res/layout/collection_header.xml | 36 ++++ .../res/layout/collection_members_list.xml | 21 +++ .../layout/collection_members_list_item.xml | 24 +++ .../res/layout/view_collection_activity.xml | 37 +--- .../res/layout/view_collection_members.xml | 61 ++++++ .../res/menu/activity_view_collection.xml | 5 + app/src/main/res/values/strings.xml | 15 ++ 18 files changed, 826 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java create mode 100644 app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java create mode 100644 app/src/main/res/drawable/ic_account_add_dark.xml create mode 100644 app/src/main/res/drawable/ic_members_dark.xml create mode 100644 app/src/main/res/layout/collection_header.xml create mode 100644 app/src/main/res/layout/collection_members_list.xml create mode 100644 app/src/main/res/layout/collection_members_list_item.xml create mode 100644 app/src/main/res/layout/view_collection_members.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b0600fb8..e7c9dc60 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -184,6 +184,7 @@ android:parentActivityName=".ui.AccountsActivity"> + 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 92246cca..0ae3f440 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java @@ -115,6 +115,12 @@ public class Crypto { public static byte[] getKeyFingerprint(byte[] pubkey) { return sha256(pubkey); } + + public static String getPrettyKeyFingerprint(byte[] pubkey) { + byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(pubkey); + String fingerprintString = Hex.toHexString(fingerprint).toLowerCase(); + return fingerprintString.replaceAll("(.{4})", "$1 "); + } } public static class CryptoManager { @@ -125,6 +131,7 @@ public class Crypto { private final byte version; private byte[] cipherKey; private byte[] hmacKey; + private byte[] derivedKey; private void setDerivedKey(byte[] derivedKey) { cipherKey = hmac256("aes".getBytes(Charsets.UTF_8), derivedKey); @@ -133,14 +140,13 @@ public class Crypto { public CryptoManager(int version, AsymmetricKeyPair keyPair, byte[] encryptedKey) { Crypto.AsymmetricCryptoManager cryptoManager = new Crypto.AsymmetricCryptoManager(keyPair); - byte[] derivedKey = cryptoManager.decrypt(encryptedKey); + derivedKey = cryptoManager.decrypt(encryptedKey); this.version = (byte) version; setDerivedKey(derivedKey); } public CryptoManager(int version, @NonNull String keyBase64, @NonNull String salt) throws Exceptions.IntegrityException, Exceptions.VersionTooNewException { - byte[] derivedKey; if (version > Byte.MAX_VALUE) { throw new Exceptions.IntegrityException("Version is out of range."); } else if (version > Constants.CURRENT_VERSION) { @@ -238,6 +244,11 @@ public class Crypto { hmac.doFinal(ret, 0); return ret; } + + public byte[] getEncryptedKey(AsymmetricKeyPair keyPair, byte[] publicKey) { + AsymmetricCryptoManager cryptoManager = new AsymmetricCryptoManager(keyPair); + return cryptoManager.encrypt(publicKey, derivedKey); + } } static String sha256(String base) { 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 522f311a..41f0c2fb 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java @@ -144,6 +144,12 @@ public class JournalManager extends BaseManager { super(); } + public static Journal fakeWithUid(String uid) { + Journal ret = new Journal(); + ret.setUid(uid); + return ret; + } + public Journal(Crypto.CryptoManager crypto, String content, String uid) { super(crypto, content, uid); hmac = calculateHmac(crypto); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index c89096e9..9670b1bb 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -217,9 +217,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu AccountSettings settings = null; try { settings = new AccountSettings(this, account); - byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(settings.getKeyPair().getPublicKey()); - String fingerprintString = Hex.toHexString(fingerprint).toLowerCase(); - return fingerprintString.replaceAll("(.{4})", "$1 "); + return Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(settings.getKeyPair().getPublicKey()); } catch (Exception e) { e.printStackTrace(); return null; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java new file mode 100644 index 00000000..96547d02 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java @@ -0,0 +1,174 @@ +package com.etesync.syncadapter.ui; + +import android.accounts.Account; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import com.etesync.syncadapter.AccountSettings; +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.Crypto; +import com.etesync.syncadapter.journalmanager.JournalManager; +import com.etesync.syncadapter.journalmanager.UserInfoManager; +import com.etesync.syncadapter.model.CollectionInfo; + +import lombok.RequiredArgsConstructor; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +public class AddMemberFragment extends DialogFragment { + final static private String KEY_MEMBER = "memberEmail"; + private Account account; + private AccountSettings settings; + private OkHttpClient httpClient; + private HttpUrl remote; + private CollectionInfo info; + private String memberEmail; + private byte[] memberPubKey; + + public static AddMemberFragment newInstance(Account account, CollectionInfo info, String email) { + AddMemberFragment frag = new AddMemberFragment(); + Bundle args = new Bundle(1); + args.putParcelable(Constants.KEY_ACCOUNT, account); + args.putSerializable(Constants.KEY_COLLECTION_INFO, info); + args.putString(KEY_MEMBER, email); + frag.setArguments(args); + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + account = getArguments().getParcelable(Constants.KEY_ACCOUNT); + info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); + memberEmail = getArguments().getString(KEY_MEMBER); + try { + settings = new AccountSettings(getContext(), account); + httpClient = HttpClient.create(getContext(), account); + } catch (InvalidAccountException e) { + e.printStackTrace(); + } + remote = HttpUrl.get(settings.getUri()); + + new MemberAdd().execute(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setTitle(R.string.collection_members_adding); + progress.setMessage(getString(R.string.please_wait)); + progress.setIndeterminate(true); + progress.setCanceledOnTouchOutside(false); + setCancelable(false); + return progress; + } + + private class MemberAdd extends AsyncTask { + @Override + protected AddResult doInBackground(Void... voids) { + try { + UserInfoManager userInfoManager = new UserInfoManager(httpClient, remote); + + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); + + memberPubKey = userInfoManager.get(crypto, memberEmail).getPubkey(); + return new AddResult(null); + } catch (Exception e) { + return new AddResult(e); + } + } + + @Override + protected void onPostExecute(AddResult result) { + if (result.throwable == null) { + String fingerprint = Crypto.AsymmetricCryptoManager.getPrettyKeyFingerprint(memberPubKey); + + new AlertDialog.Builder(getActivity()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.trust_fingerprint_title) + .setMessage(fingerprint) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + new MemberAddSecond().execute(); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }).show(); + } else { + new AlertDialog.Builder(getActivity()) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_add_error) + .setMessage(result.throwable.getMessage()) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).show(); + dismiss(); + } + } + + @RequiredArgsConstructor + class AddResult { + final Throwable throwable; + } + } + + private class MemberAddSecond extends AsyncTask { + @Override + protected AddResultSecond doInBackground(Void... voids) { + try { + JournalManager journalsManager = new JournalManager(httpClient, remote); + + JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); + + byte[] encryptedKey = crypto.getEncryptedKey(settings.getKeyPair(), memberPubKey); + JournalManager.Member member = new JournalManager.Member(memberEmail, encryptedKey); + journalsManager.addMember(journal, member); + return new AddResultSecond(null); + } catch (Exception e) { + return new AddResultSecond(e); + } + } + + @Override + protected void onPostExecute(AddResultSecond result) { + if (result.throwable == null) { + ((Refreshable) getActivity()).refresh(); + } else { + new AlertDialog.Builder(getActivity()) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_add_error) + .setMessage(result.throwable.getMessage()) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).show(); + } + dismiss(); + } + + @RequiredArgsConstructor + class AddResultSecond { + final Throwable throwable; + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java new file mode 100644 index 00000000..eccc77fe --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersActivity.java @@ -0,0 +1,153 @@ +package com.etesync.syncadapter.ui; + +import android.accounts.Account; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.support.v7.app.AppCompatActivity; +import android.text.InputType; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.etesync.syncadapter.App; +import com.etesync.syncadapter.R; +import com.etesync.syncadapter.model.CollectionInfo; +import com.etesync.syncadapter.model.JournalEntity; +import com.etesync.syncadapter.resource.LocalCalendar; + +import io.requery.Persistable; +import io.requery.sql.EntityDataStore; + +public class CollectionMembersActivity extends AppCompatActivity implements Refreshable { + public final static String EXTRA_ACCOUNT = "account", + EXTRA_COLLECTION_INFO = "collectionInfo"; + + private Account account; + private JournalEntity journalEntity; + private CollectionMembersListFragment listFragment; + protected CollectionInfo info; + + public static Intent newIntent(Context context, Account account, CollectionInfo info) { + Intent intent = new Intent(context, CollectionMembersActivity.class); + intent.putExtra(CollectionMembersActivity.EXTRA_ACCOUNT, account); + intent.putExtra(CollectionMembersActivity.EXTRA_COLLECTION_INFO, info); + return intent; + } + + @Override + public void refresh() { + EntityDataStore data = ((App) getApplicationContext()).getData(); + + journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); + if ((journalEntity == null) || journalEntity.isDeleted()) { + finish(); + return; + } + + info = journalEntity.getInfo(); + + setTitle(R.string.collection_members_title); + + final View colorSquare = findViewById(R.id.color); + if (info.type == CollectionInfo.Type.CALENDAR) { + if (info.color != null) { + colorSquare.setBackgroundColor(info.color); + } else { + colorSquare.setBackgroundColor(LocalCalendar.defaultColor); + } + } else { + colorSquare.setVisibility(View.GONE); + } + findViewById(R.id.progressBar).setVisibility(View.GONE); + + final TextView title = (TextView) findViewById(R.id.display_name); + title.setText(info.displayName); + + final TextView desc = (TextView) findViewById(R.id.description); + desc.setText(info.description); + + if (listFragment != null) { + listFragment.refresh(); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.view_collection_members); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + account = getIntent().getExtras().getParcelable(EXTRA_ACCOUNT); + info = (CollectionInfo) getIntent().getExtras().getSerializable(EXTRA_COLLECTION_INFO); + + refresh(); + + // We refresh before this, so we don't refresh the list before it was fully created. + if (savedInstanceState == null) { + listFragment = CollectionMembersListFragment.newInstance(account, info); + getSupportFragmentManager().beginTransaction() + .add(R.id.list_entries_container, listFragment) + .commit(); + } + } + + public void onAddMemberClicked(View v) { + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + AlertDialog.Builder dialog = new AlertDialog.Builder(this) + .setTitle(R.string.collection_members_add) + .setIcon(R.drawable.ic_account_add_dark) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DialogFragment frag = AddMemberFragment.newInstance(account, info, input.getText().toString()); + frag.show(getSupportFragmentManager(), null); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + dialog.setView(input); + dialog.show(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + if (!getSupportFragmentManager().popBackStackImmediate()) { + finish(); + } + return true; + } + return false; + } + + @Override + protected void onResume() { + super.onResume(); + + App app = (App) getApplicationContext(); + if (app.getCertManager() != null) + app.getCertManager().appInForeground = true; + + refresh(); + } + + @Override + protected void onPause() { + super.onPause(); + + App app = (App) getApplicationContext(); + if (app.getCertManager() != null) + app.getCertManager().appInForeground = false; + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java new file mode 100644 index 00000000..73559d7b --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/CollectionMembersListFragment.java @@ -0,0 +1,164 @@ +package com.etesync.syncadapter.ui; + +import android.accounts.Account; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.ListFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import com.etesync.syncadapter.AccountSettings; +import com.etesync.syncadapter.App; +import com.etesync.syncadapter.Constants; +import com.etesync.syncadapter.HttpClient; +import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.JournalManager; +import com.etesync.syncadapter.model.CollectionInfo; +import com.etesync.syncadapter.model.EntryEntity; +import com.etesync.syncadapter.model.JournalEntity; +import com.etesync.syncadapter.model.JournalModel; + +import java.util.List; + +import io.requery.Persistable; +import io.requery.sql.EntityDataStore; +import lombok.RequiredArgsConstructor; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +public class CollectionMembersListFragment extends ListFragment implements AdapterView.OnItemClickListener, Refreshable { + private EntityDataStore data; + private Account account; + private CollectionInfo info; + private JournalEntity journalEntity; + + private TextView emptyTextView; + + public static CollectionMembersListFragment newInstance(Account account, CollectionInfo info) { + CollectionMembersListFragment frag = new CollectionMembersListFragment(); + Bundle args = new Bundle(1); + args.putParcelable(Constants.KEY_ACCOUNT, account); + args.putSerializable(Constants.KEY_COLLECTION_INFO, info); + frag.setArguments(args); + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + data = ((App) getContext().getApplicationContext()).getData(); + account = getArguments().getParcelable(Constants.KEY_ACCOUNT); + info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); + journalEntity = JournalModel.Journal.fetch(data, info.getServiceEntity(data), info.uid); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.collection_members_list, container, false); + + //This is instead of setEmptyText() function because of Google bug + //See: https://code.google.com/p/android/issues/detail?id=21742 + emptyTextView = (TextView) view.findViewById(android.R.id.empty); + return view; + } + + @Override + public void refresh() { + new JournalMembersFetch().execute(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + refresh(); + + getListView().setOnItemClickListener(this); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final JournalManager.Member member = (JournalManager.Member) getListAdapter().getItem(position); + + new AlertDialog.Builder(getActivity()) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.collection_members_remove_title) + .setMessage(getString(R.string.collection_members_remove, member.getUser())) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DialogFragment frag = RemoveMemberFragment.newInstance(account, info, member.getUser()); + frag.show(getFragmentManager(), null); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }).show(); + } + + class MembersListAdapter extends ArrayAdapter { + MembersListAdapter(Context context) { + super(context, R.layout.collection_members_list_item); + } + + @Override + @NonNull + public View getView(int position, View v, @NonNull ViewGroup parent) { + if (v == null) + v = LayoutInflater.from(getContext()).inflate(R.layout.collection_members_list_item, parent, false); + + JournalManager.Member member = getItem(position); + + TextView tv = (TextView) v.findViewById(R.id.title); + tv.setText(member.getUser()); + return v; + } + } + + private class JournalMembersFetch extends AsyncTask { + @Override + protected MembersResult doInBackground(Void... voids) { + try { + OkHttpClient httpClient = HttpClient.create(getContext(), account); + AccountSettings settings = new AccountSettings(getContext(), account); + JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); + + JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(journalEntity.getUid()); + return new MembersResult(journalsManager.listMembers(journal), null); + } catch (Exception e) { + return new MembersResult(null, e); + } + } + + @Override + protected void onPostExecute(MembersResult result) { + if (result.throwable == null) { + MembersListAdapter listAdapter = new MembersListAdapter(getContext()); + setListAdapter(listAdapter); + + listAdapter.addAll(result.members); + + emptyTextView.setText(R.string.collection_members_list_empty); + } else { + emptyTextView.setText(result.throwable.getLocalizedMessage()); + } + } + + @RequiredArgsConstructor + class MembersResult { + final List members; + final Throwable throwable; + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java new file mode 100644 index 00000000..a8191238 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/ui/RemoveMemberFragment.java @@ -0,0 +1,116 @@ +package com.etesync.syncadapter.ui; + +import android.accounts.Account; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import com.etesync.syncadapter.AccountSettings; +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.Crypto; +import com.etesync.syncadapter.journalmanager.JournalManager; +import com.etesync.syncadapter.journalmanager.UserInfoManager; +import com.etesync.syncadapter.model.CollectionInfo; + +import org.apache.commons.codec.Charsets; + +import lombok.RequiredArgsConstructor; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +public class RemoveMemberFragment extends DialogFragment { + final static private String KEY_MEMBER = "memberEmail"; + private AccountSettings settings; + private OkHttpClient httpClient; + private HttpUrl remote; + private CollectionInfo info; + private String memberEmail; + + public static RemoveMemberFragment newInstance(Account account, CollectionInfo info, String email) { + RemoveMemberFragment frag = new RemoveMemberFragment(); + Bundle args = new Bundle(1); + args.putParcelable(Constants.KEY_ACCOUNT, account); + args.putSerializable(Constants.KEY_COLLECTION_INFO, info); + args.putString(KEY_MEMBER, email); + frag.setArguments(args); + return frag; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Account account = getArguments().getParcelable(Constants.KEY_ACCOUNT); + info = (CollectionInfo) getArguments().getSerializable(Constants.KEY_COLLECTION_INFO); + memberEmail = getArguments().getString(KEY_MEMBER); + try { + settings = new AccountSettings(getContext(), account); + httpClient = HttpClient.create(getContext(), account); + } catch (InvalidAccountException e) { + e.printStackTrace(); + } + remote = HttpUrl.get(settings.getUri()); + + new MemberRemove().execute(); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setTitle(R.string.collection_members_removing); + progress.setMessage(getString(R.string.please_wait)); + progress.setIndeterminate(true); + progress.setCanceledOnTouchOutside(false); + setCancelable(false); + return progress; + } + + private class MemberRemove extends AsyncTask { + @Override + protected RemoveResult doInBackground(Void... voids) { + try { + JournalManager journalsManager = new JournalManager(httpClient, remote); + JournalManager.Journal journal = JournalManager.Journal.fakeWithUid(info.uid); + + JournalManager.Member member = new JournalManager.Member(memberEmail, "placeholder".getBytes(Charsets.UTF_8)); + journalsManager.deleteMember(journal, member); + + return new RemoveResult(null); + } catch (Exception e) { + return new RemoveResult(e); + } + } + + @Override + protected void onPostExecute(RemoveResult result) { + if (result.throwable == null) { + ((Refreshable) getActivity()).refresh(); + } else { + new AlertDialog.Builder(getActivity()) + .setIcon(R.drawable.ic_error_dark) + .setTitle(R.string.collection_members_remove_error) + .setMessage(result.throwable.getMessage()) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).show(); + } + dismiss(); + } + + @RequiredArgsConstructor + class RemoveResult { + final Throwable throwable; + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index a859a9ca..56ca2027 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -168,6 +168,24 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh startActivity(ImportActivity.newIntent(ViewCollectionActivity.this, account, info)); } + public void onManageMembers(MenuItem item) { + if (isOwner) { + startActivity(CollectionMembersActivity.newIntent(this, account, info)); + } else { + AlertDialog dialog = new AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(getString(R.string.members_owner_only, journalEntity.getOwner())) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).create(); + dialog.show(); + } + } + private class LoadCountTask extends AsyncTask { private int entryCount; diff --git a/app/src/main/res/drawable/ic_account_add_dark.xml b/app/src/main/res/drawable/ic_account_add_dark.xml new file mode 100644 index 00000000..d12a0502 --- /dev/null +++ b/app/src/main/res/drawable/ic_account_add_dark.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_members_dark.xml b/app/src/main/res/drawable/ic_members_dark.xml new file mode 100644 index 00000000..d5e14c6f --- /dev/null +++ b/app/src/main/res/drawable/ic_members_dark.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/collection_header.xml b/app/src/main/res/layout/collection_header.xml new file mode 100644 index 00000000..178f7fc4 --- /dev/null +++ b/app/src/main/res/layout/collection_header.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/collection_members_list.xml b/app/src/main/res/layout/collection_members_list.xml new file mode 100644 index 00000000..7d1950d7 --- /dev/null +++ b/app/src/main/res/layout/collection_members_list.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout/collection_members_list_item.xml b/app/src/main/res/layout/collection_members_list_item.xml new file mode 100644 index 00000000..ab8fd805 --- /dev/null +++ b/app/src/main/res/layout/collection_members_list_item.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_collection_activity.xml b/app/src/main/res/layout/view_collection_activity.xml index 329badc0..652666f8 100644 --- a/app/src/main/res/layout/view_collection_activity.xml +++ b/app/src/main/res/layout/view_collection_activity.xml @@ -5,42 +5,11 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - - - - - - - - - - + android:layout_margin="@dimen/activity_margin" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/activity_view_collection.xml b/app/src/main/res/menu/activity_view_collection.xml index 2a83d62a..1eb4dd3d 100644 --- a/app/src/main/res/menu/activity_view_collection.xml +++ b/app/src/main/res/menu/activity_view_collection.xml @@ -15,6 +15,11 @@ android:onClick="onEditCollection" app:showAsAction="always" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5a1f667..929cfd21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,6 +103,20 @@ Change Journal In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\". + Only the owner of this collection (%s) is allowed to view its members. + + + Members + Loading members... + No members + Add member + Error adding member + Adding member + Trust fingerprint? + Removing member + Error removing member + Remove member + Would you like to revoke %s\'s access?\nPlease be advised that a malicious user would potentially be able to retain access to encryption keys. Please refer to the FAQ for more information. EteSync permissions @@ -215,6 +229,7 @@ Description (optional): Edit Import + Manage Members Save Delete Are you sure? From 9fb9db93278f818fdfb35129533282cea3025cba Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 13 Apr 2017 21:29:11 +0100 Subject: [PATCH 16/23] ViewCollection: only allow owner to edit collections. --- .../ui/ViewCollectionActivity.java | 23 +++++++++++++++++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index 56ca2027..34f6fb77 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -10,11 +10,13 @@ package com.etesync.syncadapter.ui; import android.accounts.Account; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.provider.CalendarContract; import android.provider.ContactsContract; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.view.Gravity; import android.view.Menu; @@ -52,7 +54,9 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh EXTRA_COLLECTION_INFO = "collectionInfo"; private Account account; + private JournalEntity journalEntity; protected CollectionInfo info; + private boolean isOwner; public static Intent newIntent(Context context, Account account, CollectionInfo info) { Intent intent = new Intent(context, ViewCollectionActivity.class); @@ -65,13 +69,14 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh public void refresh() { EntityDataStore data = ((App) getApplicationContext()).getData(); - final JournalEntity journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); + journalEntity = JournalEntity.fetch(data, info.getServiceEntity(data), info.uid); if ((journalEntity == null) || journalEntity.isDeleted()) { finish(); return; } info = journalEntity.getInfo(); + isOwner = account.name.equals(journalEntity.getOwner()); final View colorSquare = findViewById(R.id.color); if (info.type == CollectionInfo.Type.CALENDAR) { @@ -161,7 +166,21 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh } public void onEditCollection(MenuItem item) { - startActivity(EditCollectionActivity.newIntent(this, account, info)); + if (isOwner) { + startActivity(EditCollectionActivity.newIntent(this, account, info)); + } else { + AlertDialog dialog = new AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(getString(R.string.edit_owner_only, journalEntity.getOwner())) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).create(); + dialog.show(); + } } public void onImport(MenuItem item) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 929cfd21..79ae6a1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,6 +104,8 @@ Change Journal In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\". Only the owner of this collection (%s) is allowed to view its members. + Not Allowed + Only the owner of this collection (%s) is allowed to edit it. Members From e15a26af9c0a2b699ec171f199314a3b7e44b075 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Apr 2017 15:48:36 +0100 Subject: [PATCH 17/23] User info: fix version detection, and don't verify on fetch. We were not detecting the version correctly, but always just assumed latest version, which is obviously wrong. In addition, before this commit we used to automatically verify on fetch, which wasn't flexible enough for some use cases. This fixes that too. --- .../syncadapter/journalmanager/UserInfoManager.java | 5 ++--- .../com/etesync/syncadapter/ui/AddMemberFragment.java | 4 +--- .../syncadapter/ui/setup/SetupUserInfoFragment.java | 8 ++++++-- .../etesync/syncadapter/journalmanager/ServiceTest.java | 8 ++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java index 49487290..f1495165 100644 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java @@ -30,7 +30,7 @@ public class UserInfoManager extends BaseManager { this.client = httpClient; } - public UserInfo get(Crypto.CryptoManager cryptoManager, String owner) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { + public UserInfo get(String owner) throws Exceptions.HttpException { HttpUrl remote = this.remote.newBuilder().addPathSegment(owner).addPathSegment("").build(); Request request = new Request.Builder() .get() @@ -50,7 +50,6 @@ public class UserInfoManager extends BaseManager { ResponseBody body = response.body(); UserInfo ret = GsonHelper.gson.fromJson(body.charStream(), UserInfo.class); - ret.verify(cryptoManager); ret.setOwner(owner); return ret; @@ -109,7 +108,7 @@ public class UserInfoManager extends BaseManager { this.content = Arrays.concatenate(calculateHmac(crypto, content), content); } - void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { + public void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { if (this.content == null) { // Nothing to verify. return; diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java index 96547d02..9e07848d 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AddMemberFragment.java @@ -79,9 +79,7 @@ public class AddMemberFragment extends DialogFragment { try { UserInfoManager userInfoManager = new UserInfoManager(httpClient, remote); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); - - memberPubKey = userInfoManager.get(crypto, memberEmail).getPubkey(); + memberPubKey = userInfoManager.get(memberEmail).getPubkey(); return new AddResult(null); } catch (Exception e) { return new AddResult(e); diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java index eda1db14..997f810c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupUserInfoFragment.java @@ -18,6 +18,7 @@ import com.etesync.syncadapter.App; import com.etesync.syncadapter.HttpClient; import com.etesync.syncadapter.InvalidAccountException; import com.etesync.syncadapter.R; +import com.etesync.syncadapter.journalmanager.Constants; import com.etesync.syncadapter.journalmanager.Crypto; import com.etesync.syncadapter.journalmanager.UserInfoManager; @@ -88,18 +89,21 @@ public class SetupUserInfoFragment extends DialogFragment { @Override protected SetupUserInfo.SetupUserInfoResult doInBackground(Account... accounts) { try { + Crypto.CryptoManager cryptoManager; OkHttpClient httpClient = HttpClient.create(getContext(), account); - Crypto.CryptoManager cryptoManager = new Crypto.CryptoManager(com.etesync.syncadapter.journalmanager.Constants.CURRENT_VERSION, settings.password(), "userInfo"); UserInfoManager userInfoManager = new UserInfoManager(httpClient, HttpUrl.get(settings.getUri())); - UserInfoManager.UserInfo userInfo = userInfoManager.get(cryptoManager, account.name); + UserInfoManager.UserInfo userInfo = userInfoManager.get(account.name); if (userInfo == null) { App.log.info("Creating userInfo for " + account.name); + cryptoManager = new Crypto.CryptoManager(Constants.CURRENT_VERSION, settings.password(), "userInfo"); userInfo = UserInfoManager.UserInfo.generate(cryptoManager, account.name); userInfoManager.create(userInfo); } else { App.log.info("Fetched userInfo for " + account.name); + cryptoManager = new Crypto.CryptoManager(userInfo.getVersion(), settings.password(), "userInfo"); + userInfo.verify(cryptoManager); } Crypto.AsymmetricKeyPair keyPair = new Crypto.AsymmetricKeyPair(userInfo.getContent(cryptoManager), userInfo.getPubkey()); diff --git a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java index a905856d..9d9dc2ae 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/ServiceTest.java @@ -210,7 +210,7 @@ public class ServiceTest { UserInfoManager manager = new UserInfoManager(httpClient, remote); // Get when there's nothing - userInfo = manager.get(cryptoManager, Helpers.USER); + userInfo = manager.get(Helpers.USER); assertNull(userInfo); // Create @@ -218,20 +218,20 @@ public class ServiceTest { manager.create(userInfo); // Get - userInfo2 = manager.get(cryptoManager, Helpers.USER); + userInfo2 = manager.get(Helpers.USER); assertNotNull(userInfo2); assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager)); // Update userInfo.setContent(cryptoManager, "test".getBytes(Charsets.UTF_8)); manager.update(userInfo); - userInfo2 = manager.get(cryptoManager, Helpers.USER); + userInfo2 = manager.get(Helpers.USER); assertNotNull(userInfo2); assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2.getContent(cryptoManager)); // Delete manager.delete(userInfo); - userInfo = manager.get(cryptoManager, Helpers.USER); + userInfo = manager.get(Helpers.USER); assertNull(userInfo); } From 93fb1e3c54284bec5f3c9b77423f2f00e29ade7b Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Apr 2017 16:07:52 +0100 Subject: [PATCH 18/23] Remove redundant dbhelper calls. These calls were made obsolete after the switch to requery. We no longer need to get the database, because we no longer use it. --- .../ContactsSyncAdapterService.java | 3 - .../syncadapter/SyncAdapterService.java | 64 ++++++++----------- .../ui/setup/SetupEncryptionFragment.java | 8 +-- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java index 74d8ead0..9c63c8a2 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/ContactsSyncAdapterService.java @@ -58,7 +58,6 @@ public class ContactsSyncAdapterService extends SyncAdapterService { NotificationHelper notificationManager = new NotificationHelper(getContext(), "journals-contacts", Constants.NOTIFICATION_CONTACTS_SYNC); notificationManager.cancel(); - ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); try { AccountSettings settings = new AccountSettings(getContext(), account); if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(settings)) @@ -97,8 +96,6 @@ public class ContactsSyncAdapterService extends SyncAdapterService { detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase); } notificationManager.notify(title, getContext().getString(syncPhase)); - } finally { - dbHelper.close(); } App.log.info("Address book sync complete"); 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 abd4f902..88d79db3 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -133,7 +133,6 @@ public abstract class SyncAdapterService extends Service { } protected class RefreshCollections { - final private ServiceDB.OpenHelper dbHelper; final private Account account; final private Context context; final private CollectionInfo.Type serviceType; @@ -142,53 +141,46 @@ public abstract class SyncAdapterService extends Service { this.account = account; this.serviceType = serviceType; context = getContext(); - dbHelper = new ServiceDB.OpenHelper(context); } void run() throws Exceptions.HttpException, Exceptions.IntegrityException, InvalidAccountException, Exceptions.GenericCryptoException { - try { - @Cleanup SQLiteDatabase db = dbHelper.getWritableDatabase(); + App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString()); - App.log.info("Refreshing " + serviceType + " collections of service #" + serviceType.toString()); + OkHttpClient httpClient = HttpClient.create(context, account); - OkHttpClient httpClient = HttpClient.create(context, account); + AccountSettings settings = new AccountSettings(context, account); + JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); - AccountSettings settings = new AccountSettings(context, account); - JournalManager journalsManager = new JournalManager(httpClient, HttpUrl.get(settings.getUri())); + List> journals = new LinkedList<>(); - List> journals = new LinkedList<>(); - - for (JournalManager.Journal journal : journalsManager.getJournals()) { - Crypto.CryptoManager crypto; - if (journal.getKey() != null) { - crypto = new Crypto.CryptoManager(journal.getVersion(), settings.getKeyPair(), journal.getKey()); - } else { - crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid()); - } - - journal.verify(crypto); - - CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto)); - info.updateFromJournal(journal); - - if (info.type.equals(serviceType)) { - journals.add(new Pair<>(journal, info)); - } + for (JournalManager.Journal journal : journalsManager.getJournals()) { + Crypto.CryptoManager crypto; + if (journal.getKey() != null) { + crypto = new Crypto.CryptoManager(journal.getVersion(), settings.getKeyPair(), journal.getKey()); + } else { + crypto = new Crypto.CryptoManager(journal.getVersion(), settings.password(), journal.getUid()); } - if (journals.isEmpty()) { - CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType); - info.uid = JournalManager.Journal.genUid(); - Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); - JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); - journalsManager.putJournal(journal); + journal.verify(crypto); + + CollectionInfo info = CollectionInfo.fromJson(journal.getContent(crypto)); + info.updateFromJournal(journal); + + if (info.type.equals(serviceType)) { journals.add(new Pair<>(journal, info)); } - - saveCollections(journals); - } finally { - dbHelper.close(); } + + if (journals.isEmpty()) { + CollectionInfo info = CollectionInfo.defaultForServiceType(serviceType); + info.uid = JournalManager.Journal.genUid(); + Crypto.CryptoManager crypto = new Crypto.CryptoManager(info.version, settings.password(), info.uid); + JournalManager.Journal journal = new JournalManager.Journal(crypto, info.toJson(), info.uid); + journalsManager.putJournal(journal); + journals.add(new Pair<>(journal, info)); + } + + saveCollections(journals); } private void saveCollections(Iterable> journals) { diff --git a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java index 253f095e..163b0682 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/setup/SetupEncryptionFragment.java @@ -158,8 +158,6 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan // add entries for account to service DB App.log.log(Level.INFO, "Writing account configuration to database", config); - @Cleanup ServiceDB.OpenHelper dbHelper = new ServiceDB.OpenHelper(getContext()); - SQLiteDatabase db = dbHelper.getWritableDatabase(); try { AccountSettings settings = new AccountSettings(getContext(), account); @@ -170,7 +168,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan if (config.cardDAV != null) { // insert CardDAV service - insertService(db, accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV); + insertService(accountName, CollectionInfo.Type.ADDRESS_BOOK, config.cardDAV); // contact sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(ContactsContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); @@ -180,7 +178,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan if (config.calDAV != null) { // insert CalDAV service - insertService(db, accountName, CollectionInfo.Type.CALENDAR, config.calDAV); + insertService(accountName, CollectionInfo.Type.CALENDAR, config.calDAV); // calendar sync is automatically enabled by isAlwaysSyncable="true" in res/xml/sync_contacts.xml settings.setSyncInterval(CalendarContract.AUTHORITY, Constants.DEFAULT_SYNC_INTERVAL); @@ -202,7 +200,7 @@ public class SetupEncryptionFragment extends DialogFragment implements LoaderMan return true; } - protected void insertService(SQLiteDatabase db, String accountName, CollectionInfo.Type serviceType, BaseConfigurationFinder.Configuration.ServiceInfo info) { + protected void insertService(String accountName, CollectionInfo.Type serviceType, BaseConfigurationFinder.Configuration.ServiceInfo info) { EntityDataStore data = ((App) getContext().getApplicationContext()).getData(); // insert service From d3057f86f03b645afa827d83650233e697a07910 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Apr 2017 16:24:24 +0100 Subject: [PATCH 19/23] Disallow sharing of address books. At the moment we only support one address book per user, and sharing address books will interfere with this model. Hopefully, we'll add multiple address book support in the next release, and then we'll re-enable this. --- .../syncadapter/ui/ViewCollectionActivity.java | 14 +++++++++++++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index 34f6fb77..a7ecf66c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -188,7 +188,19 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh } public void onManageMembers(MenuItem item) { - if (isOwner) { + if (info.type.equals(CollectionInfo.Type.ADDRESS_BOOK)) { + AlertDialog dialog = new AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.members_address_book_not_allowed) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }).create(); + dialog.show(); + } else if (isOwner) { startActivity(CollectionMembersActivity.newIntent(this, account, info)); } else { AlertDialog dialog = new AlertDialog.Builder(this) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79ae6a1c..ccde4449 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,6 +106,7 @@ Only the owner of this collection (%s) is allowed to view its members. Not Allowed Only the owner of this collection (%s) is allowed to edit it. + Sharing of address books is currently not supported. Members From 4c473841980697245659b519db9ce1d43c40fbb5 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Wed, 19 Apr 2017 13:57:12 +0100 Subject: [PATCH 20/23] Journals: support adding back deleted journals. This currently just adds the journal back, but doesn't re-apply the journal, so the calendar for example would be empty, but the journal itself would be listed and visible. --- .../com/etesync/syncadapter/syncadapter/SyncAdapterService.java | 1 + 1 file changed, 1 insertion(+) 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 88d79db3..3760ac42 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncAdapterService.java @@ -201,6 +201,7 @@ public abstract class SyncAdapterService extends Service { JournalEntity journalEntity = JournalEntity.fetchOrCreate(data, collection); journalEntity.setOwner(journal.getOwner()); journalEntity.setEncryptedKey(journal.getKey()); + journalEntity.setDeleted(false); data.upsert(journalEntity); existing.remove(collection.uid); From eeda46338d9b879aa01dd50a17486935aebde4ee Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 20 Apr 2017 15:58:39 +0100 Subject: [PATCH 21/23] Disallow sharing of journals with version < 2. The reason for that is that before version 2, all the journals of a particular user shared the same encryption key, which means, sharing a journal of version one, would essentially give away the encryption key of all of its journals, even the private ones. This is thus blocked for security reasons. --- .../syncadapter/ui/ViewCollectionActivity.java | 12 ++++++++++++ app/src/main/res/values/strings.xml | 1 + 2 files changed, 13 insertions(+) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index a7ecf66c..ac6d8742 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -197,6 +197,18 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh @Override public void onClick(DialogInterface dialog, int which) { + } + }).create(); + dialog.show(); + } else if (info.version < 2) { + AlertDialog dialog = new AlertDialog.Builder(this) + .setIcon(R.drawable.ic_info_dark) + .setTitle(R.string.not_allowed_title) + .setMessage(R.string.members_old_journals_not_allowed) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } }).create(); dialog.show(); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ccde4449..d393ebc2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -107,6 +107,7 @@ Not Allowed Only the owner of this collection (%s) is allowed to edit it. Sharing of address books is currently not supported. + Sharing of old-style journals is not allowed. In order to share this journal, create a new one, and copy its contents over using the \"import\" dialog. If you are experiencing any issues, please contact support. Members From 348e24c3e3cd1c7fea6c6b97ca3a76e735de88b7 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 20 Apr 2017 20:04:09 +0100 Subject: [PATCH 22/23] Account: indicate on the list if a collection is shared. --- .../syncadapter/ui/AccountActivity.java | 36 +++++++++++-------- .../res/layout/account_collection_item.xml | 17 +++++++-- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java index 9670b1bb..12b84ea0 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.java @@ -63,8 +63,6 @@ import com.etesync.syncadapter.ui.setup.SetupUserInfoFragment; import com.etesync.syncadapter.utils.HintManager; import com.etesync.syncadapter.utils.ShowcaseBuilder; -import org.spongycastle.util.encoders.Hex; - import java.io.IOException; import java.util.List; import java.util.logging.Level; @@ -109,7 +107,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu tbCalDAV.setOnMenuItemClickListener(this); tbCalDAV.setTitle(R.string.settings_caldav); - // load CardDAV/CalDAV collections + // load CardDAV/CalDAV journals getLoaderManager().initLoader(0, getIntent().getExtras(), this); if (!HintManager.getHintSeen(this, HINT_VIEW_COLLECTION)) { @@ -206,8 +204,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final ListView list = (ListView)parent; - final ArrayAdapter adapter = (ArrayAdapter)list.getAdapter(); - final CollectionInfo info = adapter.getItem(position); + final ArrayAdapter adapter = (ArrayAdapter)list.getAdapter(); + final JournalEntity journalEntity = adapter.getItem(position); + final CollectionInfo info = journalEntity.getInfo(); startActivity(ViewCollectionActivity.newIntent(AccountActivity.this, account, info)); } @@ -233,7 +232,7 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu long id; boolean refreshing; - List collections; + List journals; } } @@ -260,8 +259,8 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu listCardDAV.setEnabled(!info.carddav.refreshing); listCardDAV.setAlpha(info.carddav.refreshing ? 0.5f : 1); - CollectionListAdapter adapter = new CollectionListAdapter(this); - adapter.addAll(info.carddav.collections); + final CollectionListAdapter adapter = new CollectionListAdapter(this, account); + adapter.addAll(info.carddav.journals); listCardDAV.setAdapter(adapter); listCardDAV.setOnItemClickListener(onItemClickListener); } else @@ -276,8 +275,8 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu listCalDAV.setEnabled(!info.caldav.refreshing); listCalDAV.setAlpha(info.caldav.refreshing ? 0.5f : 1); - final CollectionListAdapter adapter = new CollectionListAdapter(this); - adapter.addAll(info.caldav.collections); + final CollectionListAdapter adapter = new CollectionListAdapter(this, account); + adapter.addAll(info.caldav.journals); listCalDAV.setAdapter(adapter); listCalDAV.setOnItemClickListener(onItemClickListener); } else @@ -356,14 +355,14 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu info.carddav = new AccountInfo.ServiceInfo(); info.carddav.id = id; info.carddav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, ContactsContract.AUTHORITY); - info.carddav.collections = JournalEntity.getCollections(data, serviceEntity); + info.carddav.journals = JournalEntity.getJournals(data, serviceEntity); } else if (service.equals(CollectionInfo.Type.CALENDAR)) { info.caldav = new AccountInfo.ServiceInfo(); info.caldav.id = id; info.caldav.refreshing = (davService != null && davService.isRefreshing(id)) || ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority); - info.caldav.collections = JournalEntity.getCollections(data, serviceEntity); + info.caldav.journals = JournalEntity.getJournals(data, serviceEntity); } } return info; @@ -373,9 +372,12 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu /* LIST ADAPTERS */ - public static class CollectionListAdapter extends ArrayAdapter { - public CollectionListAdapter(Context context) { + public static class CollectionListAdapter extends ArrayAdapter { + private Account account; + + public CollectionListAdapter(Context context, Account account) { super(context, R.layout.account_collection_item); + this.account = account; } @Override @@ -383,7 +385,8 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu if (v == null) v = LayoutInflater.from(getContext()).inflate(R.layout.account_collection_item, parent, false); - final CollectionInfo info = getItem(position); + final JournalEntity journalEntity = getItem(position); + final CollectionInfo info = journalEntity.getInfo(); TextView tv = (TextView)v.findViewById(R.id.title); tv.setText(TextUtils.isEmpty(info.displayName) ? info.uid : info.displayName); @@ -410,6 +413,9 @@ public class AccountActivity extends AppCompatActivity implements Toolbar.OnMenu View readOnly = v.findViewById(R.id.read_only); readOnly.setVisibility(info.readOnly ? View.VISIBLE : View.GONE); + final View shared = v.findViewById(R.id.shared); + shared.setVisibility(account.name.equals(journalEntity.getOwner()) ? View.GONE : View.VISIBLE); + return v; } } diff --git a/app/src/main/res/layout/account_collection_item.xml b/app/src/main/res/layout/account_collection_item.xml index c0184338..e832040f 100644 --- a/app/src/main/res/layout/account_collection_item.xml +++ b/app/src/main/res/layout/account_collection_item.xml @@ -12,12 +12,14 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="horizontal" - android:padding="8dp"> + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:paddingLeft="12dp" + android:paddingRight="12dp"> @@ -36,17 +38,26 @@ tools:text="Description" /> + + + \ No newline at end of file From 0bade21aae91368be30ed1b046060fbcac6e8485 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Thu, 20 Apr 2017 20:10:43 +0100 Subject: [PATCH 23/23] ViewCollection: list the journal's owner if it's not owned by us. --- .../etesync/syncadapter/ui/ViewCollectionActivity.java | 9 +++++++-- app/src/main/res/layout/collection_header.xml | 9 ++++++++- app/src/main/res/values/strings.xml | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java index ac6d8742..02febe81 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/ViewCollectionActivity.java @@ -29,8 +29,6 @@ import com.etesync.syncadapter.R; import com.etesync.syncadapter.model.CollectionInfo; import com.etesync.syncadapter.model.EntryEntity; import com.etesync.syncadapter.model.JournalEntity; -import com.etesync.syncadapter.model.JournalModel; -import com.etesync.syncadapter.model.ServiceEntity; import com.etesync.syncadapter.resource.LocalAddressBook; import com.etesync.syncadapter.resource.LocalCalendar; import com.etesync.syncadapter.ui.importlocal.ImportActivity; @@ -96,6 +94,13 @@ public class ViewCollectionActivity extends AppCompatActivity implements Refresh final TextView desc = (TextView) findViewById(R.id.description); desc.setText(info.description); + + final TextView owner = (TextView) findViewById(R.id.owner); + if (account.name.equals(journalEntity.getOwner())) { + owner.setVisibility(View.GONE); + } else { + owner.setText(getString(R.string.account_owner, journalEntity.getOwner())); + } } @Override diff --git a/app/src/main/res/layout/collection_header.xml b/app/src/main/res/layout/collection_header.xml index 178f7fc4..6c55f861 100644 --- a/app/src/main/res/layout/collection_header.xml +++ b/app/src/main/res/layout/collection_header.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginBottom="16dp" android:orientation="vertical"> + tools:text="Description" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d393ebc2..bff92d20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -103,6 +103,7 @@ Change Journal In order to import contacts and calendars into EteSync, you need to click on the menu, and choose \"Import\". + Owner: %s Only the owner of this collection (%s) is allowed to view its members. Not Allowed Only the owner of this collection (%s) is allowed to edit it.