mirror of
https://github.com/etesync/android
synced 2024-11-22 07:58:09 +00:00
Remote IPC - Add basic impelmantation
* Add service with AIDL file which can be connected to * Add UI for granting permission
This commit is contained in:
parent
af04b2fa45
commit
7de2484c74
@ -234,6 +234,22 @@
|
|||||||
|
|
||||||
<activity android:name=".ui.WebViewActivity" />
|
<activity android:name=".ui.WebViewActivity" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".remote.RemoteRegisterActivity"
|
||||||
|
android:theme="@style/AppTheme.Dialog.Alert"
|
||||||
|
android:launchMode="singleInstance"/>
|
||||||
|
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".remote.RemoteService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="ExportedService">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.etesync.syncadapter.RemoteService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
android:authorities="@string/authority_log_provider"
|
android:authorities="@string/authority_log_provider"
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
// IEteSyncService.aidl
|
||||||
|
package com.etesync.syncadapter;
|
||||||
|
|
||||||
|
// Declare any non-default types here with import statements
|
||||||
|
|
||||||
|
interface IEteSyncService {
|
||||||
|
boolean hasPermission(String journalType);
|
||||||
|
|
||||||
|
void requestPermission(String journalType);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,187 @@
|
|||||||
|
package com.etesync.syncadapter.remote;
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013-2015 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
44
app/src/main/res/layout/activity_remote_register.xml
Normal file
44
app/src/main/res/layout/activity_remote_register.xml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:paddingLeft="24dp"
|
||||||
|
android:paddingRight="24dp"
|
||||||
|
android:paddingTop="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/api_register_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="end"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/api_register_disallow"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_allow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/api_register_allow"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
@ -327,6 +327,13 @@
|
|||||||
<string name="import_button_local">From Account</string>
|
<string name="import_button_local">From Account</string>
|
||||||
<string name="import_select_account">Select Account</string>
|
<string name="import_select_account">Select Account</string>
|
||||||
|
|
||||||
<!-- Event (from Etar) -->
|
<!-- Remote register -->
|
||||||
|
<string name="api_register_title">"Allow access to EteSync?"</string>
|
||||||
|
<string name="api_register_text" formatted="false">"%s requests to access your account for %s. This will allow the app to modify your account"</string>
|
||||||
|
<string name="api_register_allow">"Allow access"</string>
|
||||||
|
<string name="api_register_disallow">"Disallow access"</string>
|
||||||
|
<string name="api_permission_not_granted">Permission not granted.</string>
|
||||||
|
<string name="api_permission_granted">Permission granted</string>
|
||||||
|
|
||||||
<string name="event_info_organizer">Organizer:</string>
|
<string name="event_info_organizer">Organizer:</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user