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); + } }