Keep Sensitive Data Safe and Private in Android app

Việc lưu trữ dữ liệu của người dùng hay một số thông tin nhạy cảm của người dùng một cách an toàn và riêng tư là một trong các yếu tố chính trong việc xây dựng sự tin tưởng của người dùng đối với ứng dụng của bạn. Trong bài viết này sẽ chỉ ra một số rủi ro tiềm ẩn và thực hiện một số biện pháp phù hợp để có thể lưu trữ data, thông tin một cách an toàn.

Để có thể chỉ ra một số rủi ro tiềm ẩn và các biện pháp trong việc đảm bảo lưu trữ data, thông tin an toàn, ta sẽ thực hiện xây dựng một ứng dụng mẫu như sau:

  • Ứng dụng có chức năng chụp ảnh
  • Ảnh được chụp sẽ được lưu trữ một cách an toàn sao cho các ứng dụng khác không thể truy cập vào ảnh đã chụp
  • Share ảnh đã chụp với các ứng dụng khác
  • Import ảnh từ những vùng lưu trữ khác trên bộ nhớ vào ứng dụng

Từ ứng dụng mẫu này ta sẽ lần lượt chỉ ra các vấn đề về bảo mật và giải quyết chúng.

Sample app

Bạn có thể get source code của ứng dụng mẫu tại đây. Đây là một ứng dụng mẫu đã được xây dựng sẵn, khởi điểm với nhiều vấn đề liên quan đến bảo mật dữ liệu người dùng. Sau khi lấy source code về, chúng ta sẽ bắt đầy đi từ branch master, sau đó từng bước xử lý vấn đề và hoàn thiện ứng dụng.

Running the sample app

Features

Vì chúng ta sẽ bắt đầu ứng dụng mẫu từ branch master, nên chúng ta sẽ bắt đầu ứng dụng với 3 màn hình như sau:

  • Images screen: hiển thị tất cả ảnh được lưu trữ trong app
  • Image Viewer screen: hiển thị ảnh được chọn
  • Import screen: hiển thị ảnh được lưu trữ trên bộ nhớ

Security Issues and missing features

Chúng ta sẽ bắt đầu ứng dụng mẫu với một số vấn đề về bảo mật dữ liệu ảnh và không thực sự lưu trữ ảnh một cách an toàn:

  • Tất cả ảnh chụp từ ứng dụng mẫu được lưu trữ ở external storage và có thể truy cập bởi bất kỳ ứng dụng nào. Chúng ta sẽ fix điều này ngay trong bước đầu tiên.
  • Bị thiếu tính năng share. Sẽ được thêm vào trong bước tiếp theo.
  • Function import ảnh từ bộ nhớ được thực hiện bằng cách scan toàn bộ external storage. Điều này đã vi phạm nguyên tắc minimizing permissions. App của bạn không nên yêu cầu quyền quá mức, chẳng hạn như quyền truy cập vào tất cả external storage. Chúng ta sẽ khác khắc phục điều này bằng cách sử dụng system intent để pick file và không yêu cầu thêm permissions.

Storage locations

Hiện tại, ứng dụng mẫu đang lưu trữ tất cả các ảnh ở external storage, và đang sử dụng api: Environment.getExternalStorageDirectory()

Tất cả data được lưu trữ ở external storage thì đồng thời với việc có nghĩa là những data sẽ có thể được truy cập bởi bất kỳ ứng dụng nào được cấp quyền READ_EXTERNAL_STORAGE.

Đối với một ứng dụng chia sẻ ảnh an toàn thì đây không phải là phương pháp phù hợp!

Store data in internal storage

Mặc định mỗi ứng dụng có một thư mục lưu trữ nội bộ riêng và không thể truy cập được bởi bất kỳ ứng dụng nào khác. Không có bất kì một permissions đặc biệt nào cung cấp quyền truy cập thư mực này. Khi ứng dụng bị xóa thì thư mực này cũng được xóa theo.

Chính vì điều trên chúng ta sẽ thay đổi appm chuyển lưu trữ ảnh từ external storage sang internal storage.

Tất cả data lưu trữ trong ứng dụng mẫu hiện đều được handle bời LocalImagesRepository (xem package com.google.samples.dataprivacy.storage).

Trong hàm khởi tạo, ta thực hiện sử dụng function context.getFilesDir() - function này sẽ trả về đường dẫn của internal storage của app. Đồng thời xóa function tham chiếu đến external storage:

LocalImagesRepository.java

public LocalImagesRepository(Context context) {
        File internalStorage = context.getFilesDir();
        mStorage = new File(internalStorage, PATH);
        ...
}

Remove the WRITE_EXTERNAL_STORAGE permission

Vì ta đã chuyển đổi sang sử dụng internal storage nên sẽ không cần bất kỳ permissions nào, vì vậy hãy xóa permission WRITE_EXTERNAL_STORAGE đi thôi. Đây là cách tốt nhất để giảm thiểu các permissions cho ứng dụng của bạn. Khi ứng dụng của bạn yêu cầu các permission nhạy cảm (WRITE_EXTERNAL_STORAGE là một trong số chúng) thì thường người dùng sẽ ngay lập tức có nhiều thắc mắc và hoài nghi về ứng dụng của bạn, thập chí đối với những người mà chú trọng về những thông tin riêng tư thì họ có thể kiểm tra/scan app bạn để có thể an tâm sử dụng.

Đầu tiên, vào file AndroidManifest.xml và thực hiện remove entry cho WRITE_EXTERNAL_STORAGE permission:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Tiếp theo, ta thực hiện remove runtime request permission, vì bây giờ ứng dụng không cần xin quyền để access vào storage. Remove function mView.requestPermissions(), và call showImages() trực tiếp luôn.

ImagesPresenter.java

@Override
public void start() {
    // Load all images when the app is started.
    showImages();        
}

Sau khi thay đổi xong thì thực hiện build lại app và check lại hoạt động.

Use system intents

Về tính năng import ảnh từ bộ nhớ vào app thì để thực hiện được bạn cần yêu cầu quyền read external storage directory. Sau khi quyền này được cấp thì bạn có thể import ảnh vào app. Phương pháp này không thực sự tốt cho app vì nó không cung cấp cho người dùng lựa chọn và kiểm soát chính xác các file, thư mục được truy cập. Cấp quyền này tức là cấp quyền cho app có thể truy cập toàn bộ file, thư mục trên external storage.

Trong ứng dụng mẫu hiện tại, chúng ta đang thực hiện runtime permissions:

  • Request permission
  • Chú thích rõ ràng tại sao quyền này được yêu cầu

Tuy là đã làm cho các bước xin quyền rất rõ ràng và clear nhưng nó vẫn là một permission nguy hiểm nên được tránh.

Chính vì vậy, thay vì cấp quyền đọc cho app, chúng ta có thể sử dụng system intent. Ta sẽ thực hiện sử dụng intent để hiển thị thông báo cho người dùng tự thực hiện chọn file, khi người dùng chọn file xong thì intent trả về sẽ cung cấp cho app quyền đọc chỉ cho file được chọn đó. Bằng cách sử dụng phương pháp này, chúng ta có thể remove tất cả các permissions liên quan đến external storage vì chúng không còn cần thiết nữa, thêm một sự yên tâm được thiết lập cho user.

Add a File Picker Intent

Ta sẽ thực hiển sửa đổi phương thức importImage() chuyển sang sử dụng intent picker để open system file browser cho người dùng có thể tự chọn file.

ImagesActivity.java

   @Override
    public void importImage() {
        // Use an ACTION_GET_CONTENT intent to select a file using the system's
        // file browser.
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        // Filter files only to those that can be "opened" and directly accessed
        // as a stream.
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        // Only show images.
        intent.setType("image/*");

        startActivityForResult(intent, REQUEST_IMAGE_IMPORT);
    }

| Intent.ACTION_GET_CONTENT | Intent này sẽ open một file browser và cho phép user chọn một file. Nó sẽ trả về 1 Uri trỏ đến file đã được chọn | | Intent.CATEGORY_OPENABLE | Chỉ liệt kê các file có thể được mở. Có nghĩa là các file được truy cập dưới dạng stream và content có thể đọc được | | setType("image/*") | Chỉ liệt kê image file trong file browser |

Intent picker này cần phải được call bằng startActivityForResult(), sau đó chúng ta cần handle intent data được trả về trong onActivityResult().

Chúng ta sẽ verify request code REQUEST_IMAGE_IMPORT để xác định data intent được trả về:

ImagesActivity.java

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        ...
            
        } else if (requestCode == REQUEST_IMAGE_IMPORT
                && resultCode == Activity.RESULT_OK && data != null) {
            // Image import intent result
            // The ACTION_GET_CONTENT Intent returns a URI pointing to the file.
            // It does not return the file itself. Extract the URI 
            // from the Intent and process it.
            Uri uri = data.getData();
            importUriImage(uri);

        } else {
        ...
        }
    }

Khi một file được chọn bằng cách sử dụng ACTION_GET_CONTENT intent, ứng dụng gọi sẽ được tự động cấp quyền truy cập vào file được chọn trong suốt thời gian chạy của app. Ngoài ra cũng có các flag đặc biệt có thể setting quyền write và persistent access như FLAG_GRANT_PERSISTABLE_URI_PERMISSION, FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION. Những action này được sử dụng cho mục đích read/write đơn giản, và khi đó app sẽ nhận được một bản sao của data.

Ngoài ra, cần lưu ý action ACTION_OPEN_DOCUMENT - nó cho phép ứng dụng có thể truy cập lâu dài, liên tục vào data owned bằng document provider.

Bạn có thể tìm hiểu kỹ hơn tại Open Files using Storage Access Framework guide.

Tiếp theo, chúng ta cần viết một method để xử lý URI được trả về bằng intent và load data ảnh dưới dạng tham chiếu bitmap.

ImagesActivity.java

    /**
     * Uses the {@link MediaStore} content provider to load the {@link Uri}
     * as a Bitmap.
     * Next, notifies the presenter to import this image.
     *
     * @param uri Points to the image to create.
     */
    private void importUriImage(@NonNull Uri uri) {
        try {
            // Use the MediaStore to load the image.
            Bitmap image = MediaStore.Images.Media.getBitmap(getContentResolver(), uri);
            if (image != null) {
                // Tell the presenter to import this image.
                mPresenter.onImportImage(image);
            }
        } catch (IOException e) {
            e.printStackTrace();
            Log.e(TAG, "Error: " + e.getMessage() + "Could not open URI: "
                    + uri.toString());
        }
    }

Rồi, ta thử build lại app và sử dụng.

File Picker use cases

Trong ứng dụng mẫu này, chúng ta đã cải thiện function import ảnh giúp nó bảo mật hơn bằng các sử dụng system-provided intent.

Nếu ứng dụng của bạn chỉ yêu cầu người dùng chọn một file hoặc một item, thì bạn có thể đơn giản hóa ứng dụng bằng cách sử dụng system intent mà không cần phải sự dụng những dòng code phức tạp nào. Intent ACTION_GET_CONTENT là một cách rất rõ ràng, đơn giản và an toàn khi mở file. Người dùng chỉ cấp quyền cho ứng dụng của bạn đối với file được chọn, điều này mang lại sự tin tưởng, tự chủ trong việc chia sẻ dữ liệu của user.

Bạn có thể tham khảo thêm ở đây: recommended best practices around data privacy and security

Data Sharing with FileProvider

Trong bước này, chúng ta sẽ khiến cho ứng dụng mẫu có thể share ảnh với những ứng dụng khác một cách an toàn và bảo mật.

Chúng ta sẽ sử dụng FileProvider component từ v4 Android Support Library để generate một content URI, một tham chiếu bảo mật cho file được chia sẻ.

Một content URI sẽ xác định data trong một intent provider. Nó bao gồm một authority và một path.

Content provider có thể áp đặt các hạn chế truy cập và quyền để giới hạn các ứng dụng có thể truy cập vào data. Nếu một file được chia sẻ chỉ sử dụng một file identifier, như khi bạn lưu trữ nó ở external storage và tham chiếu trực tiếp, thì bản không thể sử dụng phương pháp này.

Add the v4 support library dependency

FileProvider là một phần của v4 Android Support library. Bạn có thể thêm nó bằng cách thêm entry trong app/build.gradle

Specify the FileProvider

Chỉ định FileProvider trong file AndroidManifest.xml. Ở bước này sẽ enable authority cho content URIs và setup configuration cho component. Configuration bao gồm một tham chiếu đến một file XML - đây là file chỉ định thư mục nào có thể được chia sẻ bằng FileProvider và các thư mục được hiển thị.

AndroidManifest.xml

<application ... >
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.google.samples.dataprivacy.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>
...
</application>

Bạn không cần phải public file provider bởi vì việc chia sẻ được bắt đầu từ trong app android:export="false", bạn nên cung cấp quyền truy cập tạm thời cho các file được chia sẻ android:grantUriPermissions="true".

XML flie là tham chiếu như android.support.FILE_PROVIDER_PATHS metadata, sẽ chỉ định đường dẫn lưu trữ và thư mực nào có thể được chia sẻ thông qua FileProvider. Chỉ những đường dẫn được setup trong file xml này mới có sẵn để chia sẻ.

Declare shareable directories

Ta thực hiện khởi tạo file XML với tên provider_paths.xml và để nó ở thư mục res/xml trong project. Sau đó add thêm config như sau:

provider_paths.xml

<paths>
    <!-- Share the "secureimages/" directory from internal files storage
         as the name "data" -->
    <files-path name="data" path="secureimages/"/>
</paths>

Với config này, provider sẽ có quyền truy cập để chia sẻ các file trong thư mục secureimages/ trong internal storage của app (Context.getFilesDir()) dưới tên data trong FileProvider. Điều này sẽ không tự động làm cho tất cả cái file trong thư mục này có thể truy cập bởi ứng dụng khác, mà nó chỉ cho phép FileProvider tạo ra cái Uri tương ứng với các file và làm cho chúng có thể chia sẻ được. Tiếp theo, bạn cần phải send một Intent hoặc một share Uri đồng thời cấp quyền truy cập cho ứng dụng được nhận.

Để biết thêm thông tin cụ thể về cách khai báo thư mục chia sẻ, bạn có thể tham khảo ở đây: Specifying Available Files

Create a share intent

FileProvider đã được set up xong, chúng ta cần tạo một share intent khi user thực hiện action share.

Ảnh được chia sẻ từ Image Viewer page. Ta thực hiện implement ImageSharer interface cho ImageViewerActivity để thực hiện tính năng share.

Implement phương thức shareImage(String) như sau:

  1. Sử dụng FileProvide để generate một shareable URI for file được chỉ định (contentUri). path parameter trong phương thức bao gồm đường dẫn tuyệt đối đến ảnh được lưu trữ trong app.
Uri contentUri = FileProvider.getUriForFile(this, 
        "com.google.samples.dataprivacy.FileProvider", new File(path));
  1. Khởi tạo một intent với action ACTION_SEND.
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
  1. Set contentUri làm data, và type là "image/png". Điều này cho phép ứng dụng nhận xác định được chính xác dữ liệu.
intent.setDataAndType(contentUri, "image/png");
  1. Cấp quyền cho ứng dụng nhận quyền read content URI bằng các sử dụng Intent.FLAG_GRANT_READ_URI_PERMISSION. Flag này sẽ thông báo cho FileProvider rằng file có sẵn chỉ cung cấp cho app nhận intent này. Nếu bạn không chỉ định flag này thì ứng dụng nhận sẽ chỉ nhận được intent này nhưng không thể open URI hoặc truy cập vào tham chiếu của file.
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Sau khi setup xong thì chỉ việc start Intent đã khởi tạo:

startActivity(intent);

Như vậy là ta đã hoàn thành xong implement phương thức shareImage(String).

ImageViewerActivity.java

    @Override
    public void shareImage(String path) {
        Toast.makeText(this, "Sharing: " + path, Toast.LENGTH_SHORT).show();
        Uri contentUri = FileProvider.getUriForFile(this, 
        "com.google.samples.dataprivacy.FileProvider", new File(path));

        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_SEND);
        intent.setDataAndType(contentUri, "image/png");
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivity(intent);
    }

URI permissions and file providers

Setting flag FLAG_GRANT_READ_URI_PERMISSION để cho ứng dụng nhận có quyền read access vào URI. Còn với flag FLAG_GRANT_WRITE_URI_PERMISSION, thì sẽ cung cấp quyền write. Cả 2 flag này đều chỉ cấp quyền tạm thời và chỉ áp dụng cho activity khi nó đang chạy.

Ngoài ra, bạn cũng có thể cấp quyền cho một package cụ thể bằng các sử dụng Context.grantUriPermission(package, Uri, mode_flags). Cách này cũng khá hữu ích khi ứng dụng của bạn không muốn sử dụng intent.

Bạn cũng có thể tạo custom permissions để thực thi quyền truy cập cho content providers. Trong trường hợp này, bạn khai báo permissions trong <provider> element trong file manifest, và bạn có thể áp dụng permissions chỉ định này cho toàn bộ provider hoặc chỉ cho một đường dẫn cụ thể. Lưu ý rằng các custom permission này là liên tục, không giống như các quyền tạm thời được nêu phía trên. Để có thể hiểu rõ hơn bạn có thể đọc thêm ở đây implementing content provider permissions guide.

Như vậy trên đây là những vấn cơ bản liên quan đến việc lưu trữ và chia sẻ dữ liệu người dùng một cách an toàn và riêng tư cùng với các phương pháp giải quyết. Mong nó có thể giúp bạn tạo những ứng dụng an toàn và tin tưởng với người dùng hơn