diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.java deleted file mode 100644 index b0cdf68c..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.GsonHelper; - -import org.apache.commons.codec.Charsets; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.util.logging.Level; - -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -abstract class BaseManager { - final static protected MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - - protected HttpUrl remote; - protected OkHttpClient client; - - Response newCall(Request request) throws Exceptions.HttpException { - Response response; - try { - response = client.newCall(request).execute(); - } catch (IOException e) { - App.log.log(Level.SEVERE, "Failed while connecting to server", e); - throw new Exceptions.ServiceUnavailableException("[" + e.getClass().getName() + "] " + e.getLocalizedMessage()); - } - - if (!response.isSuccessful()) { - ApiError apiError = GsonHelper.gson.fromJson(response.body().charStream(), ApiError.class); - - switch (response.code()) { - case HttpURLConnection.HTTP_UNAVAILABLE: - throw new Exceptions.ServiceUnavailableException(response, "Service unavailable"); - case HttpURLConnection.HTTP_UNAUTHORIZED: - throw new Exceptions.UnauthorizedException(response, "Unauthorized auth token"); - case HttpURLConnection.HTTP_FORBIDDEN: - if (apiError.code.equals("service_inactive")) { - throw new Exceptions.UserInactiveException(response, apiError.detail); - } - default: - // Fall through. We want to always throw when unsuccessful. - } - - throw new Exceptions.HttpException(response, apiError.detail); - } - - return response; - } - - static class ApiError { - String detail; - String code; - - ApiError() { - } - } - - static class Base { - private byte[] content; - private String uid; - - void setContent(final byte[] content) { - this.content = content; - } - - byte[] getContent() { - return this.content; - } - - void setUid(final String uid) { - this.uid = uid; - } - - public String getUid() { - return uid; - } - - public String getContent(Crypto.CryptoManager crypto) { - return new String(crypto.decrypt(content), Charsets.UTF_8); - } - - void setContent(Crypto.CryptoManager crypto, String content) { - this.content = crypto.encrypt(content.getBytes(Charsets.UTF_8)); - } - - byte[] calculateHmac(Crypto.CryptoManager crypto, String uuid) { - ByteArrayOutputStream hashContent = new ByteArrayOutputStream(); - - try { - if (uuid != null) { - hashContent.write(uuid.getBytes(Charsets.UTF_8)); - } - - hashContent.write(content); - } catch (IOException e) { - // Can never happen, but just in case, return a bad hmac - return "DEADBEEFDEADBEEFDEADBEEFDEADBEEF".getBytes(); - } - - return crypto.hmac(hashContent.toByteArray()); - } - - protected Base() { - } - - Base(Crypto.CryptoManager crypto, String content, String uid) { - setContent(crypto, content); - setUid(uid); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "<" + uid + ">"; - } - - String toJson() { - return GsonHelper.gson.toJson(this, getClass()); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.kt new file mode 100644 index 00000000..78540a3c --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/BaseManager.kt @@ -0,0 +1,97 @@ +package com.etesync.syncadapter.journalmanager + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.GsonHelper +import okhttp3.* +import org.apache.commons.codec.Charsets +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.net.HttpURLConnection +import java.util.logging.Level + +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 { + response = client!!.newCall(request).execute() + } catch (e: IOException) { + App.log.log(Level.SEVERE, "Failed while connecting to server", e) + throw Exceptions.ServiceUnavailableException("[" + e.javaClass.name + "] " + e.localizedMessage) + } + + if (!response.isSuccessful) { + val apiError = GsonHelper.gson.fromJson(response.body()!!.charStream(), ApiError::class.java) + + when (response.code()) { + HttpURLConnection.HTTP_UNAVAILABLE -> throw Exceptions.ServiceUnavailableException(response, "Service unavailable") + HttpURLConnection.HTTP_UNAUTHORIZED -> throw Exceptions.UnauthorizedException(response, "Unauthorized auth token") + HttpURLConnection.HTTP_FORBIDDEN -> if (apiError.code == "service_inactive") { + throw Exceptions.UserInactiveException(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!!)!!, Charsets.UTF_8) + } + + fun setContent(crypto: Crypto.CryptoManager, content: String) { + this.content = crypto.encrypt(content.toByteArray(Charsets.UTF_8)) + } + + fun calculateHmac(crypto: Crypto.CryptoManager, uuid: String?): ByteArray { + val hashContent = ByteArrayOutputStream() + + try { + if (uuid != null) { + hashContent.write(uuid.toByteArray(Charsets.UTF_8)) + } + + 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") + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java deleted file mode 100644 index 6e8d3d51..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -public class Constants { - public final static int CURRENT_VERSION = 2; -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.kt new file mode 100644 index 00000000..7222c3ad --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Constants.kt @@ -0,0 +1,8 @@ +package com.etesync.syncadapter.journalmanager + +class Constants { + companion object { + @JvmField + val CURRENT_VERSION = 2 + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java deleted file mode 100644 index 7e24e91d..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.java +++ /dev/null @@ -1,294 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import android.support.annotation.NonNull; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.journalmanager.util.ByteUtil; -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; -import org.spongycastle.crypto.paddings.BlockCipherPadding; -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.Arrays; -import java.util.Locale; - -public class Crypto { - public static String deriveKey(String salt, String password) { - final int keySize = 190; - - 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(), 3072, 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; - } - - public static class AsymmetricKeyPair implements Serializable { - private final byte[] privateKey; - private final byte[] publicKey; - - public byte[] getPrivateKey() { - return privateKey; - } - - public byte[] getPublicKey() { - return publicKey; - } - - public AsymmetricKeyPair(final byte[] privateKey, final byte[] publicKey) { - this.privateKey = privateKey; - this.publicKey = 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 String getPrettyKeyFingerprint(byte[] pubkey) { - byte[] fingerprint = Crypto.AsymmetricCryptoManager.getKeyFingerprint(pubkey); - String spacing = " "; - String 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(); - } - - private static String getEncodedChunk(byte[] hash, int offset) { - long chunk = ByteUtil.byteArray4ToLong(hash, offset) % 100000; - return String.format(Locale.getDefault(), "%05d", chunk); - } - } - - public static class CryptoManager { - final static int HMAC_SIZE = 256 / 8; // hmac256 in bytes - - private SecureRandom _random = null; - private final byte version; - private byte[] cipherKey; - private byte[] hmacKey; - private byte[] derivedKey; - - public byte getVersion() { - return version; - } - - 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); - 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 { - if (version > Byte.MAX_VALUE) { - throw new Exceptions.IntegrityException("Version is out of range."); - } else if (version > Constants.CURRENT_VERSION) { - throw new Exceptions.VersionTooNewException("Version to new: " + String.valueOf(version)); - } else if (version == 1) { - derivedKey = Base64.decode(keyBase64, Base64.NO_WRAP); - } else { - derivedKey = hmac256(salt.getBytes(Charsets.UTF_8), Base64.decode(keyBase64, Base64.NO_WRAP)); - } - - this.version = (byte) version; - setDerivedKey(derivedKey); - } - - private static final int blockSize = 16; // AES's block size in bytes - - private BufferedBlockCipher getCipher(byte[] iv, boolean encrypt) { - KeyParameter key = new KeyParameter(cipherKey); - CipherParameters params = new ParametersWithIV(key, iv); - - BlockCipherPadding padding = new PKCS7Padding(); - BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( - new CBCBlockCipher(new AESEngine()), padding); - cipher.reset(); - cipher.init(encrypt, params); - - return cipher; - } - - byte[] decrypt(byte[] _data) { - byte[] iv = Arrays.copyOfRange(_data, 0, blockSize); - byte[] data = Arrays.copyOfRange(_data, blockSize, _data.length); - - BufferedBlockCipher cipher = getCipher(iv, false); - - byte[] buf = new byte[cipher.getOutputSize(data.length)]; - int len = cipher.processBytes(data, 0, data.length, buf, 0); - try { - len += cipher.doFinal(buf, len); - } catch (InvalidCipherTextException e) { - e.printStackTrace(); - App.log.severe("Invalid ciphertext: " + Base64.encodeToString(_data, Base64.NO_WRAP)); - return null; - } - - // remove padding - byte[] out = new byte[len]; - System.arraycopy(buf, 0, out, 0, len); - - return out; - } - - byte[] encrypt(byte[] data) { - byte[] iv = new byte[blockSize]; - getRandom().nextBytes(iv); - - BufferedBlockCipher cipher = getCipher(iv, true); - - byte[] buf = new byte[cipher.getOutputSize(data.length) + blockSize]; - System.arraycopy(iv, 0, buf, 0, iv.length); - int len = iv.length + cipher.processBytes(data, 0, data.length, buf, iv.length); - try { - cipher.doFinal(buf, len); - } catch (InvalidCipherTextException e) { - App.log.severe("Invalid ciphertext: " + Base64.encodeToString(data, Base64.NO_WRAP)); - e.printStackTrace(); - return null; - } - - return buf; - } - - byte[] hmac(byte[] data) { - if (version == 1) { - return hmac256(hmacKey, data); - } else { - // Starting from version 2 we hmac the version too. - return hmac256(hmacKey, ArrayUtils.add(data, version)); - } - } - - private SecureRandom getRandom() { - if (_random == null) { - _random = new SecureRandom(); - } - return _random; - } - - private static byte[] hmac256(byte[] keyByte, byte[] data) { - HMac hmac = new HMac(new SHA256Digest()); - KeyParameter key = new KeyParameter(keyByte); - byte[] ret = new byte[hmac.getMacSize()]; - hmac.init(key); - hmac.update(data, 0, data.length); - 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) { - return toHex(sha256(base.getBytes(Charsets.UTF_8))); - } - - 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 ret; - } - - static String toHex(byte[] bytes) { - return Hex.toHexString(bytes).toLowerCase(); - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.kt new file mode 100644 index 00000000..30efe560 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Crypto.kt @@ -0,0 +1,263 @@ +package com.etesync.syncadapter.journalmanager + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.journalmanager.util.ByteUtil +import com.etesync.syncadapter.utils.Base64 +import org.apache.commons.codec.Charsets +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(Charsets.UTF_8), salt.toByteArray(Charsets.UTF_8), 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() + App.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() + App.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(Charsets.UTF_8), derivedKey) + hmacKey = hmac256("hmac".toByteArray(Charsets.UTF_8), 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(Charsets.UTF_8), 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 + } + + internal 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() + App.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 + } + + internal 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) { + App.log.severe("Invalid ciphertext: " + Base64.encodeToString(data, Base64.NO_WRAP)) + e.printStackTrace() + return null + } + + return buf + } + + internal 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(Charsets.UTF_8))) + } + + 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() + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.java deleted file mode 100644 index dec806c9..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import java.io.IOException; -import java.io.Serializable; -import java.net.HttpURLConnection; -import java.security.GeneralSecurityException; - -import at.bitfire.cert4android.Constants; -import okhttp3.Headers; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.Buffer; - -public class Exceptions { - public static class UnauthorizedException extends HttpException { - public UnauthorizedException(Response response, String message) { - super(response, message); - } - } - - public static class UserInactiveException extends HttpException { - public UserInactiveException(Response response, String message) { - super(response, message); - } - } - - public static class ServiceUnavailableException extends HttpException { - public long retryAfter; - - public ServiceUnavailableException(String message) { - super(message); - this.retryAfter = 0; - } - - public ServiceUnavailableException(Response response, String message) { - super(response, message); - this.retryAfter = Long.valueOf(response.header("Retry-After", "0")); - } - } - - public static class IntegrityException extends GeneralSecurityException { - public IntegrityException(String message) { - super(message); - } - } - - - public static class GenericCryptoException extends Exception { - public GenericCryptoException(String message) { - super(message); - } - } - - public static class VersionTooNewException extends GenericCryptoException { - public VersionTooNewException(String message) { - super(message); - } - } - - public static class HttpException extends Exception implements Serializable { - final int status; - final String message; - - public final String request, response; - - public HttpException(String message) { - super(message); - this.message = message; - - this.status = -1; - this.request = this.response = null; - } - - public HttpException(int status, String message) { - super(status + " " + message); - this.status = status; - this.message = message; - - request = response = null; - } - - public HttpException(Response response) { - this(response, null); - } - - public HttpException(Response response, String custom_message) { - super(response.code() + " " + response.message()); - - status = response.code(); - message = (custom_message != null) ? 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 - Request request = response.request(); - StringBuilder formatted = new StringBuilder(); - formatted.append(request.method()).append(" ").append(request.url().encodedPath()).append("\n"); - Headers headers = request.headers(); - for (String name : headers.names()) { - for (String value : headers.values(name)) { - /* Redact authorization token. */ - if (name.equals("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"); - Buffer buffer = new Buffer(); - request.body().writeTo(buffer); - while (!buffer.exhausted()) - appendByte(formatted, buffer.readByte()); - } catch (IOException e) { - Constants.log.warning("Couldn't read request body"); - } - this.request = formatted.toString(); - - // format response - formatted = new StringBuilder(); - formatted.append(response.protocol()).append(" ").append(response.code()).append(" ").append(message).append("\n"); - headers = response.headers(); - for (String name : headers.names()) - for (String value : headers.values(name)) - formatted.append(name).append(": ").append(value).append("\n"); - if (response.body() != null) { - ResponseBody body = response.body(); - try { - formatted.append("\n"); - for (byte b : body.bytes()) - appendByte(formatted, b); - } catch (IOException e) { - Constants.log.warning("Couldn't read response body"); - } - body.close(); - } - this.response = formatted.toString(); - } - - @Override - public String getMessage() { - return message; - } - - private static void appendByte(StringBuilder formatted, byte b) { - if (b == '\r') - formatted.append("[CR]"); - else if (b == '\n') - formatted.append("[LF]\n"); - else if (b >= 0x20 && b <= 0x7E) // printable ASCII - formatted.append((char) b); - else - formatted.append("[" + Integer.toHexString((int) b & 0xff) + "]"); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.kt new file mode 100644 index 00000000..8a8faa7d --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/Exceptions.kt @@ -0,0 +1,129 @@ +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 UnauthorizedException(response: Response, message: String?) : HttpException(response, message) + + class UserInactiveException(response: Response, message: String?) : HttpException(response, message) + + class ServiceUnavailableException : HttpException { + 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 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) + "]") + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.java deleted file mode 100644 index b93818b4..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import java.io.IOException; -import java.net.HttpURLConnection; - -import com.etesync.syncadapter.GsonHelper; -import okhttp3.FormBody; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -public class JournalAuthenticator { - private HttpUrl remote; - private OkHttpClient client; - - public JournalAuthenticator(OkHttpClient client, HttpUrl remote) { - this.client = client; - this.remote = remote.newBuilder() - .addPathSegments("api-token-auth") - .addPathSegment("") - .build(); - } - - private class AuthResponse { - private String token; - - private AuthResponse() { - } - } - - public String getAuthToken(String username, String password) throws Exceptions.HttpException, IOException { - FormBody.Builder formBuilder = new FormBody.Builder() - .add("username", username) - .add("password", password); - - Request request = new Request.Builder() - .post(formBuilder.build()) - .url(remote) - .build(); - - Response response = client.newCall(request).execute(); - if (response.isSuccessful()) { - return GsonHelper.gson.fromJson(response.body().charStream(), AuthResponse.class).token; - } else if (response.code() == HttpURLConnection.HTTP_BAD_REQUEST) { - throw new Exceptions.UnauthorizedException(response, "Username or password incorrect"); - } else { - throw new Exceptions.HttpException(response); - } - } -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.kt new file mode 100644 index 00000000..10d0f416 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalAuthenticator.kt @@ -0,0 +1,45 @@ +package com.etesync.syncadapter.journalmanager + +import com.etesync.syncadapter.GsonHelper +import okhttp3.FormBody +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException +import java.net.HttpURLConnection + +class JournalAuthenticator(private val client: OkHttpClient, remote: HttpUrl) { + private val remote: HttpUrl + + init { + this.remote = remote.newBuilder() + .addPathSegments("api-token-auth") + .addPathSegment("") + .build() + } + + private inner class AuthResponse private constructor() { + val token: String? = null + } + + @Throws(Exceptions.HttpException::class, IOException::class) + fun getAuthToken(username: String, password: String): String? { + 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) + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.java deleted file mode 100644 index ac26c95d..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import com.etesync.syncadapter.App; -import com.etesync.syncadapter.GsonHelper; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.List; - -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; - -public class JournalEntryManager extends BaseManager { - final String uid; - final static private Type entryType = new TypeToken>() { - }.getType(); - - public String getUid() { - return uid; - } - - public JournalEntryManager(OkHttpClient httpClient, HttpUrl remote, String journal) { - this.uid = journal; - this.remote = remote.newBuilder() - .addPathSegments("api/v1/journals") - .addPathSegments(journal) - .addPathSegment("entries") - .addPathSegment("") - .build(); - App.log.info("Created for: " + this.remote.toString()); - - this.client = httpClient; - } - - public List list(Crypto.CryptoManager crypto, String last, int limit) throws Exceptions.HttpException, Exceptions.IntegrityException { - Entry previousEntry = null; - HttpUrl.Builder urlBuilder = this.remote.newBuilder(); - if (last != null) { - urlBuilder.addQueryParameter("last", last); - previousEntry = Entry.getFakeWithUid(last); - } - - if (limit > 0) { - urlBuilder.addQueryParameter("limit", String.valueOf(limit)); - } - - HttpUrl remote = urlBuilder.build(); - - Request request = new Request.Builder() - .get() - .url(remote) - .build(); - - Response response = newCall(request); - ResponseBody body = response.body(); - List ret = GsonHelper.gson.fromJson(body.charStream(), entryType); - - for (Entry entry : ret) { - entry.verify(crypto, previousEntry); - previousEntry = entry; - } - - return ret; - } - - public void create(List entries, String last) throws Exceptions.HttpException { - HttpUrl.Builder urlBuilder = this.remote.newBuilder(); - if (last != null) { - urlBuilder.addQueryParameter("last", last); - } - - HttpUrl remote = urlBuilder.build(); - - RequestBody body = RequestBody.create(JSON, GsonHelper.gson.toJson(entries, entryType)); - - Request request = new Request.Builder() - .post(body) - .url(remote) - .build(); - - newCall(request); - } - - public static class Entry extends Base { - public Entry() { - super(); - } - - public void update(Crypto.CryptoManager crypto, String content, Entry previous) { - setContent(crypto, content); - setUid(calculateHmac(crypto, previous)); - } - - void verify(Crypto.CryptoManager crypto, Entry previous) throws Exceptions.IntegrityException { - String correctHash = calculateHmac(crypto, previous); - if (!getUid().equals(correctHash)) { - throw new Exceptions.IntegrityException("Bad HMAC. " + getUid() + " != " + correctHash); - } - } - - public static Entry getFakeWithUid(String uid) { - Entry ret = new Entry(); - ret.setUid(uid); - return ret; - } - - private String calculateHmac(Crypto.CryptoManager crypto, Entry previous) { - String uuid = null; - if (previous != null) { - uuid = previous.getUid(); - } - - return Crypto.toHex(calculateHmac(crypto, uuid)); - } - } - -} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.kt new file mode 100644 index 00000000..64182f45 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalEntryManager.kt @@ -0,0 +1,116 @@ +package com.etesync.syncadapter.journalmanager + +import com.etesync.syncadapter.App +import com.etesync.syncadapter.GsonHelper +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() + App.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 { + 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>(body!!.charStream(), entryType) + + for (entry in ret) { + entry.verify(crypto, previousEntry) + previousEntry = entry + } + + return ret + } + + @Throws(Exceptions.HttpException::class) + fun create(entries: List, 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>() { + + }.type + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java deleted file mode 100644 index fa18e1b8..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.java +++ /dev/null @@ -1,235 +0,0 @@ -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.lang.reflect.Type; -import java.util.List; -import java.util.UUID; - -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.sha256; -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) { - this.remote = remote.newBuilder() - .addPathSegments("api/v1/journals") - .addPathSegment("") - .build(); - App.log.info("Created for: " + this.remote.toString()); - - this.client = httpClient; - } - - public List list() throws Exceptions.HttpException { - Request request = new Request.Builder() - .get() - .url(remote) - .build(); - - Response response = newCall(request); - ResponseBody body = response.body(); - List ret = GsonHelper.gson.fromJson(body.charStream(), journalType); - - for (Journal journal : ret) { - journal.processFromJson(); - } - - return ret; - } - - public void delete(Journal journal) throws Exceptions.HttpException { - HttpUrl remote = this.remote.resolve(journal.getUid() + "/"); - Request request = new Request.Builder() - .delete() - .url(remote) - .build(); - - newCall(request); - } - - public void create(Journal journal) throws Exceptions.HttpException { - RequestBody body = RequestBody.create(JSON, journal.toJson()); - - Request request = new Request.Builder() - .post(body) - .url(remote) - .build(); - - newCall(request); - } - - public void update(Journal journal) throws Exceptions.HttpException { - HttpUrl remote = this.remote.resolve(journal.getUid() + "/"); - RequestBody body = RequestBody.create(JSON, journal.toJson()); - - Request request = new Request.Builder() - .put(body) - .url(remote) - .build(); - - newCall(request); - } - - private HttpUrl getMemberRemote(Journal journal, String user) { - HttpUrl.Builder bulider = this.remote.newBuilder(); - bulider.addPathSegment(journal.getUid()) - .addPathSegment("members"); - if (user != null) { - bulider.addPathSegment(user); - } - bulider.addPathSegment(""); - return bulider.build(); - } - - public List listMembers(Journal journal) throws Exceptions.HttpException, Exceptions.IntegrityException, Exceptions.GenericCryptoException { - Request request = new Request.Builder() - .get() - .url(getMemberRemote(journal, null)) - .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, member.getUser())) - .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, null)) - .build(); - - newCall(request); - } - - public static class Journal extends Base { - private String owner; - private byte[] key; - private int version = -1; - private boolean readOnly = false; - - public String getOwner() { - return this.owner; - } - - public byte[] getKey() { - return this.key; - } - - public int getVersion() { - return this.version; - } - - public boolean isReadOnly() { - return this.readOnly; - } - - private transient byte[] hmac = null; - - @SuppressWarnings("unused") - private Journal() { - 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); - version = crypto.getVersion(); - } - - private void processFromJson() { - hmac = Arrays.copyOfRange(getContent(), 0, HMAC_SIZE); - setContent(Arrays.copyOfRange(getContent(), HMAC_SIZE, getContent().length)); - } - - public void verify(Crypto.CryptoManager crypto) throws Exceptions.IntegrityException { - if (hmac == null) { - throw new Exceptions.IntegrityException("HMAC is null!"); - } - - byte[] correctHash = calculateHmac(crypto); - if (!Arrays.areEqual(hmac, correctHash)) { - throw new Exceptions.IntegrityException("Bad HMAC. " + toHex(hmac) + " != " + toHex(correctHash)); - } - } - - byte[] calculateHmac(Crypto.CryptoManager crypto) { - return super.calculateHmac(crypto, getUid()); - } - - public static String genUid() { - return sha256(UUID.randomUUID().toString()); - } - - @Override - String toJson() { - byte[] rawContent = getContent(); - setContent(Arrays.concatenate(hmac, rawContent)); - String ret = super.toJson(); - setContent(rawContent); - return ret; - } - } - - public static class Member { - private String user; - private byte[] key; - - public String getUser() { - return user; - } - - public byte[] getKey() { - return 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/main/java/com/etesync/syncadapter/journalmanager/JournalManager.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.kt new file mode 100644 index 00000000..445d41da --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/JournalManager.kt @@ -0,0 +1,217 @@ +package com.etesync.syncadapter.journalmanager + +import com.etesync.syncadapter.App +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.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() + App.log.info("Created for: " + this.remote!!.toString()) + + this.client = httpClient + } + + @Throws(Exceptions.HttpException::class) + fun list(): List { + val request = Request.Builder() + .get() + .url(remote!!) + .build() + + val response = newCall(request) + val body = response.body() + val ret = GsonHelper.gson.fromJson>(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 { + 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 isReadOnly = 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? + + private constructor() { + this.user = null + this.key = null + } + + constructor(user: String, encryptedKey: ByteArray) { + this.user = user + this.key = encryptedKey + } + + internal fun toJson(): String { + return GsonHelper.gson.toJson(this, javaClass) + } + } + + companion object { + private val journalType = object : TypeToken>() { + + }.type + private val memberType = object : TypeToken>() { + + }.type + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java deleted file mode 100644 index 01e49cff..00000000 --- a/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.etesync.syncadapter.journalmanager; - -import com.etesync.syncadapter.GsonHelper; - -import org.spongycastle.util.Arrays; - -import java.io.IOException; -import java.net.HttpURLConnection; - -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(String owner) throws Exceptions.HttpException { - 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.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 { - private transient String owner; - private byte version; - private byte[] pubkey; - private byte[] content; - - public void setOwner(final String owner) { - this.owner = owner; - } - - public String getOwner() { - return this.owner; - } - - public byte getVersion() { - return this.version; - } - - public byte[] getPubkey() { - return this.pubkey; - } - - 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); - } - - public 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/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.kt b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.kt new file mode 100644 index 00000000..4d6a65a0 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/journalmanager/UserInfoManager.kt @@ -0,0 +1,146 @@ +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) + operator fun get(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 + 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) + } + + internal 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) + } + } + } +} diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.java b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.java index 03859b2b..7e84cda7 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/AddressBooksSyncAdapterService.java @@ -95,7 +95,7 @@ public class AddressBooksSyncAdapterService extends SyncAdapterService { } } catch (Exceptions.ServiceUnavailableException e) { syncResult.stats.numIoExceptions++; - syncResult.delayUntil = (e.retryAfter > 0) ? e.retryAfter : Constants.DEFAULT_RETRY_DELAY; + syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY; } catch (Exception | OutOfMemoryError e) { if (e instanceof ContactsStorageException || e instanceof SQLiteException) { App.log.log(Level.SEVERE, "Couldn't prepare local address books", e); 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 852f83e4..14a53bf5 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/CalendarsSyncAdapterService.java @@ -82,7 +82,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService { } } catch (Exceptions.ServiceUnavailableException e) { syncResult.stats.numIoExceptions++; - syncResult.delayUntil = (e.retryAfter > 0) ? e.retryAfter : Constants.DEFAULT_RETRY_DELAY; + syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY; } catch (Exception | OutOfMemoryError e) { if (e instanceof CalendarStorageException || e instanceof SQLiteException) { App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e); 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 ac0e7713..af9bd527 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.java @@ -212,7 +212,7 @@ abstract public class SyncManager { syncResult.stats.numIoExceptions++; } catch (Exceptions.ServiceUnavailableException e) { syncResult.stats.numIoExceptions++; - syncResult.delayUntil = (e.retryAfter > 0) ? e.retryAfter : Constants.DEFAULT_RETRY_DELAY; + syncResult.delayUntil = (e.getRetryAfter() > 0) ? e.getRetryAfter() : Constants.DEFAULT_RETRY_DELAY; } catch (InterruptedException e) { // Restart sync if interrupted syncResult.fullSyncRequested = true; 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 47fb32e0..49db012c 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java +++ b/app/src/main/java/com/etesync/syncadapter/ui/DebugInfoActivity.java @@ -155,10 +155,10 @@ public class DebugInfoActivity extends BaseActivity implements LoaderManager.Loa if (throwable instanceof HttpException) { HttpException http = (HttpException)throwable; - if (http.request != null) - report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n"); - if (http.response != null) - report.append("HTTP RESPONSE:\n").append(http.response).append("\n"); + if (http.getRequest() != null) + report.append("\nHTTP REQUEST:\n").append(http.getRequest()).append("\n\n"); + if (http.getRequest() != null) + report.append("HTTP RESPONSE:\n").append(http.getRequest()).append("\n"); } if (throwable != null) 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 b0d65764..5ea6018f 100644 --- a/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java +++ b/app/src/test/java/com/etesync/syncadapter/journalmanager/EncryptionTest.java @@ -82,6 +82,6 @@ public class EncryptionTest { // Mostly for coverage. Make sure it's the expected sha256 value. assertEquals("ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", - Hex.toHexString(Crypto.AsymmetricCryptoManager.getKeyFingerprint("a".getBytes(Charsets.UTF_8))).toLowerCase()); + Hex.toHexString(Crypto.AsymmetricCryptoManager.Companion.getKeyFingerprint("a".getBytes(Charsets.UTF_8))).toLowerCase()); } }