Android Studioの落とし穴 ライブラリプロジェクト

A+ a-


EclipseからAndroid Studioへの開発環境移行はお済でしょうか?
仕事では、長期の仕事だったり、バージョンアップでもAndroid Studioへ移行するだけの理由がないなど、なかなか移行できませんが、そろそろ一回りしてようやく移行できた!という感じではないでしょうか?

今回はAndroid Studioでライブラリプロジェクトを使用する際の思わぬ落とし穴についてお話します。

最初に結論を言うと、
「知らない間に不要なパーミッションが追加されている可能性がある」
ということです。

それでは、何故このような事が起こるのかを順に説明したいと思います。



ライブラリプロジェクト


AndroidStudioで開発していてEclipseの時より便利だなぁと感じるのはプロジェクトにライブラリを追加するときではないでしょうか

一般的なオープンソースのライブラリであればbuild.gradleファイルのdependenciesに一行追加するだけでダウンロードから参照の設定まで行ってくれます。
また、Android独自のライブラリ形式であるライブラリプロジェクトを追加する場合であってもbuild.gradleに一行追加するだけで使用できます!!
(Eclipseの頃はライブラリプロジェクトを別プロジェクトとしてインポートしたあと参照設定を行って。。。と、作業が多くて大変でした。)

ライブラリプロジェクトと通常のライブラリの違いを簡単におさらいしておくと、
「ライブラリ内にリソース(レイアウト、スタイル、文字列、etc.)を持てるかどうか」です。
ライブラリプロジェクトはリソースを持つことができ、ビルド時にアプリ本体のリソースとマージされてアプリ側からアクセス可能になります。

Eclipseを使用した場合は上記のマージ対象にAndroidManifest.xml(以下、マニフェストファイルと呼ぶ)は含まれていませんでした。そのため、ライブラリプロジェクトに含まれるActivityをアプリ側で使用する場合、アプリ本体のマニフェストファイルにライブラリプロジェクトのActivityを宣言する必要があります。※1

※1 : Eclipse(+ADT version 20 preview3)でもproject.propertiesに「manifestmerger.enabled=true」を宣言しておくとEclipseでもマージできるらしい(未確認)。
一方、AndroidStudioはマニフェストファイルも自動的にマージされます。
つまりライブラリプロジェクトのマニフェストファイルにActivityを宣言するとアプリ本体のマニフェストに宣言を追加したのと同じことになります。
この場合ライブラリの中に実装とマニフェストファイルの定義が集約されるため、ライブラリ開発者と利用者双方にとってよりコンポーネントとして扱いやすくなるメリットが有ります。


しかし勘のいい人はもうお気づきだと思いますが、
マニフェストファイルが自動的にマージされるということはライブラリプロジェクトで宣言したパーミッションもマージされるということです。
これが冒頭で述べた「知らない間に不要なパーミッションが追加されている可能性がある」ことの原因です。
場合によっては「開発者が意図しない画面(Activity)やバックグラウンド処理(Service)がアプリに含まれてしまった」なんてことも起こりえます。

このマージ処理はandroid gradle pluginが提供するManifestMergerによって実現されています。
ManifestMergerは本来アプリのビルド時にバックグラウンドで自動的に実行される機能ですが、開発者が動作を理解した上で使用しないと思いもよらぬ結果を招きます。 ここからは、ライブラリプロジェクト利用時に開発者が意識すべきポイントとManifestMergerの賢い使いかたについて説明します。

例としてGoogleが提供する位置情報取得ライブラリ(play-service-location : Google Play Servicesに含まれるライブラリプロジェクトの一つ)をアプリに組み込む際のマージ処理を確認します。

ライブラリプロジェクトのマージ

[環境]===========
IDE : AndroidStudio 1.3.1
ライブラリプロジェクト : com.google.android.gms:play-services-location:7.8.0
================
はじめにAndroidStudioのFile>New>New Project... と進めてActivityを含まないプロジェクト(LibraryProjectPermission)を作成します。 作成されたLibraryProjectPermission/app/src/main/AndroidManifest.xmlに位置情報を取得するためのパーミッションを追加します。
この時点ではapplicationクラスと「<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />」のみが宣言されている状態です(図1)。

次にLibraryProjectPermission/app/build.gradleのdependenciesにライブラリプロジェクトの記載を一行追加します(図2)。
「compile 'com.google.android.gms:play-services-location:7.8.0'」はplay-services-locationというライブラリのバージョン7.8.0を使用することを表します。
このライブラリプロジェクトにはLocationServices.FusedLocationApiという精度の高い位置情報を簡単に取得するメソッドが含まれています。
右上の[Sync Now]を押すとライブラリプロジェクトのダウンロード、参照設定、マニフェストファイルのマージが自動的に行われます。


ライブラリプロジェクトは以下の場所にダウンロードされます(図3)。
LibraryProjectPermission/app/build/intermediates/exploded-aar

今回ライブラリプロジェクトとして指定したのはplay-services-locationだけですが、このライブラリプロジェクトが他のライブラリを依存関係に持つため以下の2つのライブラリプロジェクトも自動的にダウンロードされています。
  •  play-services-base
  •  play-services-maps
これが開発者が意識すべきポイントの一つ目です。
AndroidStudioが採用するGradleビルドシステムにはライブラリの依存関係を自動的に解決する仕組みが備わっています。
この仕組みはとても便利ですが、開発者が直接指定していないライブラリも取り込まれることによって以下の問題が発生する可能性があります。
  •  バグやセキュリティホールの混入
  •  マルウェアや情報収集モジュールの混入
  •  ライセンス感染(GPLなどの商用アプリでは使用し難いライセンスのライブラリの混入)
そのため、開発者は実際にアプリに取り込まれるライブラリを確認しておく必要があります。
またライブラリのバージョン指定は「7.8.+」(7.8.nの中で最新のもの)のように指定することも可能ですが、バージョンが変わったタイミングで依存関係も変わっている可能性があります。
それらを考慮すると特殊な場合を除きバージョンは固定値で指定し、盲目的なバージョンアップは避けた方が安全といえるでしょう。

マニュフェストのマージ


追加されたplay-services-locationのマニフェストファイルを確認してみます(図4)。ここにはこのライブラリプロジェクトが要求するminSdkVersionが宣言されています。
 次にplay-services-baseのマニフェストファイルを確認してみます(図5)。
ここにはminSdkVersionの定義の他にGoogle Play Servicesを使用するために必要な<meta-data>が宣言されています。

 最後にplay-services-mapsのマニフェストファイルを確認してみます(図6)。
ここにはインターネット接続や外部記憶装置への読み書きなどGoogle Mapの表示で必要と思われる複数のパーミッションが宣言されています。

ライブラリプロジェクトの取り込み後に改めてLibraryProjectPermission/app/src/main/AndroidManifest.xmlを確認してみても内容に変化はありません。
マージ結果は開発者が編集したマニフェストファイルを直接上書きせずに以下のディレクトリに出力されます。
LibraryProjectPermission/app/build/intermediates/manifests/full/debug/AndroidManifest.xml

これが開発者が意識すべきポイントの二つ目です。
開発者はアプリリリース時に必ず最終的に出力されるAndroidManifest.xmlの内容を確認しておく必要があります。
また、見やすいわけではないですがマニフェストファイルがどのようにマージされたかのログは以下のファイルにも出力されます。
LibraryProjectPermission/app/build/outputs/logs/manifest-merger-debug-report.txt

それでは、マージされたマニフェストファイルを確認してみましょう(図7)。

このファイルは直接編集するものではないため一部エラーとして表示されている箇所がありますが、実際のアプリではこれと同じ内容のAndroidManifest.xmlが使用されます。
最終的には自分で宣言したコンポーネント以外に以下の宣言が追加されました。

<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
このうちminSdkVersionが14となっていることからもわかるように、ManifestMergerは各マニフェストファイルの宣言およびbuild.gradleの内容を調べ、重複する項目に関しては優先度が高いものを使用するよう考慮されています。
実際minSdkVersionに関してはライブラリプロジェクト側で9が宣言されていますが、アプリ本体のbuild.gradleで宣言した14が使用されています。

ここまででマニフェストファイルがManifestMergerによってマージされることはわかりましたが一つ大事なことを忘れています。
それは「知らない間に不要なパーミッションが追加されている可能性がある」ということです。

改めて今回ライブラリプロジェクトを追加した理由を思い出して下さい。
それは位置情報取得機能を使用するためです。
使用目的を意識しながら追加された宣言1つずつを確認してみます。

  • <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="23" />
    • アプリとして必須。
  • <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    • ネットワークの状態を見るために使用するパーミッション。位置情報取得には不要。
  • <uses-permission android:name="android.permission.INTERNET" />
    • インターネットに接続するためのパーミッション。位置情報取得には不要。
  • <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    • 外部記憶装置に書き込みを行うためのパーミッション。位置情報取得には不要。
  • <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    • 外部記憶装置からデータを読み込むためのパーミッション。位置情報取得には不要。
  • <uses-feature android:glEsVersion="0x00020000" android:required="true" />
    • 端末に求めるOpenGLのバージョンを指定するための宣言。位置情報取得には不要。
  • <meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
    • Google Play Serviceの機能を使用するための宣言。Google Play Serviecsの機能を使用する上で必須。(Google Play Servicesのセットアップドキュメント参照。https://developers.google.com/android/guides/setup )

こうしてみてみると、自動的に追加されたものの多くが位置情報取得には不要であることがわかります。
本来必要としないパーミッションが付与されているということは開発者にとってもユーザにとってもデメリットでありセキュリティー的にも問題となります。
セキュリティー的に問題が無くても用途が明確でないパーミッションを持つアプリはユーザを不安にさせ、結果的にアプリのダウンロード数が減ってしまう可能性があります。

マージのコントロール方法


それではManifestMergerによって不要な宣言が追加されないようにするにはどのようにすれば良いのでしょうか。
ここからはManifestMergerの賢い使い方を説明します。

賢いと言ってもやることは単純でマージ処理で追加される不要な宣言に対して予め特定の属性を追加してマージ時に削除されるようするだけです。
不要な宣言を削除するように修正したLibraryProjectPermission/app/src/main/AndroidManifest.xmlを見てみましょう(図8)。
注目すべきは予めマージ後に不要となる宣言を追加した上で「tools:node="remove"」属性を指定することです。また、toolsというネームスペースはmanifestタグの属性「xmlns:tools="http://schemas.android.com/tools"」として追加しています。

この状態で一度ビルドを行いマージ後のマニフェストファイルを確認すると不要な宣言が削除されていることがわかります(図9)。

今回の例では実際の位置情報取得処理は省略しますが上記の不要な宣言の削除を行った状態でもGoogle Play ServiceのLocationServices.FusedLocationApiによる位置情報取得が行えることを確認しています。

只不要な権限であるかの判断は慎重にする必要があります。必要ないと思って消したものが実は使用されていたりした場合は、SecurityExceptionが発生します。ソースを見たり、テストを十分に行って不必要な権限かの判断をしてください。

まとめ

最後に今回の内容をおさらいします。
  • ライブラリプロジェクトの依存関係に注意し実際に取り込まれたライブラリを確認する。
  • ライブラリプロジェクトのマニフェストファイルはアプリ本体のマニフェストファイルにマージされる。
  • ManifestMergerは本来不要なパーミッションなどもマージすることがある。
  • ライブラリプロジェクトで宣言された不要なパーミッションは「tools:node="remove"」で取り除ける。

ManifestMergerの動作に関しては今回紹介したもの以外にも多くのオプションが存在します。
それらを使用することで特定の属性のみをマージしたり、特定のライブラリプロジェクトからマージするときだけ宣言を削除したりすることも可能です。
また、ライブラリプロジェクトを使用する場合に限らずbuild.gradleで指定した値でマニフェストファイルを上書きする際にもManifestMergerは使用されます。
是非とも公式ドキュメントを一度読んで見ることをお勧めします。
http://tools.android.com/tech-docs/new-build-system/user-guide/manifest-merger

最後に、少し宣伝をしますと、RiskFinderでは、解析したパーミッションの表示や、APKで使用しているライブラリの表示ができます。自身で作成したプログラムであれば、自分で確認もできます、協力会社に依頼したアプリケーション等は、なかなか確認が難しいです。(Android Studioの今回説明した機能により一層確認が難しくなりました)。特定の権限を削除してよいか、良くないかの判断もパーミッションを利用するAPIの使用箇所をリストアップするので、解析、確認に役立ちます。
 リスクファインダーではソースもいらず一発で確認が可能ですので、ご興味のある方はご連絡ください。
がく

がく

アンドロイドアプリの脆弱性検査ツールのRiskFinder株式会社 代表取締役 スマフォのセキュリティ色々やってます。

リスクファインダーはAndroidアプリのセキュリティホールを見つけます!

Androidスマートフォンが急速に普及するとともにアプリの脆弱性報告も急増しています。
リスクファインダーは、Androidアプリの脆弱性診断WEBサービスです。
500項目以上のチェックでアプリの脆弱性や問題を検出し、セキュアなアプリ開発をサポートします。

  • ブラウザでファイルをアップロードするだけ
  • アプリの脆弱性を指摘するだけでなく対処方法も提示
  • 頻繁にバージョンアップするAndroidの最新情報に素早く対応

リスクファインダーの詳細はこちら