アンドロイドアプリからデバイスロック画面の呼び出し: Confirm Credential
アンドロイドもう8歳らしいです。昔からアンドロイドをやっていた方は、一度はアプリからデバイスロック画面を使って、本人認証できないかと思った事があるかと思います。「権限がなくてできません」と怒られてできなかったのですが、Android 5.0からできるようになってました。Android 6.0のConfirm Credentialを見てて、該当メソッドが、API Level21と記載されており、ええ!!と思い動作確認したら、5.0でも動きました。多分私以外にも同じような人がいると思いますので、「Confirm Credential」ご紹介です。
デバイスロック画面
Confirm Credential直訳すると資格確認となり非常にそっけなくなんと訳したらいいのか判断がつかないので英語のままで通しますが、自分のアプリからシステムのデバイスロック画面を呼び出し、ユーザが正しい値を入れたらOKをもらうという事ができます。
デバイスロック画面とは、ユーザがPINとかパスワードとかパターンを設定しデバイスをスリープした時から復帰する時に入力する物です。もちろんこれらの、PIN,パスワード、パターン等の値自体はアプリケーションにはわかりません。わかるのはユーザが正しい値を入力したかどうかだけです。
Confirm Credential Sample
Android6.0で追加されたサンプルプログラムにComfirm Credential Sampleがありますので、そのサンプルアプリを例にとり説明をしたいと思います。http://developer.android.com/samples/ConfirmCredential/index.html
サンプルアプリ動作
このサンプルアプリは買い物アプリです。商品購入時にデバイスロック画面を表示して、アプリ利用者がデバイス所有者本人かどうかを確認します。起動画面
アプリを起動すると「白のバックパック」を$62.68で購入する画面が表示されます。「PURCHASE」ボタンを押して購入します。サーバーとの連携は行っていないので仮想の購入です。アプリは起動時に端末がデバイスロック設定されているかを確認し、デバイスロックが設定されていない場合は購入ボタンを無効にします。
認証画面
購入ボタンを押すと、ユーザが本人であるかを確認するため下記のようなデバイスロック画面が表示されます。3種類のうちどれが表示されるかは、ユーザがどのデバイスロック方法を使用しているかで異なります。このデバイスロック画面は、システムにより表示されます。プログラム側でUIを作成する必要はありません。
※)デバイスに指紋が登録されている場合は自動的に画面下部に指紋アイコンが表示され、指紋による認証も可能になります。(一番右のPINの画面の下に指紋マークを出してみました)
購入完了画面
ユーザがデバイスロック画面で正しい値を入力した場合は、画面下に「Device credential confirmed.」「The device credential has been already confirmed within the last 30s seconds.」が表示されます。また再購入できないように購入ボタンは無効化されます。再認証(改造)
このサンプルにはデバイスロック画面の表示以外にもう一つの機能が含まれています。それはデバイスを最後にアンロックしてから一定時間が経過しているかどうかを確認する機能です。
この機能を確認するにはサンプルプログラムのshowPurchaseConfirmation、showAlreadyAuthenticatedメソッド内にある「PURCHASE」ボタンをDisableにしている部分をコメントアウトするように修正します。private void showPurchaseConfirmation() { findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE); //findViewById(R.id.purchase_button).setEnabled(false); } private void showAlreadyAuthenticated() { TextView textView = (TextView) findViewById( R.id.already_has_valid_device_credential_message); textView.setVisibility(View.VISIBLE); textView.setText(getString( R.string.already_confirmed_device_credentials_within_last_x_seconds, AUTHENTICATION_DURATION_SECONDS)); //findViewById(R.id.purchase_button).setEnabled(false); }上記のようにコメントアウトすると認証成功した時もボタンがDisableになりません。
この修正により一度商品を購入した後でも購入ボタンが押せるようになります。二回目以降の購入では「デバイスを最後にアンロックしてから一定時間が経過しているかどうかを確認する機能」により、最後のアンロックから30秒経過している場合のみデバイスロック画面が再表示されます。
この30秒以内経過以内ならデバイスロックを表示しないというのが、もう一つの機能です。
ソース解説
ソースコードはMainActivity.javaだけの非常に小さなアプリですパーミッションは必要なし
通常、アプリから指紋認証機能を使用するにはマニフェストファイルに以下に示すUSE_FINGERPRINTパーミッションの宣言が必要となります。しかし、このサンプルアプリでは上記パーミッションの取得をしていません。これは指紋認証を使用しているのはシステムが表示したデバイスロック画面であり、このアプリ自体は指紋認証の機能を持っていないからですアンドロイドではパーミッションをなるべく使わないアプリケーションの実装方法が推奨されております。
詳しくは以下の資料を参照してください。
Android Permissions Best-Practices:
Android Permissions Best-Practices:日本語訳
また、タオソフトウェアブログにも3つ記載があります。
CAMERAパーミッションなしに写真を取る方法
SEND_SMSパーミッションなしにSMSを送る方法
READ_CONTACTSなしで電話帳から電話番号を取得する方法
起動時の処理
このアプリは、デバイスロック設定されていないと、動作しませんので、KeyguardManager.isKeyguardSecure()メソッドを使用して、ユーザがデバイスロックを設定しているかを確認します。デバイスロックが設定されていない場合は「Setting → Security → ScreenLockでLockScreenを設定してください」と表示され、「PURCHASE」(購入)ボタンを無効化します。mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); if (!mKeyguardManager.isKeyguardSecure()) { // Show a message that the user hasn't set up a lock screen. Toast.makeText(this,"Secure lock screen hasn't set up.\n" + "Go to 'Settings -> Security -> Screenlock' to set up a lock screen", Toast.LENGTH_LONG).show(); purchaseButton.setEnabled(false); return; }
デバイスロック画面による認証
デバイスロック画面の表示は非常にシンプルな作りとなっています。
KeyGuardManagerクラスのcreateConfirmDeviceCredentialIntentメソッドでIntentを作成し、startActiviyForResultにインテントを渡す事でシステムがデバイスロック画面を表示します。ユーザにより正しい値が入力されたかどうかは、通常のstartActivityForResultの使い方と同じく、onActivityResultで受け取りresulstCodeがRESULT_OKかどうかで判断します。
前述しましたが、この機能を使用するのにUSE_FINGERPRINTパーミッションは必要ありません。
private void showAuthenticationScreen() { // Create the Confirm Credentials screen. You can customize the title and description. Or // we will provide a generic one for you if you leave it null 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) { if (tryEncrypt()) { showPurchaseConfirmation(); } } else { // The user canceled or didn’t complete the lock screen // operation. Go to error/cancellation flow. } } }このサンプルプログラムでは、ユーザがデバイスロック画面で正常な値を入れた時showPurchaseConfirmationを呼び出し暗号化処理を行っておりますが、この部分はアプリケーション毎に異なる部分で、暗号化とは切り離して考えるのが良いと思います。
createConfirmDeviceCredentialIntentメソッド詳細
KeyGuardManagerクラス
public Intent createConfirmDeviceCredentialIntent (CharSequence title, CharSequence description)
デバイス上の現在のユーザが資格を持っているかを確認するためデバイスロック画面(PIN,パターン、パスワード)を表示するためのインテントを返します。startActivityForResult(Intent, int)を使用してこのアクティビティを起動します。ユーザが正常に値を入力したかはRESULT_OKを確認します。
アプリ起動時にデバイスロックが設定されているかのチェックを行いましたが、ユーザがタスク切り替えを行い「デバイスロック設定なし」にする事が考えらます。デバイスロック設定なしの状態でこのメソッドを呼び出した場合nullが返されます。従って戻り値のnullチェックが必須になります。
その他、メソッドの引数としてタイトルと説明文を指定する事ができます。これらの値はデバイスロック画面に表示されます。
デバイスロック認証の有効期限
先に変更を加えたこのプログラムは、最後にデバイスロックアンロックに成功してから30秒以上経過すると再度デバイスロック画面を表示します。秒数は実際のサービスにより異なりますが、長くするとセキュリティー的に問題がありますし、あまり短くしてもユーザに負荷をかけるのでサービスに応じた適切な値にします。
このデバイスロック認証の有効期限がどのように実現されているかを解説します。
アンドロイドキーストア内への鍵の作成
アプリケーションを起動した後、createKeyメソッドにて、アンドロイドキーストア内にAES共通鍵を作成します。(キーストアとは、暗号化の鍵と証明書の格納場所でアンドロイドではAndroid4.3で導入されました)
この鍵は作成時にsetUserAuthenticationRequire()にてデバイスロックと関連付けをされ、setUserAuthenticationValidityDurationSeconds()にて鍵の有効期限(30秒)が設定されています。この鍵の作成はアプリケーション起動時に1回のみ行われます。
/** * アンドロイドキーストアに対称鍵(共通鍵)を作成します。最後にデバイスロック認証をしてからXXX秒以内化の認証に使用します。 */ private void createKey() { // 支払証明、トークン、その他のデータを複合化するキーを生成します。 try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); // 後で取り出せるように、アンドロイドキーストアーにエイリアス名をセットします。 //そして、Builderのコンストラクターに含めます。 keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setUserAuthenticationRequired(true) // 30秒以内のデバイロック解除を要求します。 .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build()); keyGenerator.generateKey(); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException e) { throw new RuntimeException("Failed to create a symmetric key", e); } }
暗号化
デバイスロック有効期限が設定された鍵を使用して暗号化を行います。 有効期限内の場合は暗号化に成功し、有効期限が切れている場合はデバイスロック画面を再度表示させて認証を行います(鍵の再生成は必要ありません)。 有効期限が切れている場合、chiper.init()はUserNotAuthenticatedExceptionエクセプションをスローします。 暗号化に成功した時は、購入に成功しましたと画面に表示しています。暗号化処理は「最後にデバイスアンロックされてからの経過時間を確認するため」に行っているため、暗号化したデータは特に使用しません。/** * createKeyメソッドで作成され鍵で、データを暗号化します。 デバイスロック画面での認証が成功した時のみに動作します。 */ private boolean tryEncrypt() { try { KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); keyStore.load(null); SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null); Cipher cipher = Cipher.getInstance( KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); // Try encrypting something, it will only work if the user authenticated within // the last AUTHENTICATION_DURATION_SECONDS seconds. cipher.init(Cipher.ENCRYPT_MODE, secretKey); cipher.doFinal(SECRET_BYTE_ARRAY); // If the user has recently authenticated, you will reach here. showAlreadyAuthenticated(); return true; } catch (UserNotAuthenticatedException e) { // User is not authenticated, let's authenticate with device credentials. showAuthenticationScreen(); return false; } catch (KeyPermanentlyInvalidatedException e) { // This happens if the lock screen has been disabled or reset after the key was // generated after the key was generated. Toast.makeText(this, "Keys are invalidated after created. Retry the purchase\n" + e.getMessage(), Toast.LENGTH_LONG).show(); return false; } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException(e); } }最後にデバイスアンロックを行ってからの経過時間はアプリ経由で行ったアンロックだけでなく、端末をONにしたときにシステムにより表示されるデバイスロックも含まれます。つまり端末をONにして30秒以内にこのサンプルアプリの「PURCASE」ボタンを押したとき、デバイスロック画面は表示されません。
まとめ
Confirm Credential Sampleプログラムは前述したように、二つの機能が入っているのと、ボタンをDisableするコードを削除しないと動きの確認ができないので、非常にわかりにくくなっています。二つの機能を分けて考えると、考えやすくなると思います。
デバイスロック画面がプログラムから呼び出せるようになったわけですが、昔これが出来なかった理由としては、プログラムがシステムのデバイスロック画面と非常に似たダイアログを作成し、ユーザのPINやパスワードを取得してしまう悪いソフトを恐れての事だったと記憶しています。
この問題は解決しているわけではなく、このようなアプリが表示するデバイスロック画面と、システムが表示するデバイスロック画面は区別がつかないので、私としては、このようなダイアログが出てきたらキャンセルしようと思っています。
とへんなまとめになってしまいましたが、以上です。
ブログ内の指紋関係の記事
追記(2015/11/18)
冒頭に5.0から動作すると記載がありますが、createConfirmDeviceCredentialIntent メソッドでintentを取得しstartActivityResultで結果をもらうメソッドの部分です。このサンプルでは追加で、キーストアを使って30秒とか色々していますが、その部分は6.0からしか動作しません