1
0
mirror of https://github.com/etesync/android synced 2024-11-22 16:08:13 +00:00

Move to the external journalmanager module (moved the code there)

This commit is contained in:
Tom Hacohen 2020-01-28 17:01:21 +02:00
parent b70e8903c5
commit ceead4815b
35 changed files with 48 additions and 1792 deletions

View File

@ -133,6 +133,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.anko:anko-commons:0.10.4" implementation "org.jetbrains.anko:anko-commons:0.10.4"
implementation "com.etesync:journalmanager:1.0.1"
def acraVersion = '5.3.0' def acraVersion = '5.3.0'
implementation "ch.acra:acra-mail:$acraVersion" implementation "ch.acra:acra-mail:$acraVersion"
@ -157,10 +158,6 @@ dependencies {
kapt "io.requery:requery-processor:$requeryVersion" kapt "io.requery:requery-processor:$requeryVersion"
implementation 'com.google.code.findbugs:jsr305:3.0.2' implementation 'com.google.code.findbugs:jsr305:3.0.2'
def spongyCastleVersion = "1.54.0.0"
implementation "com.madgag.spongycastle:core:$spongyCastleVersion"
implementation "com.madgag.spongycastle:prov:$spongyCastleVersion"
def okhttp3Version = "3.12.1" def okhttp3Version = "3.12.1"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3Version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3Version"

View File

@ -19,7 +19,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod import at.bitfire.vcard4android.GroupMethod
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.utils.Base64 import com.etesync.syncadapter.utils.Base64
import java.net.URI import java.net.URI

View File

@ -1,116 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.GsonHelper
import com.etesync.syncadapter.log.Logger
import okhttp3.*
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.util.logging.Level
import javax.net.ssl.SSLHandshakeException
import javax.net.ssl.SSLProtocolException
abstract class BaseManager {
protected var remote: HttpUrl? = null
protected var client: OkHttpClient? = null
@Throws(Exceptions.HttpException::class)
fun newCall(request: Request): Response {
val response: Response
try {
Logger.log.fine("Making request for ${request.url()}")
response = client!!.newCall(request).execute()
} catch (e: IOException) {
if (e is SSLProtocolException) {
throw e
} else if (e is SSLHandshakeException && e.cause is SSLProtocolException) {
throw e
}
Logger.log.log(Level.SEVERE, "Failed while connecting to server", e)
throw Exceptions.ServiceUnavailableException("[" + e.javaClass.name + "] " + e.localizedMessage)
}
if (!response.isSuccessful) {
val apiError = if (response.header("Content-Type", "application/json")!!.startsWith("application/json"))
GsonHelper.gson.fromJson(response.body()!!.charStream(), ApiError::class.java)
else
ApiError(code="got_html", detail="Got HTML while expecting JSON")
when (response.code()) {
HttpURLConnection.HTTP_BAD_GATEWAY -> throw Exceptions.BadGatewayException(response, "Bad gateway: most likely a server restart")
HttpURLConnection.HTTP_UNAVAILABLE -> throw Exceptions.ServiceUnavailableException(response, "Service unavailable")
HttpURLConnection.HTTP_UNAUTHORIZED -> throw Exceptions.UnauthorizedException(response, "Unauthorized auth token")
HttpURLConnection.HTTP_CONFLICT -> throw Exceptions.ConflictException(response, "Http conflict")
HttpURLConnection.HTTP_FORBIDDEN -> {
if (apiError.code == "service_inactive") {
throw Exceptions.UserInactiveException(response, apiError.detail)
} else if (apiError.code == "associate_not_allowed") {
throw Exceptions.AssociateNotAllowedException(response, apiError.detail)
} else if (apiError.code == "journal_owner_inactive") {
throw Exceptions.ReadOnlyException(response, apiError.detail)
}
}
}// Fall through. We want to always throw when unsuccessful.
throw Exceptions.HttpException(response, apiError.detail)
}
return response
}
internal class ApiError(
var detail: String? = null,
var code: String? = null
)
open class Base {
var content: ByteArray? = null
var uid: String? = null
fun getContent(crypto: Crypto.CryptoManager): String {
return String(crypto.decrypt(content!!)!!)
}
fun setContent(crypto: Crypto.CryptoManager, content: String) {
this.content = crypto.encrypt(content.toByteArray())
}
fun calculateHmac(crypto: Crypto.CryptoManager, uuid: String?): ByteArray {
val hashContent = ByteArrayOutputStream()
try {
if (uuid != null) {
hashContent.write(uuid.toByteArray())
}
hashContent.write(content!!)
} catch (e: IOException) {
// Can never happen, but just in case, return a bad hmac
return "DEADBEEFDEADBEEFDEADBEEFDEADBEEF".toByteArray()
}
return crypto.hmac(hashContent.toByteArray())
}
protected constructor() {}
constructor(crypto: Crypto.CryptoManager, content: String, uid: String) {
setContent(crypto, content)
this.uid = uid
}
override fun toString(): String {
return javaClass.simpleName + "<" + uid + ">"
}
internal open fun toJson(): String {
return GsonHelper.gson.toJson(this, javaClass)
}
}
companion object {
val JSON = MediaType.parse("application/json; charset=utf-8")
}
}

View File

@ -1,8 +0,0 @@
package com.etesync.syncadapter.journalmanager
class Constants {
companion object {
@JvmField
val CURRENT_VERSION = 2
}
}

View File

@ -1,262 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.journalmanager.util.ByteUtil
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.utils.Base64
import org.apache.commons.lang3.ArrayUtils
import org.spongycastle.crypto.AsymmetricBlockCipher
import org.spongycastle.crypto.BufferedBlockCipher
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
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.io.Serializable
import java.math.BigInteger
import java.security.SecureRandom
import java.util.*
object Crypto {
@JvmStatic
fun deriveKey(salt: String, password: String): String {
val keySize = 190
return Base64.encodeToString(SCrypt.generate(password.toByteArray(), salt.toByteArray(), 16384, 8, 1, keySize), Base64.NO_WRAP)
}
@JvmStatic
fun generateKeyPair(): AsymmetricKeyPair? {
val keyPairGenerator = RSAKeyPairGenerator()
keyPairGenerator.init(RSAKeyGenerationParameters(BigInteger.valueOf(65537), SecureRandom(), 3072, 160))
val keyPair = keyPairGenerator.generateKeyPair()
try {
val privateKeyInfo = PrivateKeyInfoFactory.createPrivateKeyInfo(keyPair.private)
val publicKeyInfo = SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(keyPair.public)
return AsymmetricKeyPair(privateKeyInfo.encoded, publicKeyInfo.encoded)
} catch (e: IOException) {
e.printStackTrace()
}
return null
}
class AsymmetricKeyPair(val privateKey: ByteArray, val publicKey: ByteArray) : Serializable
class AsymmetricCryptoManager(private val keyPair: AsymmetricKeyPair) {
fun encrypt(pubkey: ByteArray, content: ByteArray?): ByteArray? {
var cipher: AsymmetricBlockCipher = RSAEngine()
cipher = OAEPEncoding(cipher)
try {
cipher.init(true, PublicKeyFactory.createKey(pubkey))
return cipher.processBlock(content, 0, content!!.size)
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InvalidCipherTextException) {
e.printStackTrace()
Logger.log.severe("Invalid ciphertext: " + Base64.encodeToString(content, Base64.NO_WRAP))
}
return null
}
fun decrypt(cipherText: ByteArray): ByteArray? {
var cipher: AsymmetricBlockCipher = RSAEngine()
cipher = OAEPEncoding(cipher)
try {
cipher.init(false, PrivateKeyFactory.createKey(keyPair.privateKey))
return cipher.processBlock(cipherText, 0, cipherText.size)
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InvalidCipherTextException) {
e.printStackTrace()
Logger.log.severe("Invalid ciphertext: " + Base64.encodeToString(cipherText, Base64.NO_WRAP))
}
return null
}
companion object {
fun getKeyFingerprint(pubkey: ByteArray): ByteArray {
return sha256(pubkey)
}
@JvmStatic
fun getPrettyKeyFingerprint(pubkey: ByteArray): String {
val fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(pubkey)
val spacing = " "
val ret = getEncodedChunk(fingerprint, 0) + spacing +
getEncodedChunk(fingerprint, 4) + spacing +
getEncodedChunk(fingerprint, 8) + spacing +
getEncodedChunk(fingerprint, 12) + "\n" +
getEncodedChunk(fingerprint, 16) + spacing +
getEncodedChunk(fingerprint, 20) + spacing +
getEncodedChunk(fingerprint, 24) + spacing +
getEncodedChunk(fingerprint, 28)
return ret.trim { it <= ' ' }
}
private fun getEncodedChunk(hash: ByteArray, offset: Int): String {
val chunk = ByteUtil.byteArray4ToLong(hash, offset) % 100000
return String.format(Locale.getDefault(), "%05d", chunk)
}
}
}
class CryptoManager {
val version: Byte
private var cipherKey: ByteArray? = null
private var hmacKey: ByteArray? = null
private var derivedKey: ByteArray? = null
private val random: SecureRandom
get() = SecureRandom()
private fun setDerivedKey(derivedKey: ByteArray?) {
cipherKey = hmac256("aes".toByteArray(), derivedKey)
hmacKey = hmac256("hmac".toByteArray(), derivedKey)
}
constructor(version: Int, keyPair: AsymmetricKeyPair, encryptedKey: ByteArray) {
val cryptoManager = Crypto.AsymmetricCryptoManager(keyPair)
derivedKey = cryptoManager.decrypt(encryptedKey)
this.version = version.toByte()
setDerivedKey(derivedKey)
}
@Throws(Exceptions.IntegrityException::class, Exceptions.VersionTooNewException::class)
constructor(version: Int, keyBase64: String, salt: String) {
if (version > java.lang.Byte.MAX_VALUE) {
throw Exceptions.IntegrityException("Version is out of range.")
} else if (version > Constants.CURRENT_VERSION) {
throw Exceptions.VersionTooNewException("Version to new: " + version.toString())
} else if (version == 1) {
derivedKey = Base64.decode(keyBase64, Base64.NO_WRAP)
} else {
derivedKey = hmac256(salt.toByteArray(), Base64.decode(keyBase64, Base64.NO_WRAP))
}
this.version = version.toByte()
setDerivedKey(derivedKey)
}
private fun getCipher(iv: ByteArray, encrypt: Boolean): BufferedBlockCipher {
val key = KeyParameter(cipherKey!!)
val params = ParametersWithIV(key, iv)
val padding = PKCS7Padding()
val cipher = PaddedBufferedBlockCipher(
CBCBlockCipher(AESEngine()), padding)
cipher.reset()
cipher.init(encrypt, params)
return cipher
}
fun decrypt(_data: ByteArray): ByteArray? {
val iv = Arrays.copyOfRange(_data, 0, blockSize)
val data = Arrays.copyOfRange(_data, blockSize, _data.size)
val cipher = getCipher(iv, false)
val buf = ByteArray(cipher.getOutputSize(data.size))
var len = cipher.processBytes(data, 0, data.size, buf, 0)
try {
len += cipher.doFinal(buf, len)
} catch (e: InvalidCipherTextException) {
e.printStackTrace()
Logger.log.severe("Invalid ciphertext: " + Base64.encodeToString(_data, Base64.NO_WRAP))
return null
}
// remove padding
val out = ByteArray(len)
System.arraycopy(buf, 0, out, 0, len)
return out
}
fun encrypt(data: ByteArray): ByteArray? {
val iv = ByteArray(blockSize)
random.nextBytes(iv)
val cipher = getCipher(iv, true)
val buf = ByteArray(cipher.getOutputSize(data.size) + blockSize)
System.arraycopy(iv, 0, buf, 0, iv.size)
val len = iv.size + cipher.processBytes(data, 0, data.size, buf, iv.size)
try {
cipher.doFinal(buf, len)
} catch (e: InvalidCipherTextException) {
Logger.log.severe("Invalid ciphertext: " + Base64.encodeToString(data, Base64.NO_WRAP))
e.printStackTrace()
return null
}
return buf
}
fun hmac(data: ByteArray): ByteArray {
return if (version.toInt() == 1) {
hmac256(hmacKey, data)
} else {
// Starting from version 2 we hmac the version too.
hmac256(hmacKey, ArrayUtils.add(data, version))
}
}
fun getEncryptedKey(keyPair: AsymmetricKeyPair, publicKey: ByteArray): ByteArray? {
val cryptoManager = AsymmetricCryptoManager(keyPair)
return cryptoManager.encrypt(publicKey, derivedKey)
}
companion object {
val HMAC_SIZE = 256 / 8 // hmac256 in bytes
private val blockSize = 16 // AES's block size in bytes
private fun hmac256(keyByte: ByteArray?, data: ByteArray?): ByteArray {
val hmac = HMac(SHA256Digest())
val key = KeyParameter(keyByte!!)
val ret = ByteArray(hmac.macSize)
hmac.init(key)
hmac.update(data, 0, data!!.size)
hmac.doFinal(ret, 0)
return ret
}
}
}
internal fun sha256(base: String): String {
return toHex(sha256(base.toByteArray()))
}
private fun sha256(base: ByteArray): ByteArray {
val digest = SHA256Digest()
digest.update(base, 0, base.size)
val ret = ByteArray(digest.digestSize)
digest.doFinal(ret, 0)
return ret
}
internal fun toHex(bytes: ByteArray): String {
return Hex.toHexString(bytes).toLowerCase()
}
}

View File

@ -1,141 +0,0 @@
package com.etesync.syncadapter.journalmanager
import at.bitfire.cert4android.Constants
import okhttp3.Response
import okio.Buffer
import java.io.IOException
import java.io.Serializable
import java.security.GeneralSecurityException
class Exceptions {
class AssociateNotAllowedException(response: Response, message: String?) : HttpException(response, message)
class ConflictException(response: Response, message: String?) : IgnorableHttpException(response, message ?: "Conflict exception")
class ReadOnlyException(response: Response, message: String?) : HttpException(response, message)
class UnauthorizedException(response: Response, message: String?) : HttpException(response, message)
class UserInactiveException(response: Response, message: String?) : HttpException(response, message)
class BadGatewayException(response: Response, message: String) : IgnorableHttpException(response, message)
class ServiceUnavailableException : IgnorableHttpException {
var retryAfter: Long = 0
constructor(message: String) : super(message) {
this.retryAfter = 0
}
constructor(response: Response, message: String) : super(response, message) {
this.retryAfter = java.lang.Long.valueOf(response.header("Retry-After", "0")!!)
}
}
class IntegrityException(message: String) : GeneralSecurityException(message)
open class IgnorableHttpException : HttpException {
constructor(message: String) : super(message)
constructor(response: Response, message: String) : super(response, message)
}
open class GenericCryptoException(message: String) : Exception(message)
class VersionTooNewException(message: String) : GenericCryptoException(message)
open class HttpException : Exception, Serializable {
internal val status: Int
override val message: String
val request: String?
val response: String?
constructor(message: String) : super(message) {
this.message = message
this.status = -1
this.response = null
this.request = this.response
}
constructor(status: Int, message: String) : super(status.toString() + " " + message) {
this.status = status
this.message = message
response = null
request = response
}
@JvmOverloads constructor(response: Response, custom_message: String? = null) : super(response.code().toString() + " " + response.message()) {
status = response.code()
message = custom_message ?: response.message()
/* As we don't know the media type and character set of request and response body,
only printable ASCII characters will be shown in clear text. Other octets will
be shown as "[xx]" where xx is the hex value of the octet.
*/
// format request
val request = response.request()
var formatted = StringBuilder()
formatted.append(request.method()).append(" ").append(request.url().encodedPath()).append("\n")
var headers = request.headers()
for (name in headers.names()) {
for (value in headers.values(name)) {
/* Redact authorization token. */
if (name == "Authorization") {
formatted.append(name).append(": ").append("XXXXXX").append("\n")
} else {
formatted.append(name).append(": ").append(value).append("\n")
}
}
}
if (request.body() != null)
try {
formatted.append("\n")
val buffer = Buffer()
request.body()!!.writeTo(buffer)
while (!buffer.exhausted())
appendByte(formatted, buffer.readByte())
} catch (e: IOException) {
Constants.log.warning("Couldn't read request body")
}
this.request = formatted.toString()
// format response
formatted = StringBuilder()
formatted.append(response.protocol()).append(" ").append(response.code()).append(" ").append(message).append("\n")
headers = response.headers()
for (name in headers.names())
for (value in headers.values(name))
formatted.append(name).append(": ").append(value).append("\n")
if (response.body() != null) {
val body = response.body()
try {
formatted.append("\n")
for (b in body!!.bytes())
appendByte(formatted, b)
} catch (e: IOException) {
Constants.log.warning("Couldn't read response body")
}
body!!.close()
}
this.response = formatted.toString()
}
private fun appendByte(formatted: StringBuilder, b: Byte) {
if (b == '\r'.toByte())
formatted.append("[CR]")
else if (b == '\n'.toByte())
formatted.append("[LF]\n")
else if (b >= 0x20 && b <= 0x7E)
// printable ASCII
formatted.append(b.toChar())
else
formatted.append("[" + Integer.toHexString(b.toInt() and 0xff) + "]")
}
}
}

View File

@ -1,61 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.GsonHelper
import okhttp3.*
import java.io.IOException
import java.net.HttpURLConnection
class JournalAuthenticator(private val client: OkHttpClient, private val remote: HttpUrl) {
private inner class AuthResponse private constructor() {
val token: String? = null
}
@Throws(Exceptions.HttpException::class, IOException::class)
fun getAuthToken(username: String, password: String): String? {
val remote = remote.newBuilder()
.addPathSegments("api-token-auth")
.addPathSegment("")
.build()
val formBuilder = FormBody.Builder()
.add("username", username)
.add("password", password)
val request = Request.Builder()
.post(formBuilder.build())
.url(remote)
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
return GsonHelper.gson.fromJson(response.body()!!.charStream(), AuthResponse::class.java).token
} else if (response.code() == HttpURLConnection.HTTP_BAD_REQUEST) {
throw Exceptions.UnauthorizedException(response, "Username or password incorrect")
} else {
throw Exceptions.HttpException(response)
}
}
fun invalidateAuthToken(authToken: String) {
val remote = remote.newBuilder()
.addPathSegments("api/logout")
.addPathSegment("")
.build()
val body = RequestBody.create(null, byteArrayOf())
val request = Request.Builder()
.post(body)
.url(remote)
.build()
val response = client.newCall(request).execute()
if (response.isSuccessful) {
return
} else {
when (response.code()) {
HttpURLConnection.HTTP_BAD_GATEWAY -> throw Exceptions.BadGatewayException(response, "Bad gateway: most likely a server restart")
HttpURLConnection.HTTP_UNAVAILABLE -> throw Exceptions.ServiceUnavailableException(response, "Service unavailable")
HttpURLConnection.HTTP_UNAUTHORIZED -> throw Exceptions.UnauthorizedException(response, "Unauthorized auth token")
}
}
}
}

View File

@ -1,116 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.GsonHelper
import com.etesync.syncadapter.log.Logger
import com.google.gson.reflect.TypeToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
class JournalEntryManager(httpClient: OkHttpClient, remote: HttpUrl, val uid: String) : BaseManager() {
init {
this.remote = remote.newBuilder()
.addPathSegments("api/v1/journals")
.addPathSegments(uid)
.addPathSegment("entries")
.addPathSegment("")
.build()
Logger.log.info("Created for: " + this.remote!!.toString())
this.client = httpClient
}
@Throws(Exceptions.HttpException::class, Exceptions.IntegrityException::class)
fun list(crypto: Crypto.CryptoManager, last: String?, limit: Int): List<Entry> {
var previousEntry: Entry? = null
val urlBuilder = this.remote!!.newBuilder()
if (last != null) {
urlBuilder.addQueryParameter("last", last)
previousEntry = Entry.getFakeWithUid(last)
}
if (limit > 0) {
urlBuilder.addQueryParameter("limit", limit.toString())
}
val remote = urlBuilder.build()
val request = Request.Builder()
.get()
.url(remote)
.build()
val response = newCall(request)
val body = response.body()
val ret = GsonHelper.gson.fromJson<List<Entry>>(body!!.charStream(), entryType)
for (entry in ret) {
entry.verify(crypto, previousEntry)
previousEntry = entry
}
return ret
}
@Throws(Exceptions.HttpException::class)
fun create(entries: List<Entry>, last: String?) {
val urlBuilder = this.remote!!.newBuilder()
if (last != null) {
urlBuilder.addQueryParameter("last", last)
}
val remote = urlBuilder.build()
val body = RequestBody.create(BaseManager.JSON, GsonHelper.gson.toJson(entries, entryType))
val request = Request.Builder()
.post(body)
.url(remote)
.build()
newCall(request)
}
class Entry : BaseManager.Base() {
fun update(crypto: Crypto.CryptoManager, content: String, previous: Entry?) {
setContent(crypto, content)
uid = calculateHmac(crypto, previous)
}
@Throws(Exceptions.IntegrityException::class)
internal fun verify(crypto: Crypto.CryptoManager, previous: Entry?) {
val correctHash = calculateHmac(crypto, previous)
if (uid != correctHash) {
throw Exceptions.IntegrityException("Bad HMAC. $uid != $correctHash")
}
}
private fun calculateHmac(crypto: Crypto.CryptoManager, previous: Entry?): String {
var uuid: String? = null
if (previous != null) {
uuid = previous.uid
}
return Crypto.toHex(calculateHmac(crypto, uuid))
}
companion object {
@JvmStatic
fun getFakeWithUid(uid: String): Entry {
val ret = Entry()
ret.uid = uid
return ret
}
}
}
companion object {
private val entryType = object : TypeToken<List<Entry>>() {
}.type
}
}

View File

@ -1,220 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.GsonHelper
import com.etesync.syncadapter.journalmanager.Crypto.CryptoManager.Companion.HMAC_SIZE
import com.etesync.syncadapter.journalmanager.Crypto.sha256
import com.etesync.syncadapter.journalmanager.Crypto.toHex
import com.etesync.syncadapter.log.Logger
import com.google.gson.reflect.TypeToken
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import org.spongycastle.util.Arrays
import java.util.*
class JournalManager(httpClient: OkHttpClient, remote: HttpUrl) : BaseManager() {
init {
this.remote = remote.newBuilder()
.addPathSegments("api/v1/journals")
.addPathSegment("")
.build()
Logger.log.info("Created for: " + this.remote!!.toString())
this.client = httpClient
}
@Throws(Exceptions.HttpException::class)
fun list(): List<Journal> {
val request = Request.Builder()
.get()
.url(remote!!)
.build()
val response = newCall(request)
val body = response.body()
val ret = GsonHelper.gson.fromJson<List<Journal>>(body!!.charStream(), journalType)
for (journal in ret) {
journal.processFromJson()
}
return ret
}
@Throws(Exceptions.HttpException::class)
fun delete(journal: Journal) {
val remote = this.remote!!.resolve(journal.uid!! + "/")
val request = Request.Builder()
.delete()
.url(remote!!)
.build()
newCall(request)
}
@Throws(Exceptions.HttpException::class)
fun create(journal: Journal) {
val body = RequestBody.create(BaseManager.JSON, journal.toJson())
val request = Request.Builder()
.post(body)
.url(remote!!)
.build()
newCall(request)
}
@Throws(Exceptions.HttpException::class)
fun update(journal: Journal) {
val remote = this.remote!!.resolve(journal.uid!! + "/")
val body = RequestBody.create(BaseManager.JSON, journal.toJson())
val request = Request.Builder()
.put(body)
.url(remote!!)
.build()
newCall(request)
}
private fun getMemberRemote(journal: Journal, user: String?): HttpUrl {
val bulider = this.remote!!.newBuilder()
bulider.addPathSegment(journal.uid!!)
.addPathSegment("members")
if (user != null) {
bulider.addPathSegment(user)
}
bulider.addPathSegment("")
return bulider.build()
}
@Throws(Exceptions.HttpException::class, Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
fun listMembers(journal: Journal): List<Member> {
val request = Request.Builder()
.get()
.url(getMemberRemote(journal, null))
.build()
val response = newCall(request)
val body = response.body()
return GsonHelper.gson.fromJson(body!!.charStream(), memberType)
}
@Throws(Exceptions.HttpException::class)
fun deleteMember(journal: Journal, member: Member) {
val body = RequestBody.create(BaseManager.JSON, member.toJson())
val request = Request.Builder()
.delete(body)
.url(getMemberRemote(journal, member.user))
.build()
newCall(request)
}
@Throws(Exceptions.HttpException::class)
fun addMember(journal: Journal, member: Member) {
val body = RequestBody.create(BaseManager.JSON, member.toJson())
val request = Request.Builder()
.post(body)
.url(getMemberRemote(journal, null))
.build()
newCall(request)
}
class Journal : BaseManager.Base {
val owner: String? = null
val key: ByteArray? = null
var version = -1
val readOnly = false
@Transient
private var hmac: ByteArray? = null
private constructor() : super() {}
constructor(crypto: Crypto.CryptoManager, content: String, uid: String) : super(crypto, content, uid) {
hmac = calculateHmac(crypto)
version = crypto.version.toInt()
}
fun processFromJson() {
hmac = Arrays.copyOfRange(content!!, 0, HMAC_SIZE)
content = Arrays.copyOfRange(content!!, HMAC_SIZE, content!!.size)
}
@Throws(Exceptions.IntegrityException::class)
fun verify(crypto: Crypto.CryptoManager) {
val hmac = this.hmac;
if (hmac == null) {
throw Exceptions.IntegrityException("HMAC is null!")
}
val correctHash = calculateHmac(crypto)
if (!Arrays.areEqual(hmac, correctHash)) {
throw Exceptions.IntegrityException("Bad HMAC. " + toHex(hmac) + " != " + toHex(correctHash))
}
}
internal fun calculateHmac(crypto: Crypto.CryptoManager): ByteArray {
return super.calculateHmac(crypto, uid)
}
internal override fun toJson(): String {
val rawContent = content
content = Arrays.concatenate(hmac, rawContent)
val ret = super.toJson()
content = rawContent
return ret
}
companion object {
@JvmStatic
fun fakeWithUid(uid: String): Journal {
val ret = Journal()
ret.uid = uid
return ret
}
@JvmStatic
fun genUid(): String {
return sha256(UUID.randomUUID().toString())
}
}
}
class Member {
val user: String?
val key: ByteArray?
val readOnly: Boolean
private constructor() {
this.user = null
this.key = null
this.readOnly = false
}
constructor(user: String, encryptedKey: ByteArray, readOnly: Boolean = false) {
this.user = user
this.key = encryptedKey
this.readOnly = readOnly
}
internal fun toJson(): String {
return GsonHelper.gson.toJson(this, javaClass)
}
}
companion object {
private val journalType = object : TypeToken<List<Journal>>() {
}.type
private val memberType = object : TypeToken<List<Member>>() {
}.type
}
}

View File

@ -1,148 +0,0 @@
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.GsonHelper
import com.etesync.syncadapter.journalmanager.Crypto.CryptoManager.Companion.HMAC_SIZE
import com.etesync.syncadapter.journalmanager.Crypto.toHex
import okhttp3.*
import org.spongycastle.util.Arrays
import java.io.IOException
import java.net.HttpURLConnection
class UserInfoManager(httpClient: OkHttpClient, remote: HttpUrl) : BaseManager() {
init {
this.remote = remote.newBuilder()
.addPathSegments("api/v1/user")
.addPathSegment("")
.build()
this.client = httpClient
}
@Throws(Exceptions.HttpException::class)
fun fetch(owner: String): UserInfo? {
val remote = this.remote!!.newBuilder().addPathSegment(owner).addPathSegment("").build()
val request = Request.Builder()
.get()
.url(remote)
.build()
val response: Response
try {
response = newCall(request)
} catch (e: Exceptions.HttpException) {
return if (e.status == HttpURLConnection.HTTP_NOT_FOUND) {
null
} else {
throw e
}
}
val body = response.body()
val ret = GsonHelper.gson.fromJson(body!!.charStream(), UserInfo::class.java)
ret.owner = owner
return ret
}
@Throws(Exceptions.HttpException::class)
fun delete(userInfo: UserInfo) {
val remote = this.remote!!.newBuilder().addPathSegment(userInfo.owner!!).addPathSegment("").build()
val request = Request.Builder()
.delete()
.url(remote)
.build()
newCall(request)
}
@Throws(Exceptions.HttpException::class)
fun create(userInfo: UserInfo) {
val body = RequestBody.create(BaseManager.JSON, userInfo.toJson())
val request = Request.Builder()
.post(body)
.url(remote!!)
.build()
newCall(request)
}
@Throws(Exceptions.HttpException::class)
fun update(userInfo: UserInfo) {
val remote = this.remote!!.newBuilder().addPathSegment(userInfo.owner!!).addPathSegment("").build()
val body = RequestBody.create(BaseManager.JSON, userInfo.toJson())
val request = Request.Builder()
.put(body)
.url(remote)
.build()
newCall(request)
}
class UserInfo {
@Transient
var owner: String? = null
@Transient
val plan: String? = null
val version: Byte?
val pubkey: ByteArray?
private var content: ByteArray? = null
fun getContent(crypto: Crypto.CryptoManager): ByteArray? {
val content = Arrays.copyOfRange(this.content!!, HMAC_SIZE, this.content!!.size)
return crypto.decrypt(content)
}
fun setContent(crypto: Crypto.CryptoManager, rawContent: ByteArray) {
val content = crypto.encrypt(rawContent)
this.content = Arrays.concatenate(calculateHmac(crypto, content), content)
}
@Throws(Exceptions.IntegrityException::class)
fun verify(crypto: Crypto.CryptoManager) {
if (this.content == null) {
// Nothing to verify.
return
}
val hmac = Arrays.copyOfRange(this.content!!, 0, HMAC_SIZE)
val content = Arrays.copyOfRange(this.content!!, HMAC_SIZE, this.content!!.size)
val correctHash = calculateHmac(crypto, content)
if (!Arrays.areEqual(hmac, correctHash)) {
throw Exceptions.IntegrityException("Bad HMAC. " + toHex(hmac) + " != " + toHex(correctHash))
}
}
private fun calculateHmac(crypto: Crypto.CryptoManager, content: ByteArray?): ByteArray {
return crypto.hmac(Arrays.concatenate(content, pubkey))
}
private constructor() {
this.version = null
this.pubkey = null
}
constructor(crypto: Crypto.CryptoManager, owner: String, pubkey: ByteArray, content: ByteArray) {
this.owner = owner
this.pubkey = pubkey
version = crypto.version
setContent(crypto, content)
}
internal fun toJson(): String {
return GsonHelper.gson.toJson(this, javaClass)
}
companion object {
@JvmStatic
@Throws(IOException::class)
fun generate(cryptoManager: Crypto.CryptoManager, owner: String): UserInfo {
val keyPair = Crypto.generateKeyPair()
return UserInfo(cryptoManager, owner, keyPair!!.publicKey, keyPair.privateKey)
}
}
}
}

View File

@ -1,248 +0,0 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package com.etesync.syncadapter.journalmanager.util;
import org.apache.commons.codec.binary.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.text.ParseException;
public class ByteUtil {
public static byte[] combine(byte[]... elements) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (byte[] element : elements) {
baos.write(element);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static byte[][] split(byte[] input, int firstLength, int secondLength) {
byte[][] parts = new byte[2][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
return parts;
}
public static byte[][] split(byte[] input, int firstLength, int secondLength, int thirdLength)
throws ParseException
{
if (input == null || firstLength < 0 || secondLength < 0 || thirdLength < 0 ||
input.length < firstLength + secondLength + thirdLength)
{
throw new ParseException("Input too small: " + (input == null ? null : Hex.encodeHex(input)), 0);
}
byte[][] parts = new byte[3][];
parts[0] = new byte[firstLength];
System.arraycopy(input, 0, parts[0], 0, firstLength);
parts[1] = new byte[secondLength];
System.arraycopy(input, firstLength, parts[1], 0, secondLength);
parts[2] = new byte[thirdLength];
System.arraycopy(input, firstLength + secondLength, parts[2], 0, thirdLength);
return parts;
}
public static byte[] trim(byte[] input, int length) {
byte[] result = new byte[length];
System.arraycopy(input, 0, result, 0, result.length);
return result;
}
public static byte[] copyFrom(byte[] input) {
byte[] output = new byte[input.length];
System.arraycopy(input, 0, output, 0, output.length);
return output;
}
public static byte intsToByteHighAndLow(int highValue, int lowValue) {
return (byte)((highValue << 4 | lowValue) & 0xFF);
}
public static int highBitsToInt(byte value) {
return (value & 0xFF) >> 4;
}
public static int lowBitsToInt(byte value) {
return (value & 0xF);
}
public static int highBitsToMedium(int value) {
return (value >> 12);
}
public static int lowBitsToMedium(int value) {
return (value & 0xFFF);
}
public static byte[] shortToByteArray(int value) {
byte[] bytes = new byte[2];
shortToByteArray(bytes, 0, value);
return bytes;
}
public static int shortToByteArray(byte[] bytes, int offset, int value) {
bytes[offset+1] = (byte)value;
bytes[offset] = (byte)(value >> 8);
return 2;
}
public static int shortToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
return 2;
}
public static byte[] mediumToByteArray(int value) {
byte[] bytes = new byte[3];
mediumToByteArray(bytes, 0, value);
return bytes;
}
public static int mediumToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 2] = (byte)value;
bytes[offset + 1] = (byte)(value >> 8);
bytes[offset] = (byte)(value >> 16);
return 3;
}
public static byte[] intToByteArray(int value) {
byte[] bytes = new byte[4];
intToByteArray(bytes, 0, value);
return bytes;
}
public static int intToByteArray(byte[] bytes, int offset, int value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset] = (byte)(value >> 24);
return 4;
}
public static int intToLittleEndianByteArray(byte[] bytes, int offset, int value) {
bytes[offset] = (byte)value;
bytes[offset+1] = (byte)(value >> 8);
bytes[offset+2] = (byte)(value >> 16);
bytes[offset+3] = (byte)(value >> 24);
return 4;
}
public static byte[] longToByteArray(long l) {
byte[] bytes = new byte[8];
longToByteArray(bytes, 0, l);
return bytes;
}
public static int longToByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 7] = (byte)value;
bytes[offset + 6] = (byte)(value >> 8);
bytes[offset + 5] = (byte)(value >> 16);
bytes[offset + 4] = (byte)(value >> 24);
bytes[offset + 3] = (byte)(value >> 32);
bytes[offset + 2] = (byte)(value >> 40);
bytes[offset + 1] = (byte)(value >> 48);
bytes[offset] = (byte)(value >> 56);
return 8;
}
public static int longTo4ByteArray(byte[] bytes, int offset, long value) {
bytes[offset + 3] = (byte)value;
bytes[offset + 2] = (byte)(value >> 8);
bytes[offset + 1] = (byte)(value >> 16);
bytes[offset + 0] = (byte)(value >> 24);
return 4;
}
public static int byteArrayToShort(byte[] bytes) {
return byteArrayToShort(bytes, 0);
}
public static int byteArrayToShort(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 8 | (bytes[offset + 1] & 0xff);
}
// The SSL patented 3-byte Value.
public static int byteArrayToMedium(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset + 2] & 0xff);
}
public static int byteArrayToInt(byte[] bytes) {
return byteArrayToInt(bytes, 0);
}
public static int byteArrayToInt(byte[] bytes, int offset) {
return
(bytes[offset] & 0xff) << 24 |
(bytes[offset + 1] & 0xff) << 16 |
(bytes[offset + 2] & 0xff) << 8 |
(bytes[offset + 3] & 0xff);
}
public static int byteArrayToIntLittleEndian(byte[] bytes, int offset) {
return
(bytes[offset + 3] & 0xff) << 24 |
(bytes[offset + 2] & 0xff) << 16 |
(bytes[offset + 1] & 0xff) << 8 |
(bytes[offset] & 0xff);
}
public static long byteArrayToLong(byte[] bytes) {
return byteArrayToLong(bytes, 0);
}
public static long byteArray4ToLong(byte[] bytes, int offset) {
return
((bytes[offset + 0] & 0xffL) << 24) |
((bytes[offset + 1] & 0xffL) << 16) |
((bytes[offset + 2] & 0xffL) << 8) |
((bytes[offset + 3] & 0xffL));
}
public static long byteArray5ToLong(byte[] bytes, int offset) {
return
((bytes[offset] & 0xffL) << 32) |
((bytes[offset + 1] & 0xffL) << 24) |
((bytes[offset + 2] & 0xffL) << 16) |
((bytes[offset + 3] & 0xffL) << 8) |
((bytes[offset + 4] & 0xffL));
}
public static long byteArrayToLong(byte[] bytes, int offset) {
return
((bytes[offset] & 0xffL) << 56) |
((bytes[offset + 1] & 0xffL) << 48) |
((bytes[offset + 2] & 0xffL) << 40) |
((bytes[offset + 3] & 0xffL) << 32) |
((bytes[offset + 4] & 0xffL) << 24) |
((bytes[offset + 5] & 0xffL) << 16) |
((bytes[offset + 6] & 0xffL) << 8) |
((bytes[offset + 7] & 0xffL));
}
}

View File

@ -9,13 +9,11 @@
package com.etesync.syncadapter.model package com.etesync.syncadapter.model
import android.content.ContentValues import android.content.ContentValues
import com.etesync.syncadapter.journalmanager.Constants import com.etesync.journalmanager.Constants
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.model.ServiceDB.Collections import com.etesync.syncadapter.model.ServiceDB.Collections
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.annotations.Expose import com.google.gson.annotations.Expose
import io.requery.Persistable
import io.requery.sql.EntityDataStore
import java.io.Serializable import java.io.Serializable
class CollectionInfo : Serializable { class CollectionInfo : Serializable {

View File

@ -1,8 +1,8 @@
package com.etesync.syncadapter.model package com.etesync.syncadapter.model
import com.etesync.syncadapter.GsonHelper import com.etesync.syncadapter.GsonHelper
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.JournalEntryManager import com.etesync.journalmanager.JournalEntryManager
import java.io.Serializable import java.io.Serializable

View File

@ -20,8 +20,8 @@ import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager import com.etesync.journalmanager.JournalEntryManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry import com.etesync.syncadapter.model.SyncEntry

View File

@ -20,8 +20,8 @@ import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.HttpClient import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager import com.etesync.journalmanager.JournalEntryManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry import com.etesync.syncadapter.model.SyncEntry

View File

@ -23,9 +23,9 @@ import androidx.core.util.Pair
import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity
@ -51,7 +51,7 @@ class CachedJournalFetcher {
for (journal in journalsManager.list()) { for (journal in journalsManager.list()) {
val crypto: Crypto.CryptoManager val crypto: Crypto.CryptoManager
if (journal.key != null) { if (journal.key != null) {
crypto = Crypto.CryptoManager(journal.version, settings.keyPair!!, journal.key) crypto = Crypto.CryptoManager(journal.version, settings.keyPair!!, journal.key!!)
} else { } else {
crypto = Crypto.CryptoManager(journal.version, settings.password(), journal.uid!!) crypto = Crypto.CryptoManager(journal.version, settings.password(), journal.uid!!)
} }

View File

@ -18,9 +18,9 @@ import at.bitfire.ical4android.InvalidCalendarException
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalEntryManager import com.etesync.journalmanager.JournalEntryManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.* import com.etesync.syncadapter.model.*
import com.etesync.syncadapter.model.SyncEntry.Actions.ADD import com.etesync.syncadapter.model.SyncEntry.Actions.ADD

View File

@ -14,7 +14,7 @@ import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.ui.AccountSettingsActivity import com.etesync.syncadapter.ui.AccountSettingsActivity
import com.etesync.syncadapter.ui.DebugInfoActivity import com.etesync.syncadapter.ui.DebugInfoActivity

View File

@ -16,7 +16,7 @@ import at.bitfire.ical4android.Task
import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.JournalEntryManager import com.etesync.journalmanager.JournalEntryManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.SyncEntry import com.etesync.syncadapter.model.SyncEntry

View File

@ -28,9 +28,9 @@ import androidx.core.content.ContextCompat
import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalAuthenticator import com.etesync.journalmanager.JournalAuthenticator
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity

View File

@ -12,9 +12,9 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.journalmanager.UserInfoManager import com.etesync.journalmanager.UserInfoManager
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity
import okhttp3.HttpUrl import okhttp3.HttpUrl

View File

@ -18,9 +18,9 @@ import androidx.appcompat.app.AlertDialog
import com.etesync.syncadapter.AccountSettings import com.etesync.syncadapter.AccountSettings
import com.etesync.syncadapter.HttpClient import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.journalmanager.UserInfoManager import com.etesync.journalmanager.UserInfoManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.syncadapter.requestSync import com.etesync.syncadapter.syncadapter.requestSync
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@ -84,7 +84,7 @@ open class ChangeEncryptionPasswordActivity : BaseActivity() {
Logger.log.info("Finished deriving new key") Logger.log.info("Finished deriving new key")
val userInfoContent = userInfo.getContent(cryptoManager)!! val userInfoContent = userInfo.getContent(cryptoManager)!!
cryptoManager = Crypto.CryptoManager(userInfo.version.toInt(), new_key, "userInfo") cryptoManager = Crypto.CryptoManager(userInfo.version!!.toInt(), new_key, "userInfo")
userInfo.setContent(cryptoManager, userInfoContent) userInfo.setContent(cryptoManager, userInfoContent)
Logger.log.info("Fetching journal list") Logger.log.info("Fetching journal list")

View File

@ -12,7 +12,7 @@ import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.ListFragment import androidx.fragment.app.ListFragment
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel import com.etesync.syncadapter.model.JournalModel

View File

@ -21,9 +21,9 @@ import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader import androidx.loader.content.Loader
import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.model.JournalModel import com.etesync.syncadapter.model.JournalModel

View File

@ -28,7 +28,7 @@ import androidx.core.content.ContextCompat
import at.bitfire.vcard4android.ContactsStorageException import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.Constants.KEY_ACCOUNT import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.journalmanager.Exceptions.HttpException import com.etesync.journalmanager.Exceptions.HttpException
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.EntryEntity import com.etesync.syncadapter.model.EntryEntity
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity

View File

@ -20,9 +20,9 @@ import androidx.loader.app.LoaderManager
import androidx.loader.content.AsyncTaskLoader import androidx.loader.content.AsyncTaskLoader
import androidx.loader.content.Loader import androidx.loader.content.Loader
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity
import okhttp3.HttpUrl import okhttp3.HttpUrl

View File

@ -10,13 +10,12 @@ package com.etesync.syncadapter.ui
import android.accounts.Account import android.accounts.Account
import android.app.Dialog import android.app.Dialog
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.etesync.syncadapter.Constants import com.etesync.syncadapter.Constants
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Exceptions.HttpException import com.etesync.journalmanager.Exceptions.HttpException
import java.io.IOException import java.io.IOException
class ExceptionInfoFragment : DialogFragment() { class ExceptionInfoFragment : DialogFragment() {

View File

@ -8,7 +8,7 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.JournalManager import com.etesync.journalmanager.JournalManager
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import okhttp3.HttpUrl import okhttp3.HttpUrl

View File

@ -9,9 +9,9 @@ package com.etesync.syncadapter.ui.setup
import android.content.Context import android.content.Context
import com.etesync.syncadapter.HttpClient import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.JournalAuthenticator import com.etesync.journalmanager.JournalAuthenticator
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import okhttp3.HttpUrl import okhttp3.HttpUrl

View File

@ -22,9 +22,9 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider
import com.etesync.syncadapter.* import com.etesync.syncadapter.*
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.Exceptions import com.etesync.journalmanager.Exceptions
import com.etesync.syncadapter.journalmanager.UserInfoManager import com.etesync.journalmanager.UserInfoManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.model.JournalEntity import com.etesync.syncadapter.model.JournalEntity

View File

@ -13,9 +13,9 @@ import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.HttpClient import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.InvalidAccountException import com.etesync.syncadapter.InvalidAccountException
import com.etesync.syncadapter.R import com.etesync.syncadapter.R
import com.etesync.syncadapter.journalmanager.Constants import com.etesync.journalmanager.Constants
import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.journalmanager.Crypto
import com.etesync.syncadapter.journalmanager.UserInfoManager import com.etesync.journalmanager.UserInfoManager
import com.etesync.syncadapter.log.Logger import com.etesync.syncadapter.log.Logger
import okhttp3.HttpUrl import okhttp3.HttpUrl

View File

@ -1,55 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.HttpClient
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import org.junit.After
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import java.io.IOException
class AuthenticatorTest {
private var httpClient: OkHttpClient? = null
private var remote: HttpUrl? = null
@Before
@Throws(IOException::class)
fun setUp() {
httpClient = HttpClient.Builder().build().okHttpClient
remote = HttpUrl.parse("http://localhost:8000") // FIXME: hardcode for now, should make configureable
}
@After
@Throws(IOException::class)
fun tearDown() {
}
@Test
@Throws(IOException::class, Exceptions.HttpException::class)
fun testAuthToken() {
val journalAuthenticator = JournalAuthenticator(httpClient!!, remote!!)
val authToken = journalAuthenticator.getAuthToken(Helpers.USER, Helpers.PASSWORD)
assertNotEquals(authToken!!.length.toLong(), 0)
val httpClient2 = HttpClient.Builder(null, null, authToken).build().okHttpClient
val journalAuthenticator2 = JournalAuthenticator(httpClient2!!, remote!!)
journalAuthenticator2.invalidateAuthToken(authToken)
}
@Test(expected = Exceptions.UnauthorizedException::class)
@Throws(Exceptions.IntegrityException::class, Exceptions.VersionTooNewException::class, IOException::class, Exceptions.HttpException::class)
fun testNoUser() {
val journalAuthenticator = JournalAuthenticator(httpClient!!, remote!!)
val authToken = journalAuthenticator.getAuthToken(Helpers.USER, "BadPassword")
assertNotEquals(authToken!!.length.toLong(), 0)
}
}

View File

@ -1,90 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.utils.Base64
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.spongycastle.util.encoders.Hex
import java.io.IOException
class EncryptionTest {
@Before
@Throws(IOException::class)
fun setUp() {
}
@After
@Throws(IOException::class)
fun tearDown() {
}
@Test
fun testDerivePassword() {
val key = Crypto.deriveKey(Helpers.USER, Helpers.PASSWORD)
assertEquals(key, Helpers.keyBase64)
}
@Test
@Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
fun testCryptoV1() {
val cryptoManager = Crypto.CryptoManager(1, Helpers.keyBase64, "TestSaltShouldBeJournalId")
val clearText = "This Is Some Test Cleartext."
val cipher = cryptoManager.encrypt(clearText.toByteArray())
assertEquals(clearText, String(cryptoManager.decrypt(cipher!!)!!))
val expected = "Lz+HUFzh1HdjxuGdQrBwBG1IzHT0ug6mO8fwePSbXtc="
assertEquals(expected, Base64.encodeToString(cryptoManager.hmac("Some test data".toByteArray()), Base64.NO_WRAP))
}
@Test
@Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
fun testCryptoV2() {
val cryptoManager = Crypto.CryptoManager(2, Helpers.keyBase64, "TestSaltShouldBeJournalId")
val clearText = "This Is Some Test Cleartext."
val cipher = cryptoManager.encrypt(clearText.toByteArray())
assertEquals(clearText, String(cryptoManager.decrypt(cipher!!)!!))
val expected = "XQ/A0gentOaE98R9wzf3zEIAHj4OH1GF8J4C6JiJupo="
assertEquals(expected, Base64.encodeToString(cryptoManager.hmac("Some test data".toByteArray()), Base64.NO_WRAP))
}
@Test(expected = Exceptions.VersionTooNewException::class)
@Throws(Exceptions.IntegrityException::class, Exceptions.VersionTooNewException::class)
fun testCryptoVersionTooNew() {
Crypto.CryptoManager(120, Helpers.keyBase64, "TestSaltShouldBeJournalId")
}
@Test(expected = Exceptions.IntegrityException::class)
@Throws(Exceptions.IntegrityException::class, Exceptions.VersionTooNewException::class)
fun testCryptoVersionOutOfRange() {
Crypto.CryptoManager(999, Helpers.keyBase64, "TestSaltShouldBeJournalId")
}
@Test
@Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
fun testAsymCrypto() {
val keyPair = Crypto.generateKeyPair()
val cryptoManager = Crypto.AsymmetricCryptoManager(keyPair!!)
val clearText = "This Is Some Test Cleartext.".toByteArray()
val cipher = cryptoManager.encrypt(keyPair.publicKey, clearText)
val 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".toByteArray())).toLowerCase())
}
}

View File

@ -1,8 +0,0 @@
package com.etesync.syncadapter.journalmanager
internal object Helpers {
val USER = "test@localhost"
val USER2 = "test2@localhost"
val PASSWORD = "SomePassword"
val keyBase64 = "Gpn6j6WJ/9JJbVkWhmEfZjlqSps5rwEOzjUOO0rqufvb4vtT4UfRgx0uMivuGwjF7/8Y1z1glIASX7Oz/4l2jucgf+lAzg2oTZFodWkXRZCDmFa7c9a8/04xIs7koFmUH34Rl9XXW6V2/GDVigQhQU8uWnrGo795tupoNQMbtB8RgMX5GyuxR55FvcybHpYBbwrDIsKvXcBxWFEscdNU8zyeq3yjvDo/W/y24dApW3mnNo7vswoL2rpkZj3dqw=="
}

View File

@ -1,265 +0,0 @@
/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.journalmanager
import com.etesync.syncadapter.App
import com.etesync.syncadapter.HttpClient
import com.etesync.syncadapter.model.CollectionInfo
import okhttp3.*
import okio.BufferedSink
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.io.IOException
import java.util.*
class ServiceTest {
private var httpClient: OkHttpClient? = null
private var remote: HttpUrl? = null
private var authToken: String? = null
@Before
@Throws(Exception::class)
fun setUp() {
httpClient = HttpClient.Builder().build().okHttpClient
remote = HttpUrl.parse("http://localhost:8000") // FIXME: hardcode for now, should make configureable
val journalAuthenticator = JournalAuthenticator(httpClient!!, remote!!)
authToken = journalAuthenticator.getAuthToken(Helpers.USER, Helpers.PASSWORD)
httpClient = HttpClient.Builder(null, null, authToken!!).build().okHttpClient
/* Reset */
val request = Request.Builder()
.post(object : RequestBody() {
override fun contentType(): MediaType? {
return null
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
}
})
.url(remote!!.newBuilder().addEncodedPathSegments("reset/").build())
.build()
val response = httpClient!!.newCall(request).execute()
if (!response.isSuccessful) {
throw Exception("Failed resetting")
}
}
@After
@Throws(IOException::class)
fun tearDown() {
}
@Test
@Throws(IOException::class, Exceptions.HttpException::class, Exceptions.GenericCryptoException::class, Exceptions.IntegrityException::class)
fun testSyncSimple() {
var caught: Exception?
val journalManager = JournalManager(httpClient!!, remote!!)
val info = CollectionInfo.defaultForServiceType(CollectionInfo.Type.ADDRESS_BOOK)
info.uid = JournalManager.Journal.genUid()
info.displayName = "Test"
val crypto = Crypto.CryptoManager(info.version, Helpers.keyBase64, info.uid!!)
var journal = JournalManager.Journal(crypto, info.toJson(), info.uid!!)
journalManager.create(journal)
// Try pushing the same journal (uid clash)
try {
caught = null
journalManager.create(journal)
} catch (e: Exceptions.HttpException) {
caught = e
}
assertNotNull(caught)
var journals: List<JournalManager.Journal> = journalManager.list()
assertEquals(journals.size.toLong(), 1)
var info2 = CollectionInfo.fromJson(journals[0].getContent(crypto))
assertEquals(info2.displayName, info.displayName)
// Update journal
info.displayName = "Test 2"
journal = JournalManager.Journal(crypto, info.toJson(), info.uid!!)
journalManager.update(journal)
journals = journalManager.list()
assertEquals(journals.size.toLong(), 1)
info2 = CollectionInfo.fromJson(journals[0].getContent(crypto))
assertEquals(info2.displayName, info.displayName)
// Delete journal
journalManager.delete(journal)
journals = journalManager.list()
assertEquals(journals.size.toLong(), 0)
// Bad HMAC
info.uid = JournalManager.Journal.genUid()
journal = JournalManager.Journal(crypto, info.toJson(), info.uid!!)
info.displayName = "Test 3"
//// We assume this doesn't update the hmac.
journal.setContent(crypto, info.toJson())
journalManager.create(journal)
try {
caught = null
for (journal1 in journalManager.list()) {
val crypto1 = Crypto.CryptoManager(info.version, Helpers.keyBase64, journal1.uid!!)
journal1.verify(crypto1)
}
} catch (e: Exceptions.IntegrityException) {
caught = e
}
assertNotNull(caught)
}
@Test
@Throws(IOException::class, Exceptions.HttpException::class, Exceptions.GenericCryptoException::class, Exceptions.IntegrityException::class)
fun testSyncEntry() {
var caught: Exception?
val journalManager = JournalManager(httpClient!!, remote!!)
val info = CollectionInfo.defaultForServiceType(CollectionInfo.Type.ADDRESS_BOOK)
info.uid = JournalManager.Journal.genUid()
info.displayName = "Test"
val crypto = Crypto.CryptoManager(info.version, Helpers.keyBase64, info.uid!!)
val journal = JournalManager.Journal(crypto, info.toJson(), info.uid!!)
journalManager.create(journal)
val journalEntryManager = JournalEntryManager(httpClient!!, remote!!, info.uid!!)
var previousEntry: JournalEntryManager.Entry? = null
val entry = JournalEntryManager.Entry()
entry.update(crypto, "Content", previousEntry)
var entries: MutableList<JournalEntryManager.Entry> = LinkedList()
var retEntries: List<JournalEntryManager.Entry>
entries.add(entry)
journalEntryManager.create(entries, null)
previousEntry = entry
entries.clear()
var entry2 = JournalEntryManager.Entry()
entry2.update(crypto, "Content", previousEntry)
entries.add(entry2)
// Pushing a correct entries without the last parameter
try {
caught = null
journalEntryManager.create(entries, null)
} catch (e: Exceptions.HttpException) {
caught = e
}
assertNotNull(caught)
// Adding a second entry
journalEntryManager.create(entries, previousEntry.uid)
previousEntry = entry2
entries.clear()
entries.add(entry)
entries.add(entry2)
// Check last works:
retEntries = journalEntryManager.list(crypto, entry.uid, 0)
assertEquals(retEntries.size.toLong(), 1)
retEntries = journalEntryManager.list(crypto, entry2.uid, 0)
assertEquals(retEntries.size.toLong(), 0)
// Corrupt the journal and verify we catch it
entries.clear()
entry2 = JournalEntryManager.Entry()
entry2.update(crypto, "Content", null)
entries.add(entry2)
journalEntryManager.create(entries, previousEntry.uid)
try {
caught = null
journalEntryManager.list(crypto, null, 0)
} catch (e: Exceptions.IntegrityException) {
caught = e
}
assertNotNull(caught)
}
@Test
@Throws(IOException::class, Exceptions.HttpException::class, Exceptions.GenericCryptoException::class, Exceptions.IntegrityException::class)
fun testUserInfo() {
val cryptoManager = Crypto.CryptoManager(Constants.CURRENT_VERSION, Helpers.keyBase64, "userInfo")
var userInfo: UserInfoManager.UserInfo?
var userInfo2: UserInfoManager.UserInfo?
val manager = UserInfoManager(httpClient!!, remote!!)
// Get when there's nothing
userInfo = manager.fetch(Helpers.USER)
assertNull(userInfo)
// Create
userInfo = UserInfoManager.UserInfo.generate(cryptoManager, Helpers.USER)
manager.create(userInfo)
// Get
userInfo2 = manager.fetch(Helpers.USER)
assertNotNull(userInfo2)
assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2!!.getContent(cryptoManager))
// Update
userInfo.setContent(cryptoManager, "test".toByteArray())
manager.update(userInfo)
userInfo2 = manager.fetch(Helpers.USER)
assertNotNull(userInfo2)
assertArrayEquals(userInfo.getContent(cryptoManager), userInfo2!!.getContent(cryptoManager))
// Delete
manager.delete(userInfo)
userInfo = manager.fetch(Helpers.USER)
assertNull(userInfo)
}
@Test
@Throws(IOException::class, Exceptions.HttpException::class, Exceptions.GenericCryptoException::class, Exceptions.IntegrityException::class)
fun testJournalMember() {
val journalManager = JournalManager(httpClient!!, remote!!)
val info = CollectionInfo.defaultForServiceType(CollectionInfo.Type.ADDRESS_BOOK)
info.uid = JournalManager.Journal.genUid()
info.displayName = "Test"
val crypto = Crypto.CryptoManager(info.version, Helpers.keyBase64, info.uid!!)
val journal = JournalManager.Journal(crypto, info.toJson(), info.uid!!)
journalManager.create(journal)
assertEquals(journalManager.listMembers(journal).size.toLong(), 0)
// Test inviting ourselves
val member = JournalManager.Member(Helpers.USER, "test".toByteArray())
journalManager.addMember(journal, member)
// We shouldn't show in the list
assertEquals(journalManager.listMembers(journal).size.toLong(), 0)
// Though we should have a key in the journal
assertNotNull(journalManager.list().first().key)
val member2 = JournalManager.Member(Helpers.USER2, "test".toByteArray())
journalManager.addMember(journal, member2)
assertEquals(journalManager.listMembers(journal).size.toLong(), 1)
// Uninviting user
journalManager.deleteMember(journal, member2)
assertEquals(journalManager.listMembers(journal).size.toLong(), 0)
}
}