LoginSignup
6
5

More than 5 years have passed since last update.

Android 「App security best practices」 まとめ

Last updated at Posted at 2019-03-01

あらためて目を通す機会があったので、Androidの公式ページに掲載されているApp security best practicesのセクションについて和訳してみました。

注意事項

以下、2019/03/01現在の公式ページを和訳したものになります。
意訳や端折ってる箇所もありますので、間違い等ありましたらご指摘いただけると幸いです。

安全なコミュニケーションを強化する

暗黙的インテントとエクスポートしていないコンテンツプロバイダーを利用する

App Chooser を使う

暗黙的インテントで複数起動できるアプリがある場合は、App Chooserが明示的に表示される。
これにより、ユーザーは機密情報を信頼するアプリに転送できる。

val intent = Intent(ACTION_SEND)
val possibleActivitiesList: List<ResolveInfo> =
        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)
}

署名付きパーミッションを適用する

自分で管理/所有している2つのアプリ間でデータを共有するときは署名付きパーミッションを使用する。
このパーミッションはユーザー確認を必要としない代わりに、アプリが同じ署名鍵を使用して署名されていることを確認する。

AndroidManifest.xml
<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にアクセスすることを明示的に禁止することができる。
<provider>要素のandroid:export属性はAndroid 4.1.1(APIレベル16)以下の場合はデフォルトtrueになっているので、その場合には特に重要な設定である。

AndroidManifest.xml
<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>

ネットワークのセキュリティ対策を適用する

SSLを使う

アプリが信頼できるCAによって発行された証明書を持つWebサーバーと通信する場合はHTTPSリクエストは簡単に実装できる。

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

ネットワークセキュリティ設定を追加する

アプリが新規またはカスタムのCAを使用している場合は、設定ファイルでネットワークのセキュリティ設定を宣言できる。
このプロセスにより、アプリコードを変更せずに設定を作成できる。

  1. マニュフェストに設定を宣言する

    AndroidManifest.xml
    <manifest ... >
    <application
        android:networkSecurityConfig="@xml/network_security_config"
        ... >
        <!-- Place child elements of <application> element here. -->
    </application>
    </manifest>
    
  2. XMLのリソースファイルを追加する

  • クリアテキストを無効にして、特定のドメインへのすべてのトラフィックがHTTPSを使用する設定
res/xml/network_security_config.xml
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">secure.example.com</domain>
        ...
    </domain-config>
</network-security-config>
  • デバッグ時のみユーザーがインストールした証明書を許可する設定
res/xml/network_security_config.xml
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

TrustManagerを作成する

以下のどれかに当てはまる場合はTrustManagerを設定する必要があるかもしれない

  • 新しいCAまたはカスタムCAによって署名された証明書を持つWebサーバーと通信している
  • そのCAは、使用しているデバイスから信頼されていない
  • ネットワークセキュリティ設定は使用できない

この設定については以下のコンテンツを参照すること

WebViewは慎重に使用する

可能な場合は常に、ホワイトリストに登録されたコンテンツのみをWebViewでロードすること
アプリ内のWebViewではユーザーが自分の管理外のサイトに移動することはできないようにするべき

さらに、アプリケーションのWebView内のコンテンツを完全に制御および信頼している場合を除き、JavaScriptインターフェイスのサポートを有効にしないこと

HTML Message channelsを利用する

Android 6.0(APIレベル23)以降でJavaScriptを使用する必要がある場合はevaluateJavascript()ではなく、HTML Message Channels を利用してWebサイトとアプリ間で通信する。

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

// messagePorts[0] and messagePorts[1] represent the two ports.
// They are already tangled to 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"))

適切な権限を提供する

必要な最小限の権限のみを要求すること。また、可能であれば、アプリはこれらの権限の一部を不要になったら放棄すること

Intentを使って許可を延期する

可能であれば、別のアプリで実行できる操作を実行するための権限をアプリに追加しない。
代わりにすでに必要な権限を持っている別のアプリを起動することで許可を延期すること。

以下の例は、READ_CONTACTSWRITE_CONTACTSの権限を要求する代わりに連絡先アプリにユーザーを誘導する方法である。

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

また、ストレージへのアクセスやファイルの選択など、アプリがファイルベースのI/Oを実行する必要がある場合は、システムがアプリに代わって操作を完了できるため、特別な権限は必要なくなる。
さらに、ユーザーが特定のURIでコンテンツを選択した後、呼び出し側のアプリは選択されたリソースへの許可を与えられる。

アプリ間でデータを安全に共有する

より安全な方法でアプリコンテンツを他のアプリに共有するには以下に従うこと

  • 必要に応じて、読み取り/書き込み専用の権限を強制する
  • FLAG_GRANT_READ_URI_PERMISSIONFLAG_GRANT_WRITE_URI_PERMISSIONのフラグを利用して、データへのワンタイムアクセスを提供する
  • データを共有するときのURIはFileProviderを利用してfile://ではなくcontent://を使用すること。

次に例として、URIパーミッション付与フラグとコンテンツプロバイダパーミッションを使用して、アプリケーションのPDFファイルを別のPDFビューアアプリケーションに表示する方法を示す

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

安全にデータを保存する

プライベートデータを内部ストレージに保存する

アプリ毎にサンドボックス化されているデバイスの内部ストレージに保存する。
この領域は閲覧許可などは必要なく、他のアプリはファイルにアクセスすることはできないようになっている。
アンインストール時にはアプリと一緒に内部ストレージに保存した全てのファイルを削除する。

次に例として、内部ストレージにデータを書き込む方法および読み取る方法を示す

// 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!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}
// The file name cannot contain path separators.
val FILE_NAME = "sensitive_info.txt"
val fis = openFileInput(FILE_NAME)

// available() determines the approximate number of bytes that can be
// read without blocking.
val bytesAvailable = fis.available()
val fileBuffer = ByteArray(bytesAvailable)
val topSecretFileContents = StringBuilder(bytesAvailable).apply {
    // Make sure that read() returns a number of bytes that is equal to the
    // file's size.
    while (fis.read(fileBuffer) != -1) {
        append(fileBuffer)
    }
}

外部ストレージは慎重に使用する

スコープディレクトリアクセスを使用する

アプリごとにSandbox化されているプライベート領域を利用すること
この領域はファイル閲覧の許可は必要なく、他のアプリからは参照することはできない領域となっている

書き込みの例

val FILE_NAME = "sensitive_info.txt"
val fileContents = "This is some top-secret information!"

openFileOutput(FILE_NAME, Context.MODE_PRIVATE).use { fos ->
    fos.write(fileContents.toByteArray())
}

読み込みの例

val FILE_NAME = "sensitive_info.txt"
val fis = openFileInput(FILE_NAME)

val bytesAvailable = fis.available()
val fileBuffer = ByteArray(bytesAvailable)
val topSecretFileContents = StringBuilder(bytesAvailable).apply {
    while (fis.read(fileBuffer) != -1) {
        append(fileBuffer)
    }
}

データの有効性を確認する

外部ストレージのデータを使用している場合は、データの内容が破損または変更されていないことを確認すること
安定した形式ではなくなったファイルを処理するためのロジックも含める必要がある
例として、ファイルの有効性をチェックする権限とロジックを示す

AndroidManifest.xml
<manifest>
    <!-- API 19以降はサンドボックス化されているプライベート領域を利用すること。
         その場合は、権限の付与は必要ありません。 -->
    <uses-permission
          android:name="android.permission.READ_EXTERNAL_STORAGE"
          android:maxSdkVersion="18" />
</manifest>
private val UNAVAILABLE_STORAGE_STATES: Set<String> =
        setOf(MEDIA_REMOVED, MEDIA_UNMOUNTED, MEDIA_BAD_REMOVAL, MEDIA_UNMOUNTABLE)
// ...
val ringtone = File(getExternalFilesDir(DIRECTORY_RINGTONES), "my_awesome_new_ringtone.m4a")
when {
    isExternalStorageEmulated(ringtone) -> {
        Log.e(TAG, "External storage is not present")
    }
    UNAVAILABLE_STORAGE_STATES.contains(getExternalStorageState(ringtone)) -> {
        Log.e(TAG, "External storage is not available")
    }
    else -> {
        val fis = FileInputStream(ringtone)

        // available() determines the approximate number of bytes that
        // can be read without blocking.
        val bytesAvailable: Int = fis.available()
        val fileBuffer = ByteArray(bytesAvailable)
        StringBuilder(bytesAvailable).apply {
            while (fis.read(fileBuffer) != -1) {
                append(fileBuffer)
            }
            // Implement appropriate logic for checking a file's validity.
            checkFileValidity(this)
        }
    }
}

機密性の低いデータのみをキャッシュファイルに保存する

機密性の低いデータに素早くアクセスするためには、キャッシュ領域に保存すること。
1MBを超える場合はgetExternalCacheDir()それ以外はgetCacheDir()を使用してFileオブジェクトを取得すること。

以下例として、直近ダウンロードしたファイルをキャッシュする方法を示す

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

    • getExternalCacheDir()を利用して共有ストレージに配置すると、アプリ実行中でもユーザーがこのファイルを触ることができるため、ユーザー操作によるキャッシュミスを適切に処理するためのロジックを含める必要がある
    • これらのファイルにはセキュリティが適用されていないため、WRITE_EXTERNAL_STORAGE権限を持つアプリはすべてアクセスすることができる。
  • 関連コンテンツ

プライベートモードでSharedPreferencesを使用する

SharedPreferencesを利用する際にはMODE_PRIVATEを使用すること。
MODE_PRIVATEでは自分のアプリだけがアクセスできるようになるため。

アプリ間でデータ共有を行う場合はSharedPreferencesを使用しないこと
アプリ間でのデータ共有については以下を参照

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

ほとんどのアプリは特別なタスクを実行するのに外部のライブラリやデバイスシステム情報を使用する。
アプリの依存関係を最新に保つことが重要。

Google Playサービスのセキュリティプロバイダを確認する

GooglePlayサービスが更新されているか確認すること(ただし非同期で)
最新でない場合は、アプリは認証エラーを引き起こすはず。

確認方法については以下を参考にする

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

アプリをデプロイする前に、すべてのライブラリ、SDK、およびその他の依存関係が最新であることを確認すること

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

まとめ

こうして改めて確認すると、やりきれてないものもあるなぁと思ってしまいました。
実はこれは1ページまとめただけで、セキュリティのセクションは他にも色々あるんですよね。
とは言えセキュリティに関することなので、一度はしっかりと目を通すことをお勧めします。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5