2015年12月16日

Android

Androidでも指紋認証しよう!

  • このエントリーをはてなブックマークに追加

Yahoo! JAPAN Tech Advent Calendar 2015の16日目の記事です。一覧はこちら

こんにちは。Yahoo! JAPAN Tech Advent Calendar 2015の16日目を担当します、Androidエンジニアの里山です。今回は、Androidプラットフォームの最新状況のお話として指紋認証をテーマにしたいと思います。

アプリ開発の次のステージは

株式会社日経BPコンサルティングの調査によると、2015年8月31日時点で国内のスマホの普及率は49.7%、10代、20代といった若年層のスマホ普及率は8割以上にも上るという調査結果が出ています。アクティブな年代で普及しきった印象があるので、長期的に見るとこれからのスマホユーザーの伸びは鈍化していくものと思われます。

このような状況の中で、アプリ開発の世界も少しずつ変わってきています。新規ユーザーが右肩上がりに獲得できる時代ではないので、業界全体として、これまでよりもより本質的な価値提供によって、ユーザーとのエンゲージメント(サービスとユーザーの深いつながり、親密度)を高める方向に興味が向かっています。

日本を中心に事業展開しているヤフーでは、「安心・安全」というキーワードを、ユーザーとのエンゲージメントの要素のひとつとして訴求しています。現在のスマホの普及度からみても、年齢・性別・職業、様々な属性の方が利用しているので、ユーザーのスマホ・インターネットに対するリテラシーは多様であると考えられます。リテラシーに関わらず、ユーザーに安心してアプリを使ってもらう環境を用意することは、わたしたちヤフーのユーザーへの課題解決につながるのではないかと考えています。

私の所属するブラウザーサービスでは、今年5月、iOS向けのヤフーブラウザーをリリースしました。主な特徴として、指紋認証を利用し、ログインするサイトのIDやパスワードを管理する「かんたんログイン機能」や、ブックマークフォルダーに指紋で鍵をかける「プライベートブックマーク機能」を提供していて、ユーザーに「安心・安全」に使ってもらうことを意識しています。

図:1 ヤフーブラウザー(iOS版)は指紋認証を採用している

ブラウザーサービスでは、Android版のヤフーブラウザーも配信しています。こちらでも指紋認証を利用する可能性を探るべく、今回は、Androidでの指紋認証の最新動向を探ってみました。

Android 6.0 Marshmallowで正式に指紋認証APIが登場

Googleは10月7日、Androidの最新バージョンである、「Android 6.0 Marshmallow」を正式に発表しました。Marshmallowでは大幅にAPIが拡充されましたが、その中で指紋認証のための仕組みとして「Fingerprint Authentication」「Confirm Credential」が入ってきました。

Fingerprint Authentication

「Fingerprint Authentication」は、指紋認証機能を開発者が自由にアプリに組み込む事ができるのが特徴です。SystemServiceからFingerprintManagerのインスタンスを生成して利用します。開発者は指紋認証フローのUIを実装し、Android標準の指紋認証アイコンをUIに利用する必要があります。アイコンは、Googleの提供するサンプルアプリ「FingerprintDialog」に同梱されています。

図:2 Android標準の指紋認証アイコン

「Fingerprint Authentication」を利用する複数のアプリケーションを開発する場合、各アプリは独立してユーザー認証しなければならないとGoogleのドキュメントには書いてあります。今のところは複数アプリ間のシングルサインオンはしてはいけないようです。

Confirm Credential

「Confirm Credential」はデバイスのロック画面をアプリに利用する仕組みです。ユーザーがすでに設定しているデバイスロックの方法に基づき、ユーザーを認証することができます。開発者としては、独自のUIを構築する必要も、パスワードを管理することもなく手軽にアプリに認証を導入できます。ユーザーとしてもアプリ個別のパスワードを覚えることから開放されます。

対応端末

「Android 6.0 Marshmallow」の発表とともに、開発者向けのレファレンス端末として、「Nexus 5X」「Nexus 6P」が発表され、2015年12月現在両機種とも発売されています。現時点で指紋認証を試す場合はこの端末しかありませんが、今後ラインナップは増えていくでしょう。

※ 現在発売されている「Xperia Z5」などの端末も指紋認証に対応していますが、現時点ではAndroid 5.x系の端末なので、MarshmallowのAPIは利用できないので気をつけてください。(今後アップデートで使えるようになるかもしれません)

図:3 Nexus 5X、Nexus 6P (Google ストアより)

サンプルコードを追って実装を知る

Googleは「Fingerprint Authentication」「Confirm Credential」の実装サンプルを用意しています。こちらを元に実装方法を確認しましょう。

一部、わかりやすいように書き換えて説明しています。

Fingerprint Authenticationの実装

Googleは、「Fingerprint Authentication」の実装例として、ふたつのサンプルを用意しています。ひとつは、ローカルデータを暗号化する「対称鍵」を利用する方法で、サンプルは、Google Developersのサイトにある、FingerprintDialogになります。もうひとつは、公開鍵と秘密鍵のペアからなる「非対称鍵」を利用する方法で、サーバーサイドと公開鍵を持ち合うことで、アプリとサーバー間のデータ通信がセキュアに保たれ、ログインや購入フローと行った処理を指紋認証とひも付けることができる例となります。サンプルは同じくGoogle Developersのサイトにある、AsymmetricFingerprintDialogで公開されています。今回は、より複雑な「AsymmetricFingerprintDialog」を元に説明します。

図:4 AsymmetricFingerprintDialogアプリ

主な処理フローは以下のような形になります。

図:5 AsymmetricFingerprintDialogアプリの処理フロー

ソースコード一式がダウンロードできますので、動かしながら確認すると良いでしょう。
AndroidStudioのFile->Import Sample…からも取得できます。

「Fingerprint Authentication」の実装手順は以下のとおりです。

1.指紋認証利用の有効化

指紋認証機能を使うために、AndroidManifest.xmlに以下のuses-permissionの設定を入れます。

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

2.FingerprintManagerのインスタンス取得

まずはFingerprintManagerのインスタンス取得です。
基本的には、Contextのインスタンスから、getSystemService()メソッドで呼び出します。

FingerprintManager mFingerprintManager = (FingerprintManager)context.getSystemService(FingerprintManager.class);

このメソッドが動作するには、端末のAPI Levelが23以上である必要があります。実際にはAPI Levelがより低い端末をサポートしているアプリがほとんどだと思いますので、端末のSDK Versionによって、指紋認証を使う/使わないのハンドリングを行う必要があります。

実は、FingerprintManagerには、FingerprintManagerCompatというサポートクラスが用意されています。このクラスを使ってFingerprintManagerクラスを利用することで、OSバージョン別のハンドリングをうまくラッピングしてくれるので、こちらを利用するほうが良いでしょう。

FingerprintManagerCompatを使うと、以下のように書くだけで、インスタンス生成時のOSバージョン振り分けの記述が必要なくなります。

FingerprintManagerCompat mFingerprintManager = FingerprintManagerCompat.from(context);

3.ロック画面に指紋が設定されているかチェックする

指紋認証を実現するには、端末にあらかじめ指紋が登録されている必要があります。その確認を行っていきます。

まずは、ロック画面を制御するKeyguardManagerのisKeyguardSecure()メソッドでロック画面に指紋やパスワードが設定されているか事前に確認しておきます。

KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KeyguardManager.class);
if (!mKeyguardManager.isKeyguardSecure()) {
 // ロック画面に指紋設定やパスワードなどを設定するようにユーザーにメッセージング
 // 購入ボタンの非活性など
}

その上で、FingerprintManagerのhasEnrolledFingerprints()メソッドで指紋が登録されているかを確認します。

if (!mFingerprintManager.hasEnrolledFingerprints()) {
 // 購入ボタンの非活性など
 // 指紋が設定されていない旨、ユーザーにメッセージング
}

 4.指紋認証後に利用できる非対称鍵を生成する

「Fingerprint Authentication」は、Android KeyStoreと連携して使うことができます。指紋認証前に非対称鍵を作り、指紋認証と連携させることで、指紋認証後に鍵を使うことが可能になります。鍵の生成にはKeyPairGeneratorを利用します。

〜省略〜
KeyPairGenerator mKeyGenerator;
try {
   mKeyGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
   // error handling
}
〜省略〜
try {
 mKeyPairGenerator.initialize(
    new KeyGenParameterSpec.Builder("<任意の鍵の名前>", KeyProperties.PURPOSE_SIGN)
                            .setDigests(KeyProperties.DIGEST_SHA256)
                            .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
                            .setUserAuthenticationRequired(true)
                            .build());
  mKeyPairGenerator.generateKeyPair();
} catch (InvalidAlgorithmParameterException e) {
    // error handling.
}
〜省略〜

サーバーサイドと連携するには、生成した鍵から公開鍵を取得し、サーバーサイドに登録する必要があります。PublicKeyをKeyStoreから取得し、サーバーサイドに送付して登録します。

KeyStore mKeyStore;
try {
 // 公開鍵を取得する
 mKeyStore = KeyStore.getInstance("AndroidKeyStore");
 mKeyStore.load(null);
 PublicKey publicKey = mKeyStore.getCertificate("<任意の鍵の名前>").getPublicKey();

 // 以下の変換処理は、API Level 23のバグにより、現在は必要とのこと
 KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm());
 X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
 PublicKey verificationKey = factory.generatePublic(spec);

 // ここでverificationKeyをサーバーサイドに登録する

} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException |
                IOException | InvalidKeySpecException e) {
 // error handling 
}

 5.指紋認証を行う

指紋認証を行います。指紋認証のプロセス自体は、FingerprintManagerのauthenticateメソッドを呼び出すだけです。 秘密鍵から署名オブジェクトを作り、FingerPrintManager.CryptoObjectに変換して渡します。
コールバックはFingerprintManager.AuthenticationCallbackのインスタンスを渡すことで受け取れます。

// 秘密鍵から署名オブジェクトを生成
Signature mSignature;
try {
     mKeyStore.load(null);
     PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
     mSignature.initSign(key);
} catch (KeyPermanentlyInvalidatedException e) {
    // error handling
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
                | NoSuchAlgorithmException | InvalidKeyException e) {
    // error handling
}
CancellationSignal mCancellationSignal = new CancellationSignal();
// 指紋認証を行う
mFingerprintManager
        .authenticate(new FingerprintManager.CryptoObject(mSignature), 
                           mCancellationSignal, 0, new FingerprintManager.AuthenticationCallback(){
            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                // 認証中のエラー
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                // センサーが汚れているなど、リカバリー可能なエラーのとき呼ばれる
                // メッセージなどを出す
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                // 認証成功
            }

            @Override
            public void onAuthenticationFailed() {
                // 認証失敗
            }
       }, null);

 6.公開鍵を取得する

指紋認証に成功した時、FingerprintManager.AuthenticationCallbackクラスのonAuthenticationSucceededメソッドがコールバックされます。この引数にあるFingerprintManager.AuthenticationResultクラスのインスタンスから、認証前に渡した暗号化オブジェクトを取得することができます。このオブジェクトから、サーバーに送りたい値を暗号化し、サーバーに送付します。サーバー側では公開鍵の値を元に検証することで、指紋認証したことをサーバー側でも確認できます。

@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
   FingerprintManager.CryptoObject object = result.getCryptoObject();
   if(object != null){
       try {
           Signature signature = object.getSignature();
           Transaction transaction = new Transaction("<サーバーサイドで検証したい値>", 1, new SecureRandom().nextLong());
          signature.update(transaction.toByteArray());
          byte[] sigBytes = signature.sign();
            // サーバーサイドに送り、サーバーサイドで検証する
       } catch (SignatureException e) {
            // error handling
       }
    }
}

Confirm Credentialの実装

次は、アプリからOSのロック画面を利用する「Confirm Credential」のサンプルをみてみましょう。こちらもサンプルはGoogle Developersのサイトにある、Confirm Credentialで公開されています。AndroidStudioのFile->Import Sample…からも取得することができます。

図:6 Confirm Credentialアプリ

1.KeyGuardManagerの取得とスクリーンロック設定の確認

「Confirm Credential」は端末のロックスクリーンを利用するので、端末自体にロック設定がされていないと利用できません。「Fingerprint Authentication」と同じように、ロック画面を制御するKeyguardManagerのisKeyguardSecure()メソッドでロック画面に指紋やパスワードが設定されているか事前に確認しておきます。

KeyguardManager mKeyguardManager = (KeyguardManager)context.getSystemService(KeyguardManager.class);
if (!mKeyguardManager.isKeyguardSecure()) {
 // ロック画面に指紋設定やパスワードなどを設定するようにユーザーにメッセージング
 // 購入ボタンの非活性など
}

2.鍵の生成

対称鍵を生成して、対称鍵の利用時に認証を要求するように設定します。

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

   keyGenerator.init(new KeyGenParameterSpec.Builder("<任意のKey名>",
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true) // 鍵を利用するときに認証を要求
                    .setUserAuthenticationValidityDurationSeconds(<任意の認証間隔(秒)>) // 次に認証を要求するまでの秒数
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                    .build());
   keyGenerator.generateKey();
 } catch (NoSuchAlgorithmException | NoSuchProviderException
                | InvalidAlgorithmParameterException | KeyStoreException
                | CertificateException | IOException e) {
      // error handling
 }

KeyGenParameterSpec.BuilderのsetUserAuthenticationRequired()メソッドにtrueを設定することで鍵を利用するときに認証を要求する設定を行っています。この設定が行われた鍵は、利用のたびに認証済みかどうかのチェックが入ります。また、setUserAuthenticationValidityDurationSeconds()メソッドで、認証間隔を設定しているところにも注目してください。ユーザーに適切な時間間隔で再度認証を走らせると良いでしょう。

3.認証のハンドリング

「Confirm Credential」の認証を行います。アプローチとしてはシンプルに、認証設定をしている鍵を利用する処理を行い、認証されていなかった場合はUserNotAuthenticatedExceptionが発生するのでそれをcatchし、ロック画面に対して、認証処理を委譲するだけです。認証処理の委譲は、KeyguardManagerのcreateConfirmDeviceCredentialIntent()メソッドでIntentを生成し、ActivityのstartActivityForResult()メソッドで委譲します。

〜省略〜
try {
    // KeyStoreを利用する処理を書く
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
    keyStore.load(null);
    SecretKey secretKey = (SecretKey) keyStore.getKey("<任意のKey名>", null);
    Cipher cipher = Cipher.getInstance(
            KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
                    + KeyProperties.ENCRYPTION_PADDING_PKCS7);
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    cipher.doFinal(<適当なバイト配列>);

    // 問題なければ認証後の処理を実行

} catch (UserNotAuthenticatedException e) {
    // 認証されていないのでロックスクリーンに認証処理を委譲
    Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
    if (intent != null) {
       startActivityForResult(intent, <任意のリクエストコード>);
    }
} catch (KeyPermanentlyInvalidatedException e) {
    // error handling
} catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
        CertificateException | UnrecoverableKeyException | IOException
        | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
    // error handling
}
〜省略〜

委譲した処理の結果は、ActivityのonActivityResult()メソッドで受け取り、resultCodeで処理ステータスを確認し、処理を振り分けます。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == <任意のリクエストコード>) {
        if (resultCode == RESULT_OK) {
            // 認証成功。認証後の処理
        } else {
            // ステータスに応じてハンドリング
        }
    }
}

指紋認証の使いドコロ

指紋認証はこれまでのパターンロックやPINロックなどと比べて非常にセキュアでかつ簡単な認証方法です。うまく使いこなすことによって、認証時にユーザーの無駄が省ける大きなメリットがあります。新しいAPIですので、今から対応しておくと、Play Storeでしっかりとした存在感を出すことができると思います。

今回、「Fingerprint Authentication」と「Confirm Credential」という2種類の方法を紹介しました。これらの使い分けのポイントはどのようなところになるのでしょうか?

「Fingerprint Authentication」は開発者側で指紋認証をフル活用できるのが魅力です。UIの面でアプリと認証画面に親和性をもたせ、より細かなUXを高めることを考えながら実装する場合には向いているでしょう。一方で「Confirm Credential」は端末のロック画面、つまりOSに処理委譲するという面で、アプリ側の実装は非常に簡単です。シンプルにアプリ内の機能にロックをかけたい用途などには向いていると思います。用途を意識しながらうまく使い分けましょう。

最後に

今回はAndroidの指紋認証について調べてみました。最初に述べたように、今後のアプリ開発のトレンドは、いかにユーザーとエンゲージメントできるかという方向に向かいつつあります。Androidは特に技術トレンドの変化が激しく、新しい機能がどんどん入ってきます。アプリの内容でうまくユーザーの課題を解決する方法もあれば、今回のようにOSの新しい機能でいち早くアプリの価値を向上させていく方法もあると思います。

ユーザーにしっかり価値を届け、多くの人に受け入れられるアプリを作って行きましょう。

Advent Calendarの16日目はこれまでです。それでは良いお年を!

参考文献

※ Google、Nexus、Androidは、Google Inc.の登録商標または商標です。

Yahoo! JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。

  • このエントリーをはてなブックマークに追加