diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 985eced2..42178a0f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -234,6 +234,22 @@
+
+
+
+
+
+
+
+
+
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+import android.os.Binder;
+
+import com.etesync.syncadapter.App;
+import com.etesync.syncadapter.utils.Base64;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * Abstract service class for remote APIs that handle app registration and user input.
+ */
+public class ApiPermissionHelper {
+
+ private static final String FILE_API = "file_api_";
+
+ private final Context mContext;
+ private PackageManager mPackageManager;
+
+ public ApiPermissionHelper(Context context) {
+ mContext = context;
+ mPackageManager = context.getPackageManager();
+ }
+
+ public static class WrongPackageCertificateException extends Exception {
+ private static final long serialVersionUID = -8294642703122196028L;
+
+ public WrongPackageCertificateException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Returns true iff the caller is allowed, or false on any type of problem.
+ * This method should only be used in cases where error handling is dealt with separately.
+ */
+ public boolean isAllowedIgnoreErrors(String journalType) {
+ try {
+ return isCallerAllowed(journalType);
+ } catch (WrongPackageCertificateException e) {
+ return false;
+ }
+ }
+
+ private static byte[] getPackageCertificate(Context context, String packageName) throws NameNotFoundException {
+ @SuppressLint("PackageManagerGetSignatures") // we do check the byte array of *all* signatures
+ PackageInfo pkgInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ // NOTE: Silly Android API naming: Signatures are actually certificates
+ Signature[] certificates = pkgInfo.signatures;
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ for (Signature cert : certificates) {
+ try {
+ outputStream.write(cert.toByteArray());
+ } catch (IOException e) {
+ throw new RuntimeException("Should not happen! Writing ByteArrayOutputStream to concat certificates failed");
+ }
+ }
+
+ // Even if an apk has several certificates, these certificates should never change
+ // Google Play does not allow the introduction of new certificates into an existing apk
+ // Also see this attack: http://stackoverflow.com/a/10567852
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * Returns package name associated with the UID, which is assigned to the process that sent you the
+ * current transaction that is being processed :)
+ *
+ * @return package name
+ */
+ protected String getCurrentCallingPackage() {
+ String[] callingPackages = mPackageManager.getPackagesForUid(Binder.getCallingUid());
+
+ // NOTE: No support for sharedUserIds
+ // callingPackages contains more than one entry when sharedUserId has been used
+ // No plans to support sharedUserIds due to many bugs connected to them:
+ // http://java-hamster.blogspot.de/2010/05/androids-shareduserid.html
+ String currentPkg = callingPackages[0];
+ App.log.info("currentPkg: " + currentPkg);
+
+ return currentPkg;
+ }
+
+ /**
+ * Checks if process that binds to this service (i.e. the package name corresponding to the
+ * process) is in the list of allowed package names.
+ *
+ * @return true if process is allowed to use this service
+ * @throws WrongPackageCertificateException
+ */
+ public boolean isCallerAllowed(String journalType) throws WrongPackageCertificateException {
+ return isUidAllowed(Binder.getCallingUid(), journalType);
+ }
+
+ private boolean isUidAllowed(int uid, String journalType)
+ throws WrongPackageCertificateException {
+
+ String[] callingPackages = mPackageManager.getPackagesForUid(uid);
+
+ // is calling package allowed to use this service?
+ for (String currentPkg : callingPackages) {
+ if (isPackageAllowed(currentPkg, journalType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if packageName is a registered app for the API. Does not return true for own package!
+ *
+ * @throws WrongPackageCertificateException
+ */
+ public boolean isPackageAllowed(String packageName, String journalType) throws WrongPackageCertificateException {
+ byte[] storedPackageCert = getCertificate(mContext, packageName, journalType);
+
+ boolean isKnownPackage = storedPackageCert != null;
+ if (!isKnownPackage) {
+ App.log.warning("Package is NOT allowed! packageName: " + packageName + " for journal type " + journalType);
+ return false;
+ }
+ App.log.info("Package is allowed! packageName: " + packageName + " for journal type " + journalType);
+
+ byte[] currentPackageCert;
+ try {
+ currentPackageCert = getPackageCertificate(mContext, packageName);
+ } catch (NameNotFoundException e) {
+ throw new WrongPackageCertificateException(e.getMessage());
+ }
+
+ boolean packageCertMatchesStored = Arrays.equals(currentPackageCert, storedPackageCert);
+ if (packageCertMatchesStored) {
+ App.log.info("Package certificate matches expected.");
+ return true;
+ }
+
+ throw new WrongPackageCertificateException("PACKAGE NOT ALLOWED DUE TO CERTIFICATE MISMATCH!");
+ }
+
+ public static void addCertificate(Context context, String packageName, String journalType) {
+ SharedPreferences sharedPref = context.getSharedPreferences(FILE_API,
+ Context.MODE_PRIVATE);
+ try {
+ sharedPref.edit().putString(getEncodedName(packageName, journalType),
+ Base64.encodeToString(getPackageCertificate(context, packageName), Base64.DEFAULT)).apply();
+ App.log.info("Adding permission for package:" + packageName + " for journal type " + journalType);
+ } catch (NameNotFoundException aE) {
+ aE.printStackTrace();
+ }
+ }
+
+ private static byte[] getCertificate(Context context, String packageName, String journalType) {
+ SharedPreferences sharedPref = context.getSharedPreferences(FILE_API,
+ Context.MODE_PRIVATE);
+ String cert = sharedPref.getString(getEncodedName(packageName, journalType), null);
+ return cert == null ? null : Base64.decode(cert, Base64.DEFAULT);
+ }
+
+ private static String getEncodedName(String packageName, String journalType) {
+ return packageName + "." + journalType;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/etesync/syncadapter/remote/RemoteRegisterActivity.java b/app/src/main/java/com/etesync/syncadapter/remote/RemoteRegisterActivity.java
new file mode 100644
index 00000000..ee9d45dc
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/remote/RemoteRegisterActivity.java
@@ -0,0 +1,60 @@
+package com.etesync.syncadapter.remote;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.etesync.syncadapter.R;
+
+/**
+ * Created by tal on 13/04/17.
+ */
+
+public class RemoteRegisterActivity extends AppCompatActivity {
+
+ private static final String KEY_PACKAGE = "package_name";
+ private static final String KEY_JOURNAL_TYPE = "journal_type";
+
+ public static void startActivity(Context context, String packageName, String journalType) {
+ Intent intent = new Intent(context, RemoteRegisterActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(KEY_PACKAGE, packageName);
+ intent.putExtra(KEY_JOURNAL_TYPE, journalType);
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_remote_register);
+
+ setTitle(R.string.api_register_title);
+
+ final String packageName = getIntent().getStringExtra(KEY_PACKAGE);
+ final String journalType = getIntent().getStringExtra(KEY_JOURNAL_TYPE);
+
+ ((TextView) findViewById(R.id.api_register_text))
+ .setText(String.format(getString(R.string.api_register_text), packageName, journalType));
+
+ (findViewById(R.id.button_cancel)).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View aView) {
+ Toast.makeText(RemoteRegisterActivity.this, R.string.api_permission_not_granted, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+ });
+
+ (findViewById(R.id.button_allow)).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View aView) {
+ Toast.makeText(RemoteRegisterActivity.this, R.string.api_permission_granted, Toast.LENGTH_SHORT).show();
+ ApiPermissionHelper.addCertificate(RemoteRegisterActivity.this, packageName, journalType);
+ finish();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/com/etesync/syncadapter/remote/RemoteService.java b/app/src/main/java/com/etesync/syncadapter/remote/RemoteService.java
new file mode 100644
index 00000000..39e6b24c
--- /dev/null
+++ b/app/src/main/java/com/etesync/syncadapter/remote/RemoteService.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2013 – 2016 Ricki Hirner (bitfire web engineering).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Public License v3.0
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/gpl.html
+ */
+
+package com.etesync.syncadapter.remote;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.etesync.syncadapter.IEteSyncService;
+
+public class RemoteService extends Service {
+
+ private ApiPermissionHelper mApiPermissionHelper;
+
+ private final IEteSyncService.Stub mBinder = new IEteSyncService.Stub() {
+ @Override
+ public boolean hasPermission(String journalType) throws RemoteException {
+ if (journalType == null || journalType.isEmpty()) return false;
+ return mApiPermissionHelper.isAllowedIgnoreErrors(journalType);
+ }
+
+ @Override
+ public void requestPermission(String journalType) throws RemoteException {
+ if (journalType == null || journalType.isEmpty()) return;
+ if (mApiPermissionHelper.isAllowedIgnoreErrors(journalType)) return;
+
+ RemoteRegisterActivity.startActivity(RemoteService.this,
+ mApiPermissionHelper.getCurrentCallingPackage(), journalType);
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mApiPermissionHelper = new ApiPermissionHelper(this);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/app/src/main/res/layout/activity_remote_register.xml b/app/src/main/res/layout/activity_remote_register.xml
new file mode 100644
index 00000000..d7f96b1d
--- /dev/null
+++ b/app/src/main/res/layout/activity_remote_register.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b6be4fbc..e5b852d7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -327,6 +327,13 @@
From Account
Select Account
-
+
+ "Allow access to EteSync?"
+ "%s requests to access your account for %s. This will allow the app to modify your account"
+ "Allow access"
+ "Disallow access"
+ Permission not granted.
+ Permission granted
+
Organizer: