應用程式安全性最佳做法

提升應用程式安全性有助於維護使用者信任及裝置完整性。

本頁說明幾種會對應用程式安全性帶來顯著正面影響的最佳做法。

強制啟用安全的通訊功能

對自家應用程式與其他應用程式/網站之間傳輸的資料採取保護措施後,您就能改善應用程式的穩定性,並確保自己傳送及接收的資料安全無虞。

保護應用程式之間的通訊

如要更安全地在應用程式之間通訊,請將隱含意圖搭配應用程式選擇工具、簽章式權限和非匯出內容供應器使用。

顯示應用程式選擇工具

如果隱含意圖可在使用者的裝置上啟動至少兩個可能的應用程式,請明確顯示應用程式選擇工具。這項互動策略可讓使用者將機密資訊轉移至他們信任的應用程式。

Kotlin

val intent = Intent(Intent.ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> =
        packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL)

// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.
if (possibleActivitiesList.size > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with."

    val chooser = resources.getString(R.string.chooser_title).let { title ->
        Intent.createChooser(intent, title)
    }
    startActivity(chooser)
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

Java

Intent intent = new Intent(Intent.ACTION_SEND);
List<ResolveInfo> possibleActivitiesList = getPackageManager()
        .queryIntentActivities(intent, PackageManager.MATCH_ALL);

// Verify that an activity in at least two apps on the user's device
// can handle the intent. Otherwise, start the intent only if an app
// on the user's device can handle the intent.
if (possibleActivitiesList.size() > 1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo with."

    String title = getResources().getString(R.string.chooser_title);
    Intent chooser = Intent.createChooser(intent, title);
    startActivity(chooser);
} else if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

相關資訊:

套用以簽名為基礎的權限

如要在您控管或擁有的兩個應用程式之間共用資料,請使用以簽名為基礎的權限。這些權限不需經過使用者確認,而是檢查存取資料的應用程式是否使用同一組簽名金鑰進行簽署。因此,這些權限可提供更簡便、更安全的使用者體驗。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <permission android:name="my_custom_permission_name"
                android:protectionLevel="signature" />

相關資訊:

禁止存取應用程式的內容供應者

除非您想將資料從應用程式傳送至不屬於您的應用程式,否則請明確禁止其他開發人員的應用程式存取應用程式的 ContentProvider 物件。如果您的應用程式是安裝在搭載 Android 4.1.1 (API 等級 16) 以下版本的裝置,當 <provider> 元件的 android:exported 屬性在這些 Android 版本預設為 true 時,此設定特別重要。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application ... >
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            ...
            android:exported="false">
            <!-- Place child elements of <provider> here. -->
        </provider>
        ...
    </application>
</manifest>

顯示機密資訊前必須先要求憑證

要求使用者提供憑證以便存取應用程式中的機密資訊或付費內容時,請要求對方提供 PIN 碼/密碼/圖案或生物特徵辨識憑證,例如臉部辨識或指紋辨識。

如要進一步瞭解如何要求生物特徵辨識憑證,請參閱生物特徵辨識驗證指南

套用網路安全性措施

下列各節將說明如何改善應用程式的網路安全性。

使用 TLS 流量

如果您的應用程式與網路伺服器通訊,具有由知名信任的憑證授權單位 (CA) 核發的憑證,請使用 HTTPS 要求,如下所示:

Kotlin

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}

Java

URL url = new URL("https://www.google.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
InputStream in = urlConnection.getInputStream();

新增網路安全性設定

如果您的應用程式使用新的或自訂 CA,您可以在設定檔中宣告網路的安全性設定。這項程序可讓您在不修改任何應用程式程式碼的情況下建立設定。

如要將網路安全性設定檔新增至應用程式,請按照下列步驟操作:

  1. 在應用程式的資訊清單中宣告設定:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
    
  3. 新增位於 res/xml/network_security_config.xml 的 XML 資源檔案。

    藉由停用明文,指定所有傳送到特定網域的流量必須使用 HTTPS:

    <network-security-config>
        <domain-config cleartextTrafficPermitted="false">
            <domain includeSubdomains="true">secure.example.com</domain>
            ...
        </domain-config>
    </network-security-config>
    

    在開發過程中,您可以使用 <debug-overrides> 元素明確允許使用者安裝的憑證。此元素會在偵錯和測試期間覆寫應用程式的安全性重要選項,而不會影響應用程式的版本設定。下列程式碼片段說明如何在您應用程式的網路安全性設定 XML 檔案中定義此元素:

    <network-security-config>
        <debug-overrides>
            <trust-anchors>
                <certificates src="user" />
            </trust-anchors>
        </debug-overrides>
    </network-security-config>
    

相關資訊: 網路安全性設定

自行建立信任的管理員

TLS 檢查工具不應接受所有憑證。假如符合下列任一條件,您可能需要設定信任管理員並處理所有發生的 TLS 警告:

  • 您正與具有新的 CA 或自訂 CA 簽署憑證的網路伺服器通訊。
  • 您目前使用的裝置不信任該 CA。
  • 您不能使用網路安全性設定

如要進一步瞭解如何完成這些步驟,請參閱有關處理不明憑證授權單位的討論。

相關資訊:

謹慎使用 WebView 物件

應用程式中的 WebView 物件不應讓使用者前往不在您控管的網站。請盡可能使用許可清單來限制應用程式 WebView 物件載入的內容。

此外,除非您完全控管及信任應用程式 WebView 物件中的內容,否則請勿啟用 JavaScript 介面支援

使用 HTML 訊息管道

如果應用程式於搭載 Android 6.0 (API 級別 23) 以上版本的裝置中必須使用 JavaScript 介面支援,請使用 HTML 訊息管道,而不要在網站與應用程式之間進行通訊,如下列程式碼片段所示:

Kotlin

val myWebView: WebView = findViewById(R.id.webview)

// channel[0] and channel[1] represent the two ports.
// They are already entangled with each other and have been started.
val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel()

// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() {

    override fun onMessage(port: WebMessagePort, message: WebMessage) {
        Log.d(TAG, "On port $port, received this message: $message")
    }
})

// Send a message from channel[1] to channel[0].
channel[1].postMessage(WebMessage("My secure message"))

Java

WebView myWebView = (WebView) findViewById(R.id.webview);

// channel[0] and channel[1] represent the two ports.
// They are already entangled with each other and have been started.
WebMessagePort[] channel = myWebView.createWebMessageChannel();

// Create handler for channel[0] to receive messages.
channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
    @Override
    public void onMessage(WebMessagePort port, WebMessage message) {
         Log.d(TAG, "On port " + port + ", received this message: " + message);
    }
});

// Send a message from channel[1] to channel[0].
channel[1].postMessage(new WebMessage("My secure message"));

相關資訊:

提供適當的權限

僅要求應用程式正常運作所需的最低權限。如果應用程式不再需要這些權限,請盡可能放棄權限。

使用意圖延後權限

請勿在應用程式中新增權限,以便完成可在其他應用程式完成的動作。請改用意圖將要求延後到擁有必要權限的其他應用程式。

下列範例說明如何使用意圖將使用者導向聯絡人應用程式,而不要求 READ_CONTACTSWRITE_CONTACTS 權限:

Kotlin

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent(Intent.ACTION_INSERT).apply {
    type = ContactsContract.Contacts.CONTENT_TYPE
}.also { intent ->
    // Make sure that the user has a contacts app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

Java

// Delegates the responsibility of creating the contact to a contacts app,
// which has already been granted the appropriate WRITE_CONTACTS permission.
Intent insertContactIntent = new Intent(Intent.ACTION_INSERT);
insertContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE);

// Make sure that the user has a contacts app installed on their device.
if (insertContactIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(insertContactIntent);
}

此外,如果您的應用程式需要執行以檔案為基礎的 I/O (例如存取儲存空間或選擇檔案),則不需要特殊權限,因為系統會代表應用程式完成執行作業。更棒的是,當使用者選取特定 URI 的內容後,呼叫應用程式即會取得所選資源的權限。

相關資訊:

在不同應用程式之間安全地共用資料

請按照下列最佳做法,以更安全的方式與其他應用程式分享應用程式內容:

下列程式碼片段說明如何使用 URI 權限授權標記和內容供應器權限,在獨立的 PDF 檢視器應用程式中顯示應用程式的 PDF 檔案:

Kotlin

// Create an Intent to launch a PDF viewer for a file owned by this app.
Intent(Intent.ACTION_VIEW).apply {
    data = Uri.parse("content://com.example/personal-info.pdf")

    // This flag gives the started app read access to the file.
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}.also { intent ->
    // Make sure that the user has a PDF viewer app installed on their device.
    intent.resolveActivity(packageManager)?.run {
        startActivity(intent)
    }
}

Java

// Create an Intent to launch a PDF viewer for a file owned by this app.
Intent viewPdfIntent = new Intent(Intent.ACTION_VIEW);
viewPdfIntent.setData(Uri.parse("content://com.example/personal-info.pdf"));

// This flag gives the started app read access to the file.
viewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

// Make sure that the user has a PDF viewer app installed on their device.
if (viewPdfIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(viewPdfIntent);
}

注意: 從可寫入應用程式主目錄執行檔案的行為屬於 W^X 違規行為。因此,指定 Android 10 (API 級別 29) 及以上級別的不可信任應用程式只能在應用程式主目錄中的檔案上叫用 exec(),只能針對嵌入應用程式 APK 檔案中的二進位檔程式碼。此外,以 Android 10 以上版本為目標的應用程式,無法在記憶體中,從使用 dlopen() 開啟的檔案修改可執行程式碼。這包括任何帶有文字再定位的共用物件 (.so) 檔案。

相關資訊:android:grantUriPermissions

安全地儲存資料

您的應用程式可能要求存取敏感的使用者資訊,但使用者必須信任您妥善保護應用程式,才能授予應用程式存取其資料的權限。

將私人資料儲存在內部儲存空間

將所有私人使用者資料儲存在裝置的內部儲存空間,並對各個應用程式採用沙箱機制。應用程式不需要要求權限,即可查看這些檔案,其他應用程式也無法存取檔案。作為一項新增的安全性措施,當使用者解除安裝應用程式時,裝置會刪除該應用程式儲存在內部儲存空間中的所有檔案。

注意:如果您儲存的資料特別敏感或私密,請考慮使用 EncryptedFile 物件作業,其可透過安全性程式庫 (而非 File 物件) 取得。

下列程式碼片段說明如何將資料寫入儲存空間:

Kotlin

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

// Create a file with this name or replace an entire existing file
// that has the same name. Note that you cannot append to an existing file,
// and the filename cannot contain path separators.
val fileToWrite = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
    File(DIRECTORY, fileToWrite),
    applicationContext,
    mainKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

val fileContent = "MY SUPER-SECRET INFORMATION"
        .toByteArray(StandardCharsets.UTF_8)
encryptedFile.openFileOutput().apply {
    write(fileContent)
    flush()
    close()
}

Java

Context context = getApplicationContext();

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

// Create a file with this name or replace an entire existing file
// that has the same name. Note that you cannot append to an existing file,
// and the filename cannot contain path separators.
String fileToWrite = "my_sensitive_data.txt";
EncryptedFile encryptedFile = new EncryptedFile.Builder(
        new File(DIRECTORY, fileToWrite),
        context,
        mainKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();

byte[] fileContent = "MY SUPER-SECRET INFORMATION"
        .getBytes(StandardCharsets.UTF_8);
OutputStream outputStream = encryptedFile.openFileOutput();
outputStream.write(fileContent);
outputStream.flush();
outputStream.close();

下列程式碼片段顯示從儲存空間讀取資料的反向作業:

Kotlin

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)

val fileToRead = "my_sensitive_data.txt"
val encryptedFile = EncryptedFile.Builder(
    File(DIRECTORY, fileToRead),
    applicationContext,
    mainKeyAlias,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

val inputStream = encryptedFile.openFileInput()
val byteArrayOutputStream = ByteArrayOutputStream()
var nextByte: Int = inputStream.read()
while (nextByte != -1) {
    byteArrayOutputStream.write(nextByte)
    nextByte = inputStream.read()
}

val plaintext: ByteArray = byteArrayOutputStream.toByteArray()

Java

Context context = getApplicationContext();

// Although you can define your own key generation parameter specification, it's
// recommended that you use the value specified here.
KeyGenParameterSpec keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC;
String mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec);

String fileToRead = "my_sensitive_data.txt";
EncryptedFile encryptedFile = new EncryptedFile.Builder(
        new File(DIRECTORY, fileToRead),
        context,
        mainKeyAlias,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build();

InputStream inputStream = encryptedFile.openFileInput();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int nextByte = inputStream.read();
while (nextByte != -1) {
    byteArrayOutputStream.write(nextByte);
    nextByte = inputStream.read();
}

byte[] plaintext = byteArrayOutputStream.toByteArray();

相關資訊:

依據用途將資料儲存在外部儲存空間中

針對應用程式專有的大型非機密檔案,以及應用程式與其他應用程式共用的檔案,使用外部儲存空間。您使用的特定 API 取決於應用程式設計是存取應用程式特定檔案或存取共用檔案。

如果檔案不含私人或機密資訊,但僅在應用程式中提供值給使用者,請將檔案儲存在外部儲存空間上的應用程式特定目錄

如果應用程式需要存取或儲存能為其他應用程式提供價值的檔案,請根據您的用途採用下列任一 API:

查看儲存磁碟區的可用性

如果您的應用程式與卸除式外部儲存裝置互動,請注意,使用者可能會在應用程式嘗試存取時移除儲存裝置。加入邏輯,確認儲存裝置是否可用

檢查資料的有效性

如果您的應用程式使用來自外部儲存空間的資料,請確認資料內容未損毀或修改。加入邏輯,處理不再是穩定格式的檔案。

以下程式碼片段包含雜湊驗證器範例:

Kotlin

val hash = calculateHash(stream)
// Store "expectedHash" in a secure location.
if (hash == expectedHash) {
    // Work with the content.
}

// Calculating the hash code can take quite a bit of time, so it shouldn't
// be done on the main thread.
suspend fun calculateHash(stream: InputStream): String {
    return withContext(Dispatchers.IO) {
        val digest = MessageDigest.getInstance("SHA-512")
        val digestStream = DigestInputStream(stream, digest)
        while (digestStream.read() != -1) {
            // The DigestInputStream does the work; nothing for us to do.
        }
        digest.digest().joinToString(":") { "%02x".format(it) }
    }
}

Java

Executor threadPoolExecutor = Executors.newFixedThreadPool(4);
private interface HashCallback {
    void onHashCalculated(@Nullable String hash);
}

boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -> {
    if (Objects.equals(hash, expectedHash)) {
        // Work with the content.
    }
});

if (!hashRunning) {
    // There was an error setting up the hash function.
}

private boolean calculateHash(@NonNull InputStream stream,
                              @NonNull Executor executor,
                              @NonNull HashCallback hashCallback) {
    final MessageDigest digest;
    try {
        digest = MessageDigest.getInstance("SHA-512");
    } catch (NoSuchAlgorithmException nsa) {
        return false;
    }

    // Calculating the hash code can take quite a bit of time, so it shouldn't
    // be done on the main thread.
    executor.execute(() -> {
        String hash;
        try (DigestInputStream digestStream =
                new DigestInputStream(stream, digest)) {
            while (digestStream.read() != -1) {
                // The DigestInputStream does the work; nothing for us to do.
            }
            StringBuilder builder = new StringBuilder();
            for (byte aByte : digest.digest()) {
                builder.append(String.format("%02x", aByte)).append(':');
            }
            hash = builder.substring(0, builder.length() - 1);
        } catch (IOException e) {
            hash = null;
        }

        final String calculatedHash = hash;
        runOnUiThread(() -> hashCallback.onHashCalculated(calculatedHash));
    });
    return true;
}

僅將非機密資料儲存在快取檔案中

如要更快速地存取非機密的應用程式資料,請將資料儲存在裝置的快取中。如果快取超過 1 MB,請使用 getExternalCacheDir()。對於 1 MB 以下的快取,請使用 getCacheDir()。這兩種方法均提供 File 物件,其中包含應用程式的快取資料。

下列程式碼片段顯示如何快取應用程式最近下載的檔案:

Kotlin

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}

Java

File cacheDir = getCacheDir();
File fileToCache = new File(myDownloadedFileUri);
String fileToCacheName = fileToCache.getName();
File cacheFile = new File(cacheDir.getPath(), fileToCacheName);

注意:如果您使用 getExternalCacheDir() 將應用程式快取放入共用儲存空間,使用者在應用程式執行期間,可能會退出包含此儲存空間的媒體。請加入邏輯,妥善處理這個使用者行為造成的快取失敗。

注意:這些檔案沒有強制執行安全措施。因此,如果應用程式指定 Android 10 (API 級別 29) 以下版本,並且具備 WRITE_EXTERNAL_STORAGE 權限,就能存取此快取內容。

相關資訊: 資料和檔案儲存空間總覽

在私人模式下使用 SharedPreferences

使用 getSharedPreferences() 建立或存取應用程式的 SharedPreferences 物件時,請使用 MODE_PRIVATE。如此一來,只有您的應用程式可以存取共用偏好設定檔案中的資訊。

如要跨應用程式共用資料,請勿使用 SharedPreferences 物件。請改為按照這篇文章的步驟,在不同應用程式之間安全地共用資料。

Security 程式庫也提供 EncryptedSharedPreferences 類別,用來納入 SharedPreferences 類別並自動加密金鑰和值。

相關資訊:

讓服務和依附元件保持在最新狀態

大多數應用程式都會使用外部程式庫和裝置系統資訊來完成特殊工作。讓應用程式的依附元件保持在最新狀態,即可讓這些通訊點更加安全。

查看 Google Play 服務安全性提供者

注意:本節只適用於針對已安裝 Google Play 服務裝置的應用程式。

如果您的應用程式使用 Google Play 服務,請確認已安裝應用程式的裝置已更新。以非同步方式執行檢查,關閉 UI 執行緒。如果裝置並非最新版本,請觸發授權錯誤。

如要判斷應用程式安裝裝置上的 Google Play 服務是否為最新版本,請按照更新安全提供者以防範安全資料傳輸層 (SSL) 漏洞指南中的步驟操作。

相關資訊:

更新所有應用程式依附元件

部署應用程式之前,請確認所有程式庫、SDK 和其他依附元件皆為最新版本:

  • 如果是第一方依附元件 (例如 Android SDK),請使用 Android Studio 中的更新工具 (例如 SDK Manager)。
  • 如果是第三方依附元件,請查看應用程式使用的程式庫網站,並安裝所有可用的更新和安全性修補程式。

相關資訊: 新增建構依附元件

更多資訊

如要進一步瞭解如何提高應用程式的安全性,請參閱下列資源: