12/08/2018, 14:18

Chia sẻ data bảo mật và hiệu quả với Content Provider trong android

Trong 4 components của android gồm Activities, Services, BroadCast Reveiver và Content Provider, thì 3 components đầu tiên hầu như các developer thường xuyên làm và tiếp xúc, riêng Content Provider được xử dụng ít hơn hoặc đôi khi có sự nhầm lẫn giữa Content Provider và SQLiteDatabase. Trong bài ...

Trong 4 components của android gồm Activities, Services, BroadCast Reveiver và Content Provider, thì 3 components đầu tiên hầu như các developer thường xuyên làm và tiếp xúc, riêng Content Provider được xử dụng ít hơn hoặc đôi khi có sự nhầm lẫn giữa Content Provider và SQLiteDatabase. Trong bài viết này tôi muốn chia sẻ với các bạn sâu hơn 1 tý về Content Provider và cách chia sẻ dữ liệu 1 cách bảo mật và hiệu quả giữa các ứng dụng với nhau sử dụng Content Provider.

Phân biệt Content Provider

Vì interface của ContentResolver và SQLiteDatabase giống rất nhiều (query, insert, delete, update), nên nhiều developer nhầm tưởng rằng Content Provider gần giống như là SQLiteDatabase. Tuy nhiên, thực sự thì Content Provider chỉ cung cấp các interface để chia sẻ data giữa các ứng dụng với nhau. Do đó cần chú ý rằng, nó không can thiệp vào định dạng lưu dữ liệu. Để lưu dữ liệu trong Content Provider, SQLiteDatabase, hoặc XML file đều có thể được sử dụng.

Phân loại Content Provider

Dựa vào mục đích sử dụng của Content Provider, có thể chia làm 5 loại sau:

content_provider.png

  • Private Content Provider: Là provider không chia sẻ data với ứng dụng khác, là loại Content Provider an toàn nhất.
  • Public Content Provider: Là provider chia sẻ cho các ứng dụng khác cùng sử dụng
  • Partner Content Provider: Là provider được sử dụng bởi 1 vài ứng dụng cụ thể làm bởi “trusted partner company” (công ty đối tác)
  • In-house Content Provider: Là provider được sử dụng bởi các ứng dụng nội bộ.
  • Temporary permit Content Provider: Là provider gần như private provider, nhưng cho phép những ứng dụng cụ thể access tới 1 URI cụ thể.

Trong bài viết này tôi muốn chia sẻ về Partner Content Provider. Đây là Content Provider mà chỉ được sử dụng bởi các ứng dụng cụ thể. Hệ thống bao gồm các ứng dụng của Partner (đối tác) và In-house (nội bộ). Dùng để bảo vệ thông tin và chức năng mà được xử lý giữa những ứng dụng Partner và In-house.

Ba điều cần lưu ý đối với Partner Content Provider:

  • Kiểm tra certificate của ứng dụng yêu cầu dữ liệu (từ Content Provider) có nằm trong danh sách cung cấp hay không (whitelist).
  • Xử lý nhận dữ liệu cẩn thận và bảo mật, kể cả data từ 1 ứng dụng của Partner
  • Thông tin cấp cho ứng dụng Partner cần trả về.

Bắt đầu code nào !!!

Ứng dụng share data

Đầu tiên làAndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app1">
    <application
        ... >
        <activity .../>
        <provider
            android:name=".MyProvider"
            android:authorities="com.example.app1.MyProvider"
            android:exported="true"
            ... />
    </application>
</manifest>

Hãy cùng chú ý ở đây:

  • android:authorities 1 hoặc nhiều URI authorities được cung cấp bởi Content Provider.
  • android:exported="true" để chỉ ra rằng provider này có thể được sử dụng từ ứng dụng khác.

Tiếp theo là MyProvider.java

Trong class này lưu ý đến cách 3 điểm sau (comments):

  • (1) Kiểm tra certificate của ứng dụng yêu cầu dữ liệu (từ Content Provider) có nằm trong danh sách partner (whitelist) hay ko?
  • (2) Xử lý nhận dữ liệu cẩn thận và bảo mật, kể cả data từ 1 ứng dụng của Partner
  • (3) Dữ liệu cần được trả về cho partner nếu thỏa (1) và (2)
public static final String AUTHORITY = "com.example.app1.MyProvider";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.myapp.contenttype";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.myapp.contenttype";
// Expose the interface that the Content Provider provides.
public interface UserData {
    String PATH = "shareddata";
    Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);
}
// add other interfaces that your ContentProvider supported
// UriMatcher
private static final int NAME_CODE = 1;
private static final int NAME_ID_CODE = 2;
private static UriMatcher sUriMatcher;
static {
    sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    sUriMatcher.addURI(AUTHORITY, UserData.PATH, NAME_CODE);
    sUriMatcher.addURI(AUTHORITY, UserData.PATH + "/#", NAME_ID_CODE);
}
// #### (1) Verify if the certificate of a requesting application has been registered in the own white list or not
private static PkgCertWhitelists sWhitelists = null;
private static void buildWhitelists(Context context) {
    boolean isdebug = Utils.isDebuggable(context);
    sWhitelists = new PkgCertWhitelists();
    // Register certificate hash value of partner application com.example.app2. add(String partnerPackage, String sha256)
    sWhiteLists.add("com.example.app2", isdebug ?
                // Certificate hash value of "androiddebugkey" in the debug.keystore.
                "719EB599 878F88E1 FCAC5028 E85BFD42 62022E53 94B6D63D A0159470 59DA3A64" :
                // Certificate hash value of "partner key" in the keystore.
                "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A"
    );
        // Add other partner's info here
}

private static boolean checkPartner(Context context, String pkgName) {
    if (sWhiteLists == null) {
        buildWhiteLists(context);
    }
    if (context.getPackageName().equals(pkgName)) {
        // current app is in used, skip checking
        return true;
    }
    return sWhiteLists.test(context, pkgName);
}

/**
 * Get the package name of the calling application.
 *
 * param context: the context
 */
private String getCallingPackage(Context context) {
    String pkgName = null;
    ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    List<ActivityManager.RunningAppProcessInfo> procList = am.getRunningAppProcesses();
    int callingPid = Binder.getCallingPid();
    if (procList != null) {
        for (ActivityManager.RunningAppProcessInfo proc : procList) {
            if (proc.pid == callingPid) {
                pkgName = proc.pkgList[proc.pkgList.length - 1];
                break;
            }
        }
    }
    return pkgName;
}
@ Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    // Check calling app is partner or not.
    if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
        throw new SecurityException("Calling application is not a partner application.");
    }
    if (mDb == null) {
        mDb = new MyDatabaseHelper(getContext());
    }
    int uriType = sUriMatcher.match(uri);
    SQLiteDatabase sqlDB = mDb.getWritableDatabase();
    long id = 0;
    //  #### (2) Handle the received request data carefully and securely, even though the data comes from a partner application. trả về dữ liệu đúng với matcher.
    //  #### (3) Return data if (1) and (2) are correct
    switch (uriType) {
        case NAME_CODE:
            id = sqlDB.insert(MyDatabaseHelper.TABLE_NAME, null, values);
        break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }
    ...// Do your query code here
}
@ Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
    if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
        throw new SecurityException("Calling application is not a partner application.");
    }
    ... // Do your insert business
}
@ Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
    if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
        throw new SecurityException("Calling application is not a partner application.");
    }
    ... // Do your delete business
}
@ Override
public int update(@NonNull Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
    if (!checkPartner(getContext(), getCallingPackage(getContext()))) {
        throw new SecurityException("Calling application is not a partner application.");
    }
    ... // Do your update business
}

PkgCertWhitelists.java

Class này làm nhiệm vụ thêm và kiểm tra hash code của các ứng dụng partner

package com.example.app1;

import android.content.Context;
import java.util.HashMap;
import java.util.Map;

public class PkgCertWhiteLists {
    private Map<String, String> mWhiteLists = new HashMap<>();

    public boolean add(String pkgName, String sha256) {
        if (pkgName == null) return false;
        if (sha256 == null) return false;
        sha256 = sha256.replaceAll(" ", "");
        if (sha256.length() != 64) {
            return false;
        }
        // SHA-256 -> 32 bytes -> 64 chars
        sha256 = sha256.toUpperCase();
        if (sha256.replaceAll("[0-9A-F]+", "").length() != 0) {
            // found non hex char
            return false;
        }
        mWhiteLists.put(pkgName, sha256);
        return true;
    }

    public boolean test(Context ctx, String pkgName) {
        // Get the correct hash value which corresponds to pkgName.
        String correctHash = mWhiteLists.get(pkgName);
        // Compare the actual hash value of pkgName with the correct hash value.
        return PkgCert.test(ctx, pkgName, correctHash);
    }
}

PkgCert.java

Class này làm nhiệm vụ tính toán, hash và decode

package com.example.app1;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;

public class PkgCert {
    public static boolean test(Context ctx, String pkgname, String correctHash) {
        if (correctHash == null) {
            return false;
        }
        correctHash = correctHash.replaceAll(" ", "");
        return correctHash.equals(hash(ctx, pkgname));
    }

    public static String hash(Context ctx, String pkgName) {
        if (pkgName == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkgInfo = pm.getPackageInfo(pkgName, PackageManager.GET_SIGNATURES);
            if (pkgInfo.signatures.length != 1) {
                return null;
            }
            Signature sig = pkgInfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}

Ứng dụng Partner

Ở đây chúng ta cần

-Phía provider cung cấp đầy đủ thông tin về shared URI and inteface. Và cả Provider sha256

-Partner cần cung cấp sha256 (build config) để cung cấp cho bên Provider.

package com.example.app2;

import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;

public class PartnerActivity extends AppCompatActivity {
    // shared provider
    public static final String SHARED_AUTHORITY = "com.example.app1.MyProvider";

    // shared interface
    public interface UserData {
        String PATH = "shareddata";
        Uri CONTENT_URI = Uri.parse("content://" + SHARED_AUTHORITY + "/" + PATH);
    }

    private ListView mLvData;
    private PartnerAdapter mAdapter;
    private ArrayList<String> mVersions = new ArrayList<>();

    private static PkgCertWhiteLists sWhiteLists = null;

    private static void buildWhiteLists(Context context) {
        boolean isDebug = BuildConfig.DEBUG;
        sWhiteLists = new PkgCertWhiteLists();
        // Register certificate hash value of provider application com.example.app1
        sWhiteLists.add("com.example.app1", isDebug ?
                // Certificate hash value of "androiddebugkey" in the debug.keystore.
                "4A4DBC9A 526EA032 D3B2FD89 45D2E971 63A066B2 4481A751 116E07D1 98E7B148" :
                // Certificate hash value of "partner key" in the keystore.
                "1F039BB5 7861C27A 3916C778 8E78CE00 690B3974 3EB8259F E2627B8D 4C0EC35A");

        // Add other partner here via add(String pkgName, String sha256)
    }

    private static boolean checkPartner(Context context, String pkgName) {
        if (sWhiteLists == null) {
            buildWhiteLists(context);
        }
        if (context.getPackageName().equals(pkgName)) {
            // current app is in used
            return true;
        }
        return sWhiteLists.test(context, pkgName);
    }

    private String providerPkgname(Uri uri) {
        String pkgName = null;
        ProviderInfo pi = getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
        if (pi != null) {
            pkgName = pi.packageName;
        }
        return pkgName;
    }

    @ Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mLvData = (ListView) findViewById(R.id.listView);
        if (!checkPartner(this, providerPkgname(UserData.CONTENT_URI))) {
            Toast.makeText(this, "There is no public provider with this authority !",
                    Toast.LENGTH_LONG).show();
            return;
        }

        try {
            Cursor cursor =
                    getContentResolver().query(UserData.CONTENT_URI, null, null, null, null);
            if (cursor != null) {
                if (!cursor.moveToFirst()) {
                    Toast.makeText(this, "no data yet", Toast.LENGTH_SHORT).show();
                } else {
                    do {
                        String version = cursor.getString(cursor.getColumnIndex("version"));
                        mVersions.add(version);
                    } while (cursor.moveToNext());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        mAdapter = new PartnerAdapter(this, mVersions);
        mLvData.setAdapter(mAdapter);
    }
}

Kết Luận

Qua bài viết hi vọng bạn có thể tìm thấy thêm vài thông tin hữu ích về Content Provider và cách dùng Content Provider để share data 1 cách hiệu quả và bảo mật.

P/S: Bài tập cho các bạn là hãy thực hiện tương tự cho các interface insert, delete, và update.

See my example code:

  • Shared Provider app: https://github.com/huunam118/SharedApp
  • Partner app: https://github.com/huunam118/PartnerApp
0