中安拓也のブログ

プログラミングについて書くブログ

iOSのFace ID, Touch IDを使用したログイン機能を設計する - Part2

はじめに

以前、iOSのFace ID, Touch IDを使用したログイン機能を設計する - 中安拓也のブログ という記事を書いたんだけど、実装してて辛かったり、そもそもセキュリティ的にマズそうだったので、色々と設計を変えた。

前回の設計

前回した設計なんですが、だいたいこういう流れで、Face ID, Touch IDを実施するつもりでした。

初回ログイン

  1. UDIDを発行して、iOSデバイスのキーチェーンに格納
  2. ユーザーがログイン画面にメールアドレスとパスワードを入力してログイン
  3. この時、発行したUDIDもサーバーに送る
  4. 「UDID」と「メールアドレス・パスワード」を関連付けてDBに保存
  5. これで次回以降は、Face ID, Touch IDによる認証が有効になる

Face ID, Touch IDによるログイン

  1. Face ID or Touch IDを実行して成功する
  2. iOSデバイスのキーチェーンからUDIDを取り出す
  3. UDID+固定文字列をHMAC-SHA256形式でハッシュ化
  4. ハッシュと現在時刻を連結させたものを、AES256で暗号化、暗号化した結果をBASE64Encodeするこれをシグネチャとする
  5. シグネチャをサーバーに送る
  6. サーバー側でBASE64Decode、AES復号化、送られてきた現在時刻から10分以上経過していないことを確認する
  7. ハッシュが一致することを確認する
  8. UDIDが正しく登録されているか確認する
  9. 認証処置完了、認証トークンを発行する

前回の設計を実際に実装してみてキツかった点

サーバサイド(Java)で復号化するのがキツイ

フロントエンド(Angular/TypeScript)で暗号化するのは、crypt-jsを使うことで簡単にできたんだけど、サーバサイド(Spring/Java)で復号のコードを書くのが、量多くて辛い。(参考サイト

暗号化と復号化に使用する共通鍵について考慮してなかった

ハッシュ化したりAESで暗号化するには、共通鍵が必要になるわけだけど、共通鍵にどのような仕組みを使うか考えてなかった。固定のパスフレーズを共通鍵にすると、それ自体がセキュリティホールになるし、どうしよ...

前回の設計のセキュリティ的にまずい点

UDIDじゃなくて、UUIDだよね?

端末固有の番号であるUDID(Unique Device Identifier)については、Cordovaのプラグインを使用して、取得する予定だったけど、そもそもUDIDは、本アプリから以外でも入手可能な番号だから、認証で使うのはまずいよね。。。せめて、UUID(Universally Unique Identifier)にしよう...

前回の設計のおさらい

UUIDじゃなくて、UDIDを使っているのは単に自分の勘違いなので、無視するとしても、前回の設計では下記の点を念頭に置いていました。

  • ジェイルブレイク対策
    • Keychainにただメールアドレスとパスワードを保存する方式だと、Keychainのセキュリティが破られた時に不安なので、わざわざUUIDを暗号化してから、Keychainに入れる方式にしていた
  • 盗聴対策
    • 通信途中にデータを奪われても、暗号化されているから安心!

今回の設計

前回の設計は、セキュリティ的には優れていたにせよ、実装が大変なので、単純にiOSのKeychainにメールアドレスとパスワードを格納して、Face ID/Touch ID 認証が成功したら、メールアドレスとパスワードを投げる設計に変更した。
通常のログイン方式でも、盗聴対策してないわけだし、ジェイルブレイクについては、端末を持っているユーザの自己責任という考え方で、設計変更については落着した。

【JavaScript】Promise チェーンで、戻り値がPromiseのメソッドをつなぐ

戻り値がPromiseのメソッドを順番に実行し、前の処理の結果を次の処理で使いたいという場合があります。そんな時は、Promise チェーンを使いましょう。

バージョン情報

  • Angular: 5.0.1
  • TypeScript: 2.4.2

サンプルコード

下記のコードは、iOSのFace ID/Touch ID認証に関するコードになります。呼び出しているメソッドの戻り値が全てPromiseなので、Promise チェーンで繋げることができます。

.thenのコールバック処理の中で呼びだすPromiseが戻り値のメソッドをreturnしているところがポイント

// 戻り値がPromiseのメソッド
this.keychainTouchId.isAvailable()
    .then((res: any) => {
        console.log(res);
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId.has(BioAuthService.KEY_A);
    }).then((res: any) => {
        console.log(res);
        // 戻り値がPromiseのメソッド
        return this.keychainTouchId
            .verify(BioAuthService.KEYCHAIN_KEY, `ロックを解除してください`);
    }).then((res: any) => {
        console.log(res);
        this.password = res;
        // 戻り値がPromiseのメソッド
        return this.storage.get(BioAuthService.KEY_B);
    }).then((res: any) => {
        console.log(res);
        this.userId = res;
        const params = {
            loginId: this.userId,
            password: this.password,
            deviceToken: null
        };
        this.action.login(params);
    }).catch((error: any) => {
        // catchは一つでよい
        console.error(error);
    });

Promiseの処理を順番に実行していく方法として、ネストしていくやり方もありますが、ネストが深くなっていくとコードが読みづらくなるので、コールバック地獄という言葉に代表されるように、アンチパターンであるとされています。

参考サイト

Promiseを使う - JavaScript | MDN

【Angular】お互いにDIしあっている(循環参照)と発生するエラー「Uncaught Error: Can't resolve all parameters for XXXService: (?, [object Object], [object Object]).」

バージョン情報

  • Angular: 5.0.1

エラー内容

AサービスクラスにBサービスクラスをDI、BサービスクラスにAサービスクラスをDI みたいなこと(循環参照状態)をしていた結果、タイトルのエラーが発生

Uncaught Error: Can't resolve all parameters for XXXService: (?, [object Object], [object Object]).
    at syntaxError (webpack-internal:///306:684)
    at CompileMetadataResolver._getDependenciesMetadata (webpack-internal:///306:15764)
    at CompileMetadataResolver._getTypeMetadata (webpack-internal:///306:15599)
    at CompileMetadataResolver._getInjectableMetadata (webpack-internal:///306:15579)
    at CompileMetadataResolver.getProviderMetadata (webpack-internal:///306:15939)
    at eval (webpack-internal:///306:15850)
    at Array.forEach (<anonymous>)
    at CompileMetadataResolver._getProvidersMetadata (webpack-internal:///306:15810)
    at CompileMetadataResolver.getNgModuleMetadata (webpack-internal:///306:15378)
    at CompileMetadataResolver.getNgModuleSummary (webpack-internal:///306:15216)

サービスクラスをもう一つ追加して、循環参照を解消した

Aサービスクラスから、いくつかメソッドをCサービスクラスに分離して、
Bサービスクラスには、Aサービスクラスでなく、CサービスクラスをDIするようにした(循環参照を解消)結果、解決した。

参考サイト

Can't resolve all parameters for User: (?, [object Object]). · Issue #34 · ionic-team/ionic2-starter-aws · GitHub

iOSのFace ID, Touch IDを使用したログイン機能を設計する

業務でFace ID, Touch IDを使ったログイン機能を実装することになったので色々と調べた。

作成予定のシステム

  • Ionic(JavaScript, Angular)で作成されたハイブリットモバイルアプリケーション
  • サーバーサイドはJava(Spring)
  • 通常のログインには、メールアドレスとパスワードを使用
  • Face ID, Touch IDによる認証が成功した時には、メールアドレス/パスワード の入力をスキップした状態でログインできる

Face ID, Touch IDに対する誤解について

Face ID, Touch IDによる認証が成功すると、認証した人を特定できるユニークなIDを発行してくれるイメージを勝手に持っていたが、そんな機能はなかった。Face ID, Touch ID のAPIが提供する情報は、あくまで認証が「成功したかどうか」のみでIDを発行したりはしない。

処理詳細

過去に類似の実装経験がある人に話を聞いた結果、下記のイメージで実装する予定。

初回ログイン

  1. UDIDを発行して、iOSデバイスのキーチェーンに格納
  2. ユーザーがログイン画面にメールアドレスとパスワードを入力してログイン
  3. この時、発行したUDIDもサーバーに送る
  4. 「UDID」と「メールアドレス・パスワード」を関連付けてDBに保存
  5. これで次回以降は、Face ID, Touch IDによる認証が有効になる

Face ID, Touch IDによるログイン

  1. Face ID or Touch IDを実行して成功する
  2. iOSデバイスのキーチェーンからUDIDを取り出す
  3. UDID+固定文字列をHMAC-SHA256形式でハッシュ化
  4. ハッシュと現在時刻を連結させたものを、AES256で暗号化、暗号化した結果をBASE64Encodeするこれをシグネチャとする
  5. シグネチャをサーバーに送る
  6. サーバー側でBASE64Decode、AES復号化、送られてきた現在時刻から10分以上経過していないことを確認する
  7. ハッシュが一致することを確認する
  8. UDIDが正しく登録されているか確認する
  9. 認証処置完了、認証トークンを発行する

Appleのレビューガイドライン

Appleのレビューガイドラインで、Face IDについての記載があったので転載する。

https://developer.apple.com/jp/app-store/review/guidelines/

2.5.13 顔認証でアカウントを認証するアプリケーションには、ARKitやその他の顔認証テクノロジーではなく、必ずLocalAuthenticationを使用する必要があります。また、13歳未満のユーザーに対しては、必ず代替の認証方法を用意する必要があります。

考えた点・補足

  • 端末のKeychainに直接メールアドレスとパスワードを保管するのは避けた
  • シグネチャは、改ざん防止ではなく、認証のために利用している
  • 顔・指紋認証に3回失敗すると、通常ログインに切り替える〜みたいな処理は、API(ライブラリ)側で勝手にやってくれる

結局違う設計にしました。この記事参照

IonicでiOSの生体認証(Face ID, Touch ID)を扱う

モチベーション

業務でFace ID, Touch ID対応のiOSアプリを作成することになったので、触っておきたい。

バージョン情報

Angular(JavaScriptのWebフレームワーク)ベースの、ハイブリットモバイルアプリ用フレームワークである「Ionic」を使っています

  • ionic-angular@3.9.2
  • Angular@5.2.10

cordova-plugin-touch-idをインストール

Ionicアプリ上で、Face ID / Touch IDの機能を使うには、cordova-plugin-touch-idのインストールが必要になる。

$ ionic cordova plugin add cordova-plugin-touch-id
$ npm install --save @ionic-native/touch-id

cordova-plugin-touch-idをインストールすることで、Face ID / Touch IDの両機能を利用することが可能になる。

AppModuleにプラグインを追加

  • TouchIDモジュールをインポート
  • providers: []配列にTouchIDモジュールを追加

src/app/app.module.ts:

・・・

import { TouchID } from '@ionic-native/touch-id';

@NgModule({
  ・・・
  providers: [
    ・・・
    TouchID,
    ・・・
  ]
})
export class AppModule {}

これで、環境構築は完了🍺🍺🍺

実装

import { Component } from "@angular/core";
import { TouchID } from "@ionic-native/touch-id";

@Component({
  selector: "page-home",
  template: `<ion-content padding>
              <button (click)="login()" ion-button>Login</button>
            </ion-content>`
})
export class HomePage {
  constructor(private touchId: TouchID) {}

  login() {
    // Touch ID, Face ID 対応端末かどうか確認する
    this.touchId
      .isAvailable()
      .then(
        res => console.log("TouchID is available!"),
        err => console.error("TouchID is not available", err)
      );

    // 確認ダイアログを出したあと、Touch ID, Face ID 認証を実行する
    this.touchId
      .verifyFingerprint("Scan your fingerprint please")
      .then(res => console.log("Ok", res), err => console.error("Error", err));
  }
}

動作確認

シミュレーター上での動作確認について。シミュレーターを起動して、Hardware -> Face ID -> Enrolled を選択することで、シミュレーター上でもFace ID, Touch IDの動作確認を実施することができる。

f:id:l08084:20180602190520p:plain

参考サイト

【メモ】Xcodeでエラー「Showing All Messages clang: error: linker command failed with exit code 1 (use -v to see invocation)」

Xcodeで実機ビルドをしたタイミングで、下記のエラーが発生した。

Showing All Messages
clang: error: linker command failed with exit code 1 (use -v to see invocation)

バージョン情報

  • Xcode: 9.3
  • cordova-plugin-touch-id: 3.3.1

原因

cordova-plugin-keychain-touch-idcordova-plugin-touch-idの両方を入れたことがおそらく原因だと思う。
cordova-plugin-keychain-touch-idのインストールをやめて、cordova-plugin-ios-keychainに変更したところ、解決した。

機能が一部、重複しているライブラリを入れたことが原因なのか....?

  • cordova-plugin-touch-id

    • Ionicアプリ上で、Face IDとTouch IDによる認証機能を呼び出すライブラリ
  • cordova-plugin-keychain-touch-id

    • Ionicアプリ上で、Face IDとTouch IDによる認証機能と、Keychainへのパスワード登録機能を可能にするライブラリ

iOS端末(iPhone/iPad)のUDIDを確認する方法

Apple開発者アカウントにデバイスを登録するときに必要になる、デバイスID(UDID)の確認方法について

バージョン情報

  • MacBook Pro(15-inch, 2017)
  • macOS High Sierra: 10.13.4
  • iTunes: 12.7.4
  • iPhone 8 plus(iOS 11.1.2)
  • Xcode: 9.3

UDIDの確認方法

デバイス番号(UDID)を調べる方法は、複数存在している。今回は、そのうちのiTunesを使う方法とXcodeを使う方法を紹介する

iTunesで調べる方法

この方法だと、iPhone(iPad)が強制的に同期されてしまうので、業務でテスト端末を使うときなどの場合は避けたほうがいいかもしれない

f:id:l08084:20180519164911p:plain

  1. Mac上でiTunesを起動する
  2. MacにiPhone(iPad)を接続する
  3. シリアル番号が表示されているところをクリック
  4. iPhone(iPad)のUDIDが表示される

Xcodeで調べる方法

f:id:l08084:20180519170208p:plain

  1. Mac上でXcodeを起動する
  2. 「Window」>「Devices」コマンドを実行
  3. iPhone(iPad)をMacに接続する
  4. Debicesタブを選択する
  5. identifierの部分にデバイス番号(UDID)が表示される

参考サイト

日本語ドキュメント - Apple Developer