1
0
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:
Tal Hacohen 2017-04-13 16:18:07 +03:00
parent af04b2fa45
commit 7de2484c74
7 changed files with 380 additions and 1 deletions

View File

@ -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"

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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();
}
});
}
}

View File

@ -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;
}
}

View 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>

View File

@ -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>