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

以上.