mirror of
https://github.com/etesync/android
synced 2024-11-25 17:38:13 +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=".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
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
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_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>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user