mirror of
https://github.com/etesync/android
synced 2025-02-16 17:42:03 +00:00
Kotlin: migrate to kotlin.
This commit is contained in:
parent
4306bceeb6
commit
959bc4992b
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
package com.etesync.syncadapter.journalmanager;
|
|
||||||
|
|
||||||
public class Constants {
|
|
||||||
public final static int CURRENT_VERSION = 2;
|
|
||||||
}
|
|
@ -0,0 +1,8 @@
|
|||||||
|
package com.etesync.syncadapter.journalmanager
|
||||||
|
|
||||||
|
class Constants {
|
||||||
|
companion object {
|
||||||
|
@JvmField
|
||||||
|
val CURRENT_VERSION = 2
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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) + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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) + "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<List<Entry>>() {
|
|
||||||
}.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<Entry> 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<Entry> ret = GsonHelper.gson.fromJson(body.charStream(), entryType);
|
|
||||||
|
|
||||||
for (Entry entry : ret) {
|
|
||||||
entry.verify(crypto, previousEntry);
|
|
||||||
previousEntry = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void create(List<Entry> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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<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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<List<Journal>>() {
|
|
||||||
}.getType();
|
|
||||||
final static private Type memberType = new TypeToken<List<Member>>() {
|
|
||||||
}.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<Journal> list() throws Exceptions.HttpException {
|
|
||||||
Request request = new Request.Builder()
|
|
||||||
.get()
|
|
||||||
.url(remote)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
Response response = newCall(request);
|
|
||||||
ResponseBody body = response.body();
|
|
||||||
List<Journal> 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<Member> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<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 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<List<Journal>>() {
|
||||||
|
|
||||||
|
}.type
|
||||||
|
private val memberType = object : TypeToken<List<Member>>() {
|
||||||
|
|
||||||
|
}.type
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -95,7 +95,7 @@ public class AddressBooksSyncAdapterService extends SyncAdapterService {
|
|||||||
}
|
}
|
||||||
} catch (Exceptions.ServiceUnavailableException e) {
|
} catch (Exceptions.ServiceUnavailableException e) {
|
||||||
syncResult.stats.numIoExceptions++;
|
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) {
|
} catch (Exception | OutOfMemoryError e) {
|
||||||
if (e instanceof ContactsStorageException || e instanceof SQLiteException) {
|
if (e instanceof ContactsStorageException || e instanceof SQLiteException) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e);
|
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e);
|
||||||
|
@ -82,7 +82,7 @@ public class CalendarsSyncAdapterService extends SyncAdapterService {
|
|||||||
}
|
}
|
||||||
} catch (Exceptions.ServiceUnavailableException e) {
|
} catch (Exceptions.ServiceUnavailableException e) {
|
||||||
syncResult.stats.numIoExceptions++;
|
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) {
|
} catch (Exception | OutOfMemoryError e) {
|
||||||
if (e instanceof CalendarStorageException || e instanceof SQLiteException) {
|
if (e instanceof CalendarStorageException || e instanceof SQLiteException) {
|
||||||
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
|
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e);
|
||||||
|
@ -212,7 +212,7 @@ abstract public class SyncManager {
|
|||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
} catch (Exceptions.ServiceUnavailableException e) {
|
} catch (Exceptions.ServiceUnavailableException e) {
|
||||||
syncResult.stats.numIoExceptions++;
|
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) {
|
} catch (InterruptedException e) {
|
||||||
// Restart sync if interrupted
|
// Restart sync if interrupted
|
||||||
syncResult.fullSyncRequested = true;
|
syncResult.fullSyncRequested = true;
|
||||||
|
@ -155,10 +155,10 @@ public class DebugInfoActivity extends BaseActivity implements LoaderManager.Loa
|
|||||||
|
|
||||||
if (throwable instanceof HttpException) {
|
if (throwable instanceof HttpException) {
|
||||||
HttpException http = (HttpException)throwable;
|
HttpException http = (HttpException)throwable;
|
||||||
if (http.request != null)
|
if (http.getRequest() != null)
|
||||||
report.append("\nHTTP REQUEST:\n").append(http.request).append("\n\n");
|
report.append("\nHTTP REQUEST:\n").append(http.getRequest()).append("\n\n");
|
||||||
if (http.response != null)
|
if (http.getRequest() != null)
|
||||||
report.append("HTTP RESPONSE:\n").append(http.response).append("\n");
|
report.append("HTTP RESPONSE:\n").append(http.getRequest()).append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (throwable != null)
|
if (throwable != null)
|
||||||
|
@ -82,6 +82,6 @@ public class EncryptionTest {
|
|||||||
|
|
||||||
// Mostly for coverage. Make sure it's the expected sha256 value.
|
// Mostly for coverage. Make sure it's the expected sha256 value.
|
||||||
assertEquals("ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user