2015/05/31

デバイス認証 - Confirm Credential

はじめに

caution!
本稿はAndroid M Preview向けに限られた内容です.
本稿の一部または全ては既存のAndroid versionやAndroid M正式版およびそれ以降のAndroid versionにおいて有効ではない可能性があります.

本稿の一部はAuthenticationの内容を参考としています. より正確な情報を得たい場合はこちらもご参考ください.
また, 本稿の説明で例示しているソースコードの完全版はGitHub上に公開しています.

Introduction

あなたのアプリケーションでDevice Credential, つまりデバイス認証機能をより簡単に使用できるAPIをAndroid M Previewは提案している.

ユーザが特定のアクションを起こす前にその端末で使用されているデバイス認証機能を利用してユーザ認証を行うことができる.
デバイス認証にはPIN, Pattern, Passwordといった認証方法が用意されている. 使用される認証形式については端末に設定されている認証形式, つまりその時端末に設定されているセキュリティロックの解除方式に左右される.
この機能を使用することで, ユーザはアプリ固有のパスワードといった認証情報を個別に覚えておく必要がなくなる.

Confirm Credentials

Confirm Credential

アプリケーションがデバイス認証をより簡単に, よりセキュアに使用できるAPIがいくつか追加された.
アプリケーションはデバイス認証状態を確認し, 必要に応じて認証画面を呼び出すことができる. アプリケーションはデバイスの認証画面をIntentで呼び出し, その結果をonActivityResultの形で得る.

デバイス認証にはタイムアウトの概念が設けられている. これは一度ユーザがあなたのアプリケーションでデバイス認証を行ったあと, 指定時間の間は認証がパスされる仕組みだ.

指定時間はデバイス認証を解除するセキュリティ鍵(共通鍵暗号方式)の有効時間として設定され, 新たに設けられたKeyGenParameterSpec.setUserAuthenticationValidityDurationSeconds()のAPIとKeyGeneratorまたはKeyPairGeneratorを使用して実現できる.

アプリケーションはユーザ体験を損ねないようデバイス認証が過度に表示されないように注意しなければならない.
先述のセキュリティ鍵をもってデバイス認証解除を試み, もしタイムアウトを満了しているようであれば
createConfirmDeviceCredentialIntent() メソッドで再度デバイス認証を行うこと.
認証の結果はstartActivityForResult(Intent, int)で返却されRESULT_OK(認証成功)かどうかをチェックするだけでよい.

デバイス認証をパスすることはデバイス操作を許可されたユーザであることを意味するが, 操作しているユーザが必ずしもデバイスオーナーである保証はない. また, デバイス認証は必ずしもユーザが設定しているとは限らない. この場合createConfirmDeviceCredentialIntent()の結果としてnullが返却される.
事前にユーザがデバイス認証の設定を行っているかを確認したい場合はKeygardManager.isKeyguardSecure()を使用できる.

if (!mKeyguardManager.isKeyguardSecure()) {
  // User hasn't set up a lock screen.
  Toast.makeText(this,
      "Security lock has not been set up.", Toast.LENGTH_LONG).show();
  // button.setEnabled(false);
}

あなたのアプリケーションにおいて特定の操作に必要とされる認証強度がデバイス認証で満足いくものであるかどうかは上記を踏まえて考える必要がある.

Coding!

Check Keyguard secure

デバイス認証機能を使用する場合, まずは端末で当該機能がONになっているかを確認する必要がある.
この機能が有効になっていないと後のセキュリティ鍵を生成することができない.

protected void onCreate(Bundle savedInstanceState) {
  ...
  if (!mKeyguardManager.isKeyguardSecure()) {
    // User hasn't set up a lock screen.
    textView.setText("セキュリティロックを設定してください");
    return;
  }

デバイス認証機能が有効であることを確認した上で, Android Key Store Systemからセキュリティ鍵(対照鍵)を作成する.
セキュリティ鍵を生成する際に有効期間も設定しておく. これがデバイス認証を再度ユーザへ求めるまでの指定時間となる.

  private void createKey() {
    try {
      KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
      keyStore.load(null);
      KeyGenerator keyGenerator = KeyGenerator.getInstance(
          KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

      // セキュリティ鍵を生成する
      keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
          KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
          .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
          // デバイス認証機能を必須化. デバイス認証機能がOFFの場合はセキュリティ例外が発生する
          .setUserAuthenticationRequired(true)
          // セキュリティ鍵の有効期間を設定する
          .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
          .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
          .build());
      keyGenerator.generateKey();
    } catch (InvalidAlgorithmParameterException
        | CertificateException | NoSuchAlgorithmException | KeyStoreException
        | IOException | NoSuchProviderException e) {
      throw new RuntimeException("鍵の生成に失敗", e);
    }
  }

デバイス認証が必要かどうか, あるいはセキュリティ鍵の有効期間内かどうかは, ここで作成したセキュリティ鍵の復号化で判断できる.
アプリ起動時等にデバイス認証機能のOn/Offを確認し, セキュリティ鍵を生成した後にユーザがデバイス認証機能をOffにするケースも考慮する必要がある.

  private void tryEncrypt() {
    try {
      KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
      keyStore.load(null);
      SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null);

      // 鍵の復号化が成功するのはデバイス認証の指定時間30秒以内である場合に限られる
      Cipher cipher = Cipher.getInstance(
          KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
              + KeyProperties.ENCRYPTION_PADDING_PKCS7);
      cipher.init(Cipher.ENCRYPT_MODE, secretKey);
      cipher.doFinal(SECRET_BYTE_ARRAY);

      // 鍵の有効期間内(認証済み)であればデバイス認証をパスする
      showAlreadyAuthenticated();
    } catch (UserNotAuthenticatedException e) {
      // デバイス認証機能にまだパスしていない
      showAuthenticationScreen();
    } catch (KeyPermanentlyInvalidatedException e) {
      // 鍵が生成されたあとにデバイス認証機能をoffにされた場合に発生する
      textView.setText("作成されていた鍵が無効になりました. 再度実行してください.");
    } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
        CertificateException | UnrecoverableKeyException | IOException
        | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
      throw new RuntimeException(e);
    }
  }

Launch Security lock screen

デバイス認証にパスしていない状態であればcreateConfirmDeviceCredentialIntent()で認証画面を呼び出せる.
引数にはそれぞれ画面のタイトルと説明文を指定でき, 認証画面の文言をカスタマイズすることができる.

  private void showAuthenticationScreen() {
    // デバイス認証画面のタイトルと説明文は変更することが可能
    Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
    if (intent != null) {
      startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
      // Challenge completed, proceed with using cipher
      if (resultCode == RESULT_OK) {
        showConfirmation();
      } else {
        // The user canceled or didn’t complete the lock screen
        // operation. Go to error/cancellation flow.
      }
    }
  }

Confirm Credential security lock

前回のデバイス認証から指定時間の範囲内であれば鍵の復号化が成功する. この場合はユーザに再度認証を求める必要はない. ユーザへの過度な認証要求を投げないように注意すること.

Confirm Credential already enable

以上.

新しいパーミッションモデル - Runtime Permission

はじめに.

caution!
本稿はAndroid M Preview向けに限られた内容です.
本稿の一部または全てはM Preview以前のAndroid versionやM正式版で有効ではない可能性があります.

本稿の一部はPermissionsの内容を参考としています. より正確な情報を得たい場合はこちらもご参考ください.
また, 本稿の説明で例示しているソースコードの完全版はGitHub上に公開しています.

New permission model

Android Mでは新しいApp permission modelが提案されている.
従来モデルではアプリケーションが使用するパーミッションのリストをインストール時にユーザへ提示し, これらの使用許可を一度に取得する必要があった.
従来モデルでは一見不要と思えるパーミッションがリストされ, マルウェアと誤解されるケースもあり, アプリケーションのインストールを阻害する1つの障壁となっていた.

新しいApp permission modelの導入により, アプリはインストール時に必要最低限のパーミッションをユーザに求め, アプリケーションをインストール後, 必要となったタイミングでユーザにパーミッションの使用許可を求めることが可能になった.

アプリケーションの付加的な機能として提供されるものであればインストール時にパーミッションの許可を求めず, それを実行する時になって初めてパーミッションの使用許可を得れば良い.
たとえば, Google Analyticsでトラッキング情報の収集をユーザが求めないケースを考える.

従来モデルであれば, アプリケーションはGoogle Analyticsを使用するために下記のパーミッションの使用許可を得る必要があった. 例えユーザがGoogle Analyticsの使用に同意しなかったとしてもだ.

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

アプリケーションはGoogle Analytics以外でこれらのパーミッションが不要であったとしても, それを前もってユーザに理解してもらうのは困難だ. むしろ情報が抜き取られるのではないかという不信感をINTERNETパーミッションが与えているかもしれない.

新しいApp permission modelではアプリケーションのインストール時にこれらのパーミッションの使用許可を得る必要がなくなる.
代わりにそれらの機能を必要とする時, つまりアプリがインストールされた後, その機能をユーザが使用する際にパーミッションの使用許可を求める形に変えることができる.

ユーザがGoogle Analyticsの使用に同意しない場合(つまりINTERNETパーミッションの使用を拒否した場合), アプリケーションのインストールを諦める必要はない. 代わりにGoogle Analytics機能の使用のみを諦め, アプリケーションはそれ以外の機能を提供すれば良いのだ.

2015.06.25 訂正
INTERNETパーミッションはこちらに記載されている通り, PROTECTION_NORMALとなるため, Runtime Permissionのそれとは異なりインストール時に付与されるパーミッションです. 上の例は誤った内容であるためここに訂正します. (ご指摘いただいた@tao_gaku様ありがとうございました.)

対照的に, 許可を得られなければアプリケーションの価値を十分に提供できないようなパーミッションであればインストール時にまとめて取得しておいたほうが良いだろう. パーミッションの使用許可を得られなかったばかりにアプリケーションがまともに動作しないようであればユーザはあなたのアプリに低い評価をつけるかもしれない.

for M Preview Apps

M Preview向けに作成されたアプリ, つまりTargetSdkVersionがM Preview以上であるアプリは新しいApp Permission modelをベースに作成されているものとしてシステムは振る舞う.

Permission settings

M Previewではユーザがアプリケーションのパーミッションをいつでも剥奪できるようにパーミッションに関する設定画面を設けている.
Settings>Apps>Advanced>App permissionsからパーミッションの設定画面を起動できる.
アプリケーションは一度使用許可を得たパーミッションであっても, 次に実行するときにはそれが剥奪されている可能性を考慮しておく必要がある.

for System applications

通常のサイクルとは異なり, System applicationはインストール時にAndroid Manifestに定義されたパーミッションをユーザの確認なしにシステムによって許可される.
ただし, System applicationであっても例外なくユーザが設定アプリケーションから個別にPermissionを剥奪することが可能であることを忘れてはいけない.

for Signature protection

Protection levelがsignatureであるパーミッションは, 同パーミッションを持つアプリケーションが既に存在する場合, ユーザの確認なしにシステムによって許可される.
ただし, これについてもSystem applicationと同じく設定アプリケーションから個別にパーミッションの剥奪が可能であることに注意する必要がある.

for legacy applications

アプリケーションがM向けに作られたものではなく(targetSdkVerison < M), かつ新しいApp Permission Model上で動作する場合, こちらも同様に設定アプリケーションから個別にパーミッションを剥奪することが可能となっている.

ただし, M向けに作られたアプリケーションは新しいApp Permission Modelに対応していないことが予想されるため, システムはその場合に”アプリケーションが正常に動作しない可能性がある”旨のダイアログを表示してパーミッションの剥奪を行う.

Legacy app permission deny

レガシーなアプリケーションがパーミッションを剥奪された場合, パーミッションが必要なAPIを実行すると必ずセキュリティーパーミッションが発生するとは限らない. 代わりに空のデータが返却されることもあるだろうし, エラーを意味するコードが返されるかもしれないし, 予期しない動作となる場合もある.
例えば, カレンダー情報の検索に必要なクエリをパーミッションの使用許可なしで実行した場合, クエリの結果は”空”を返す.

for Communication

アプリケーションが写真を撮るケースにおいて, アプリケーションがandroid.permission.CAMERAのパーミッションをユーザにリクエストし, これの使用許可を 得た上でCamera APIを使用して写真を撮影する場合. このアプローチではアプリケーションにパーミッションの必要範囲が閉じており, 操作においても同様である.

異なって, Camera APIを使用せずACTION_IMAGE_CAPTURE Intentを使って写真の撮影を試みる場合, もしあなたのアプリケーションがACTION_IMAGE_CAPTURE Intentをサポートしている場合, さらにあなたのアプリケーションがまだandroid.permission.CAMERAのパーミッション使用許可を得ていない場合, あなたのアプリケーションはandroid.permission.CAMERAの使用許可をユーザに求めるが, この時に連携元のアプリケーションが持つパーミッションが適切かどうかの確認も怠ってはならない.

Coding!

Manifest

M向けに作成されるアプリは必ず新しいApp Permission Modelに対応しておく必要がある.
AndroidManifestにはアプリに必要なパーミッションを宣言しておき, 実行時に使用許可をとればよいパーミッションであれば必要になってから許可をとる仕組みを実装する.

M Previewで新しいPermission Modelをテストするにはbuild.gradleで次を設定する.

  • compileSdkVersion is set to ‘android-MNC’
  • minSdkVersion is set to ‘MNC’

Preview SDKでは古いプラットフォームでテストできないことも明示しておく.

  • targetSdkVersion is set to ‘MNC’

M Preview版では<uses-permission>を補完する要素として新たに<uses-permission-sdk-m>を宣言できる. <uses-permission-sdk-m>はMより古いOSを搭載した端末では無視され<uses-permission>の宣言がされていないものとして扱われる. そのため古いOS上ではアプリケーションをインストールする際に<uses-permission-sdk-m>で定義されたパーミッションの使用許可は表示されず, これの使用許可も与えられない.

パーミッションを必要とする場合, 古いOSでは<uses-permission>で宣言しておく必要がある. あなたのアプリケーションが古いOSで動作するのに必要なパーミッションは<uses-permission>で宣言しておく必要がある. ただし, M以降でのみ必要となるパーミッションは<uses-permission-sdk-m>で宣言する選択肢を得られる.

注意すべきは, <uses-permission>で宣言されたパーミッションであってもユーザはこれらのパーミッションを剥奪できるということである. M以降の端末をターゲットとするのであれば<uses-permission-sdk-m>, <uses-permission>双方で定義されたパーミッションが剥奪されている状況を考慮して実装しておく必要がある.

Check and Request Permissions

新しいApp Permission Modelに対応した端末であるかどうかを確認するにはBuild.VERSION.CODENAMEでOSのコードネームを確認する. M Previewである場合は"MNC"が返却される.

  private boolean isMNC() {
    // TODO: In the Android M Preview release, checking if the platform is M is done through the codename, 
    // not the version code.Once the API has been finalised, the following check
    // should be used:
    // return Build.VERSION.SDK_INT == Build.VERSION_CODES.MNC

    return "MNC".equals(Build.VERSION.CODENAME);
  }

アプリケーションがパーミッションの使用許可を得ているかどうかの確認にはContext.checkSelfPermission(permission_name)を使用する.
カメラの使用許可を確認するにはContext.checkSelfPermission(Manifest.permission.CAMERA)で, もし使用許可を持っていない場合はrequestPermissions()で許可を得る.

public void onClick(View v) {
  // 自アプリにカメラの使用許可があるか確認
  if (!hasSelfPermission(MainActivity.this, permission.CAMERA)) {
    // 使用許可がない場合はこれをリクエストする
    requestPermissions(new String[]{permission.CAMERA}, REQUEST_CODE);
  } else {
    // 使用許可がある場合はカメラを起動する
    launchCamera();
  }
}

private boolean hasSelfPermission(Activity activity, String permission) {
  return activity.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED;
}

requestPermissionsに指定するパーミッションはAndroidManifestであらかじめ<uses-permission-sdk-m>あるいは<uses-permission>で宣言されている必要がある. AndroidManifestで宣言されていないパーミッションをリクエストすることはできない.

requestPermissionsでパーミッションをリクエストするとシステムはパーミッションの使用許可をユーザにたずねるダイアログを表示する.

Allow Camera Permission

ここでの結果はActivity.onRequestPermissionsResultで得ることができる.

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
  if (requestCode == REQUEST_CODE
      && permission.CAMERA.equals(permissions[0])
      && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    launchCamera();
  }
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Testing

新しいApp Permission Modelのために新たなADBコマンドがいくつか追加されている.

Install with permissions

adb installコマンドへ新たに-gオプションが追加された. この引数ありでインストールされたアプリケーションはAndroidManifestで宣言されている全てのパーミッションの使用が許可された状態でインストールされる.

$ adb install -g <path_to_apk>

Grant and revoke permissions

package manager (pm)コマンドにアプリケーションのパーミッションを付与/剥奪するコマンドが追加された.

# Permission Grant
$ adb pm grant <package_name> <grant_permission_name>

# Permission Revoke
$ adb pm revoke <package_name> <revoke_permission_name>

Best Practices

新しいApp Permission modelでは, あなたのアプリケーションがパーミッションを求める度にユーザの操作を中断する. これはユーザ体験を阻害する要因にもなるため, できる限りその頻度を抑える努力をすべきである.

例えば, あなたのアプリケーションでカメラで撮影した画像を取得するためにCAMERAパーミッションの使用許可を得てCamera APIを実装するのではなく, MediaStore.ACTION_IMAGE_CAPTURE のIntentによるアプリ連携でこれを実装することができる.

また, 一度に大量のパーミッション使用許可を求めるべきではない. パーミッションは必要となった時に求めるように努め, ユーザを唖然とさせないようにすること.
例えば, あなたがフォトグラフィアプリケーションを作成している場合, アプリケーションを起動した際にCameraパーミッションを求めるようにすべきだが, 撮影した画像をContactsデータを参照してShareする機能を有していた場合に, Contacts参照のパーミッション使用許可までアプリ起動時に取得してはならない.
後者のパーミッションはユーザのShareアクションまで待って, そこで初めてパーミッションの使用許可を得るのが常套手段である.

Many permission allowing

Explain why you need permissions

パーミッションの使用許可を得るダイアログには, あなたのアプリケーションがどのような理由でそのパーミッションの使用許可を求めているのかの説明文は表示されない. たとえばフォトグラフィアプリケーションが突然ローケーションサービスの使用許可を求めるとユーザはなぜフォトグラフィアプリケーションにロケーションサービスのパーミッションが必要であるのか困惑し不安になる. これは良いユーザ体験を生まない.

フォトグラフィアプリケーションは画像に埋め込まれたgeotagを解釈し, よりよいユーザ体験を提供することをユーザに説明する必要があるかもしれない. それはrequestPermissions()を呼び出す前に済ませておいたほうがよい.
例えばアプリケーションのチュートリアルにそれを埋め込んでおく方法がある. チュートリアルで機能を説明し, その時点でパーミッションの使用許可を取得しておく方法も有効だ. あるいはそこでShareのデモンストレーションをやってしまってもよいかもしれない. ユーザはあなたのアプリケーションが不必要にパーミッションの使用許可を求めていないことを理解し安心できるだろう.
もちろん, 全てのユーザが忠実にチュートリアルを読むとは限らず, また一度許可したとしても後からこれを剥奪される可能性も残っているため, Shareを実行する際にはパーミッションのチェックをいつも通り実施する必要はある.

以上.