アプリのセキュリティを強化する

アプリをセキュアにすることで、ユーザーの信頼とデバイスの整合性を保つことができます。

このページでは、アプリのセキュリティに大きなプラスの影響を与えるおすすめの方法について説明します。

通信をセキュアにする

アプリと他のアプリ間またはアプリとウェブサイト間で交換されるデータに安全保護対策を実施すると、アプリの安定性が向上し、送受信するデータが保護されます。

アプリ間の通信を保護する

アプリ間でより安全に通信するには、アプリチューザ、署名ベースの権限、エクスポートされていないコンテンツ プロバイダで暗黙的インテントを使用します。

アプリチューザを表示する

暗黙的インテントがユーザーのデバイス上で少なくとも 2 つのアプリ候補を起動できる場合は、アプリチューザを明示的に表示します。このインタラクション戦略により、ユーザーが信頼するアプリに機密情報を送信することが可能になります。

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

関連情報:

署名ベースの権限を適用する

自分が管理または所有している 2 つのアプリ間でデータを共有する場合は、署名ベースの権限を使用します。署名ベースの権限ではユーザーの確認が不要です。代わりに、データにアクセスするアプリが同じ署名鍵で署名されているかどうかがチェックされます。したがって、署名ベースの権限を使用すると、効率的でセキュアなユーザー エクスペリエンスを実現できます。

<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)以下を実行しているデバイスにアプリをインストールできる場合、特に重要です。それらの Android バージョンでは、<provider> 要素の android:exported 属性がデフォルトで 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 またはカスタム CA を使用する場合は、構成ファイル内でネットワークのセキュリティ設定を宣言します。このプロセスにより、アプリコードを編集することなく、構成を作成できます。

ネットワーク セキュリティ構成ファイルをアプリに追加する手順は次のとおりです。

  1. アプリのマニフェスト内で構成を宣言します。
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
    
  3. XML リソース ファイル res/xml/network_security_config.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_CONTACTS 権限と WRITE_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 でコンテンツを選択すると、選択されたリソースに対する権限が呼び出し元アプリに付与されるという利点もあります。

関連情報:

アプリ間でセキュアにデータを共有する

アプリのコンテンツをセキュアな方法で他のアプリと共有するには、以下のおすすめの方法に従います。

  • 必要に応じて、読み取り専用権限または書き込み専用権限を適用します。
  • FLAG_GRANT_READ_URI_PERMISSION フラグと FLAG_GRANT_WRITE_URI_PERMISSION フラグを使用して、データに対する 1 回限りのアクセス権をクライアントに提供します。
  • データを共有するときは、file:// URI ではなく content:// URI を使用します。これは FileProvider のインスタンスによって行われます。

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

データを安全に保存する

アプリはユーザーの機密情報へのアクセスを必要とする場合がありますが、ユーザーはデータが適切に保護されると信頼できる場合に限り、機密データへのアクセスをアプリに許可します。

内部ストレージに個人データを保存する

ユーザーのすべての個人データは、アプリごとにサンドボックス化されているデバイスの内部ストレージ内に保存します。これらのファイルを表示する際、アプリが権限をリクエストする必要はなく、他のアプリはファイルにアクセスできません。追加のセキュリティ対策として、ユーザーがアプリをアンインストールしたとき、アプリが内部ストレージに保存したすべてのファイルはデバイスによって削除されます。

内部ストレージにデータを書き込む方法の例を次のコード スニペットに示します。

Kotlin

// Creates a file with this name, or replaces an existing file
// that has the same name. Note that the file name cannot contain
// path separators.
val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"
File(filesDir, FILE_NAME).bufferedWriter().use { writer ->
    writer.write(fileContents)
}

Java

// Creates a file with this name, or replaces an existing file
// that has the same name. Note that the file name cannot contain
// path separators.
final String FILE_NAME = "sensitive_info.txt";
String fileContents = "This is some top-secret information!";
try (BufferedWriter writer =
             new BufferedWriter(new FileWriter(new File(getFilesDir(), FILE_NAME)))) {
    writer.write(fileContents);
} catch (IOException e) {
    // Handle exception.
}

内部ストレージからデータを読み取る逆向きの処理を次のコード スニペットに示します。

Kotlin

val FILE_NAME = "sensitive_info.txt"
val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines ->
    lines.fold("") { working, line ->
        "$working\n$line"
    }
}

Java

final String FILE_NAME = "sensitive_info.txt";
StringBuffer stringBuffer = new StringBuffer();
try (BufferedReader reader =
             new BufferedReader(new FileReader(new File(getFilesDir(), FILE_NAME)))) {

    String line = reader.readLine();
    while (line != null) {
        stringBuffer.append(line).append('\n');
        line = reader.readLine();
    }
} catch (IOException e) {
    // Handle exception.
}

関連情報:

ユースケースに基づいて外部ストレージにデータを保存する

アプリに固有の機密性のない大きなファイルや、アプリが他のアプリと共有しているファイルには、外部ストレージを使用します。アプリがアプリ固有のファイルにアクセスするように設計されているか、共有ファイルにアクセスするように設計されているかによって、使用する API は異なります。

ファイルが個人情報または機密情報を含んでおらず、アプリ内でのみユーザーに価値を提供する場合は、外部ストレージ上のアプリ固有のディレクトリにファイルを保存します。

アプリが他のアプリに価値を提供するファイルへのアクセスまたは保存を行う必要がある場合は、ユースケースに応じて次のいずれかの API を使用します。

  • メディア ファイル: アプリ間で共有される画像、オーディオ ファイル、動画を保存およびアクセスするには、Media Store 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 オブジェクトを使用しないでください。代わりに、アプリ間でデータをセキュアに共有するための手順を実施します。

セキュリティ ライブラリには、SharedPreferences クラスをラップし、キーと値を自動的に暗号化する EncryptedSharedPreferences クラスも用意されています。

関連情報:

サービスと依存関係を最新の状態に保つ

ほとんどのアプリは、外部ライブラリとデバイス システムの情報を使用して、専門的なタスクを実行します。アプリの依存関係を最新の状態に保つことにより、このような通信ポイントのセキュリティを高めることができます。

Google Play 開発者サービスのセキュリティ プロバイダをチェックする

注: このセクションの説明は、Google Play 開発者サービスがインストールされたデバイスをターゲットとするアプリのみを対象とします。

Google Play 開発者サービスを使用するアプリでは、アプリがインストールされているデバイス上で Google Play 開発者サービスが更新されていることを確認してください。UI スレッドとは別に非同期でチェックを実行します。デバイスが最新の状態でない場合、承認エラーをトリガーします。

アプリがインストールされているデバイス上で Google Play 開発者サービスが最新の状態かどうかを判断するには、セキュリティ プロバイダを更新して SSL エクスプロイトから保護するためのガイドに記載されている手順を実施します。

関連情報:

アプリの依存関係をすべて更新する

アプリをデプロイする前に、ライブラリ、SDK、その他の依存関係がすべて最新の状態であることを確認してください。

  • Android SDK などのファースト パーティ依存関係については、Android Studio 内の更新ツール(SDK Manager など)を使用します。
  • サードパーティ依存関係については、アプリが使用するライブラリのウェブサイトをチェックして、利用可能なアップデートとセキュリティ パッチをインストールします。

関連情報: ビルド依存関係の追加

詳細

アプリのセキュリティを高める方法については、以下のリソースをご覧ください。