FileMakerでセキュアーにパスワード認証をする

> **追記**
> この記事に記載されている関数は古くなりました。
> セキュリティーを強化したバージョンを新しく作成し、記事を公開していますのでそちらをご利用ください。
> [FileMakerでセキュアーにパスワード認証をする（2） \- Qiita](https://qiita.com/HAZI/items/3b94366cc8c0993fa40d)

[FileMaker Advent Calendar 2018 \- Qiita](https://qiita.com/advent-calendar/2018/filemaker) 11日目の記事です。
遅くなってすみません。急遽書くことになったので11日の枠が当日深夜になっても空いていたので、エントリーさせて頂きました。

## 経緯

ちょうど個人的に仕事で FileMaker でのユーザー認証を行う際パスワードのハッシュ化について考えているところで、FileMaker AdventCalendar に類似する記事を発見し、その記事に対してコメントで指摘させていただきました。

[【FileMaker】パスワードの保存について考える \- Qiita](https://qiita.com/jaga30/items/6b39c814381696839c37)

私が書いたコメント内容は「パスワードを暗号化して(復号可能な状態で)管理するのはやめたほうがいい」という内容でした。

今回の記事はそのコメントの補足として具体的どうパスワードをデータベースに保存するべきなのかを書きたいと思います。

**ただ、私自身もセキュリティーの専門家ではありません。記事を書くにあたって色々調べた結果をまとめた内容となっておりますのでその点ご留意ください。**

------

## はじめに

本記事は、FileMaker にもともと備わっているアカウント機能とは別に、独自でデータベースにユーザー情報を保存し認証を行うスクリプトを作成する方法について書いたものです。

パスワードは昔からそのままで保存してはいけないというのは有名な話なのでご存知の方も多いかと思います。

では、どうやって保存するべきか。というのは長年専門家たちによって考えられてきました。今現在は `ハッシュ化`、`ソルト`、`ストレッチング` の3つを行うのが一般的となっています。

今回はこの3つがなんなのかを簡単に説明し、次に FileMaker で実装するにはどうしたらいいのかといった順に説明していきたいと思います。

## ハッシュ化とは

「ハッシュ」はプログラミングの世界の色々なところで使われているので、混同しないように気をつけてください。ここでいうハッシュはJANやクレジットカードの下一桁で使われているチェックサムのような物を指します。

ハッシュはあるアルゴリズムを元に与えられたデータを要約します。要約といってもいわゆるZIP圧縮などとは違い、どの容量のデータでも常に同じ容量のデータを返します。

昔から有名なアルゴリズムは MD5 や SHA-1 などがあり、MD5の場合は下記のような結果を返します。

```shell
MD5 ("AAAAA") = f6a6263167c92de8644ac998b3c4e4d1
```

_※現在はパスワードのハッシュ化に  MD5 や SHA-1 を使うことは脆弱とされていますので使わないように気をつけてください。_

この計算結果である文字列 `f6a6263167c92de8644ac998b3c4e4d1` を「ハッシュ値」と言い、ハッシュ値を返す関数を「ハッシュ関数」と言います。

**代表的なハッシュの特性**

- 小さいテキストでも、大きなファイルでもハッシュ値を計算できる
- すべて同じデータである限り必ず同じハッシュ値になる
- 少しでもデータが違えば異なるハッシュ値になる
- どんな大きさのデータでも戻り値の長さは常に一定
  - MD5の場合は 128bit (16進数で 32文字)
- 計算が高速
  - パスワードぐらいであれば一瞬
  - MD5の場合は 1GB のファイルで2秒程度

_※すべてのハッシュアルゴリズムがこの特性と同じとは限りません_

この特性を利用してダウンロードファイルのチェックサムとして使われることがあります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/2908/7c4573cd-f8f5-a3d8-5fed-b468ab02cdff.png)
[Ubuntu Desktop 日本語 Remixのダウンロード \| Ubuntu Japanese Team](https://www.ubuntulinux.jp/download/ja-remix) より

ダウンロードに失敗していたり、Torrent や他のミラーサイトからダウンロードしたファイルがオフィシャルサイトのものと一致しているかを確かめるために利用されています。

パスワードの保存で利用する場合には下記のように利用します。

登録時

- 新規登録時にパスワードの入力を行う
- パスワードはそのまま保存せずにハッシュ化を行って保存する

認証時

- パスワードの入力を行ってもらう
- ユーザIDが一致するユーザー情報を探す
- ユーザー情報に保存されたハッシュ値と、入力してもらったパスワードをハッシュ化した値が一致していれば認証完了

### ハッシュ値の非可逆性

MD5 や SHA-1 などの関数を使ったハッシュ値から元の値に戻すことは難しく、オフィシャルでそういった機能の提供はありません。ただし、"難しい" だけで不可能ではありません。

事前に文字数が少ないとわかっている場合には総当たりを行ったり、ある程度の文字数まではレインボー攻撃といった方法でハッシュ値を当てる方法はあります。

たとえば MD5 `033bd94b1168d7e4f0d644c3c95e35bf` はある大文字のアルファベット4文字からできたハッシュ値です。なので `AAAA` から `XXXX` までを順番に試していけば、元の値を推測できます。この総当たりの値を特殊な方法でデータベース化し、高速化したやり方がレインボー攻撃です。

また、本当に単純な値の場合は Google 検索でハッシュ値を入れるだけで答えを見つけることができます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/2908/faa8cb17-120d-1430-d1ec-b56fda7503c8.png)
[033bd94b1168d7e4f0d644c3c95e35bf \- Google 検索](https://www.google.co.jp/search?q=033bd94b1168d7e4f0d644c3c95e35bf) より

これらを防ぐ方法として、`ソルト`と`ストレッチング`があります。

## ソルトとは

ハッシュ値から値を推測することが可能という話をしましたが、これはあくまで元のデータが小さいデータの場合に限ります。

たとえば 1GB の動画データから算出された MD5 の ハッシュ値(128bit)から元の動画を推測し当てることができたらこれは革命的な圧縮技術と言えます。現状そういった利用がされてないことからも、ある一定の容量を超えると推測がほぼ無理ということがわかるかと思います。

かといって、パスワードを推測困難な文字数まで増やしてしまうと今度は人が覚えられなくなってしまいます。

そこで利用されるのがソルトです。

ソルトは「塩をふりかける」という意味から来ており、パスワードに塩をふりかけるように、文字列を追加する方法です。

具体的には `yamada` というユーザーのパスワードが `Password` だったとしたら、パスワードにメールアドレス `yamada@example.com` をつなげた `yamada@exapmle.com-Password` という文字列からハッシュ値を求めます。

こうすることでパスワード単体より大幅に容量を増やすことができ、レインボー攻撃などに対応することができるようになります。

さらに、ソルトがない場合同一のパスワードを設定した別のユーザーがいる場合値は同じになりますが、ソルトがユーザー毎に異なる場合これを回避できます。

もちろん、ソルト処理を行ったとしても元のパスワードが簡単なもので、ソルト処理の方法も漏れていた場合には総当たりで簡単に当てることができてしまいます。

パスワードに最低文字数や記号などを混ぜるように求められるのはこういった理由からです。

## ストレッチング

ストレッチングは、ハッシュ化をなん度も繰り返し行うことを指します。

基本的にはソルトと一緒に行うことが推奨されており、FileMaker の計算式で書くと下記のようになります。

```java
Let([
  rawPassword = "Password";
  salt = "yamada@example.com";
  hash = MD5(salt & rawPassword);
  hash = MD5(salt & hash);
  hash = MD5(salt & hash);
  hash = MD5(salt & hash);
  hash = MD5(salt & hash);
  hash = MD5(salt & hash);
  hash = MD5(salt & hash);
];
  hash
)
```

_※MD5 関数は FileMaker にはありません。あくまで仮の関数です_

例では 7回しかストレッチングを行なっていませんが、数千回〜数万回以上行うのが一般的となっています。こうすることによって、総当りで アルファベット+数字の組み合わせ 6文字パターンのハッシュ値を算出するのに、`61,474,519 × ストレッチング回数` 回 MD5 計算を行わなくてはならないことになり、値の推測が難しくなります。

参考までにパパッと書いたスクリプトを使って総当りで試してみたところ、6文字の記号なしのパスワード + ストレッチング1万回への総当たりは私のパソコンでは約40日かかるようです。

ただ、GPUを使った方法などいろいろな総当たりの高速化が進んでいるので、6文字以上、記号+数字+大文字+小文字の利用を推奨するのがいいでしょう。

### まとめ

以上の内容で `ハッシュ化`、`ソルト`、`ストレッチング` を組み合わせることで、データベースに保存された値から元のパスワードを推測することを困難にし、ログイン認証を行うことができます。

これができれば、流出へのリスクも軽減も当然ですが開発者自身がパスワードを容易に知る事も出来なくなり安心です。

------

## FileMaker での実装

> **追記**
> この記事に記載されている関数は古くなりました。
> セキュリティーを強化したバージョンを新しく作成し、記事を公開していますのでそちらをご利用ください。
> [FileMakerでセキュアーにパスワード認証をする（2） \- Qiita](https://qiita.com/HAZI/items/3b94366cc8c0993fa40d)

------

いよいよ FileMaker での実装の話です。

ハッシュ + ソルト + ストレッチング を行うことは FileMaker でも可能です。
実際に自作してみた関数が下記のものです。

### ハッシュ化関数

**FileMaker 16 以降で利用する場合**

```java:FileMaker16以降版
// RawPasswordToHash(rawPassword; algorithm; key; stretching)
//
// 引数 rawPassword をハッシュ化します
//
// 引数:
//   rawPassword: [String] 生のパスワード
//   algorithm: [String] ハッシュ化アルゴリズム。CryptAuthCode 関数の algorithm 引数参照
//        例: `"SHA512"`
//   key: [String] Salt 用の文字列、ユーザー毎にユニークなランダム値推奨
//        例: `Get(UUID) &"-"& Get(UUID) &"-"& Get(UUID) &"-"& Get(UUID)`
//   stretching: [Integer] ストレッチング回数
//
// 戻り値タイプ: テキスト
// 戻り値:
//  => 正常時: Base64Encodeされた HMAC (Keyed-Hash Message Authentication Code)
//  => エラー時: "?" (アルゴリズムの指定が誤っている場合など)

Let([
  ~stretching = stretching - 1;
  ~rfcNumber = 4648;
  ~hash = CryptAuthCode (rawPassword ; algorithm ; key)
];
  Case(
    ~hash = "?"; ~hash;
    ~stretching < 1; Base64EncodeRFC(~rfcNumber; ~hash);
    RawPasswordToHash(~hash; algorithm; key; ~stretching)
  )
)
```

※`CryptAuthCode` を使っている関係上 FileMaker 16 以降でないと動作しません。

**実装に関して**

- ストレッチングの為にカスタム関数の再帰ループを使ってます。
  - 再帰ループを使っているので FileMaker の仕様上 49,999回 までしかストレッチングできません。
  - 次期バージョンで採用予定の `while` 関数があればもう少しわかりやすくかけるのですが…。
- ソルトは `CryptAuthCode` を代用しています。
  - HMAC を生成する関数ですがソルトの実装に利用できます (参考： [ソルトとハッシュ関数だけでパスワードをハッシュ化するのが微妙な理由 \- Qiita](https://qiita.com/yyu/items/2196b05208fb730e5af0))。
  - 独自に実装することもできますが、できる限りシンプルで確実に実装でき、再現性を高める為にあえて独自実装はしてません。
  - `CryptAuthCode` は戻り値がオブジェクトなので、テキストフィールドに保存できるよう `Base64EncodeRFC` でエンコードしています。
  - テキストとして保存することで値の比較が容易になります。
- algorithm 引数には `CryptAuthCode` で利用できるアルゴリズムを指定します。
  - 現在のところ `SHA512` を指定すれば大丈夫ですが、将来的に脆弱性が発見された場合これを変更する必要が出てくる場合があります。

**FileMaker 15〜13 で利用する場合**

```java:FileMaker15-13版
// RawPasswordToHash(rawPassword; salt; stretching)
//
// 引数 rawPassword をハッシュ化します
//
// 引数:
//   rawPassword: [String] 生のパスワード
//   salt: [String] Salt 用の文字列、ユーザー毎にユニークなランダム値推奨
//   stretching: [Integer] ストレッチング回数
//
// 戻り値タイプ: テキスト
// 戻り値: MD5 ハッシュ値

Let([
  ~stretching = stretching - 1;
  ~hash = GetContainerAttribute(salt & rawPassword; "MD5")
];
  Case(
    ~stretching < 1; ~hash;
    RawPasswordToHash(~hash; salt; ~stretching)
  )
)
```

- FileMaker 15以前の場合 MD5 以外のハッシュ値を求める方法が見当たりませんでした。
- MD5 はパスワードのハッシュ化として利用するのには脆弱性が指摘されていますが、これ以外の方法がないので、できる限りストレッチングを多くすることで対応してください。
- もし、FileMaker 16 への移行を考えている場合は引数を FileMaker 16 移行のものと同じにしておくといいでしょう。 その際新しい関数にしてもアルゴリズムが `MD5` の場合 FileMaker 15用と同様にハッシュ値になるように修正する必要があります。

```java:FileMaker15-13版-16版互換
// RawPasswordToHash(rawPassword; algorithm; key; stretching)
//
// 引数 rawPassword をハッシュ化します
//
// 引数:
//   rawPassword: [String] 生のパスワード
//   algorithm: [String] "MD5" のみ指定可能
//   key: [String] Salt 用の文字列、ユーザー毎にユニークなランダム値推奨
//   stretching: [Integer] ストレッチング回数
//
// 戻り値タイプ: テキスト
// 戻り値:
//  => 正常時: MD5 ハッシュ値
//  => エラー時: "?" (アルゴリズムの指定が誤っている場合など)

Let([
  ~stretching = stretching - 1;
  ~hash = GetContainerAttribute(key & rawPassword; "MD5")
];
  Case(
    algorithm <> "MD5"; "?";
    ~stretching < 1; ~hash;
    RawPasswordToHash(~hash; key; ~stretching)
  )
)
```

### データベースへの保存方法

では、実際のデータベースへの保存方法です。サンプルファイルがあればよかったのですが、時間の関係上今回はよういできませんでした。

必要最低限のフィールドは下記の通りです。

- ユーザーID
- パスワードハッシュ
- パスワードハッシュ化アルゴリズム
- ストレッチング回数
- ソルト

**ユーザーID**

ユーザーIDはユニークな値である必要があります。
ログイン時に利用するので、入力時手間がないようにアルファベットなどに限定したほうがいいかもしれません。

**パスワードハッシュ**

上記のパスワードハッシュ化の関数を利用して得た値を保存します。

**パスワードハッシュ化アルゴリズム**

ハッシュ化に利用したアルゴリズムを保存します。
スクリプトに直接書いてしまってもいいのですが、将来そのアルゴリズムを変更する必要が出てくる可能性があります。そういった、複数のアルゴリズムが混在する場合に必要です。

**ストレッチング回数**

アルゴリズム同様にストレッチングを行なった回数も変更の可能性があるので記録しておきましょう。FileMaker 18 で `while` 関数が実装されループ処理の軽量化ができるようになった場合回数を増やしたくなるかもしれません。

**ソルト**

ソルトは基本的にランダムな値を保存して利用します。FileMaker では `Get(UUID)` を使うのがいいでしょう。

ただし、HMAC のキーはハッシュ値より長い方が良いようです。

参考：

>  HMAC において使用される鍵は、どのような長さのものでもよい（B バイトより長い鍵は最初に H でハッシュされる）。しかし L バイトより短いものは、その関数のセキュリティ強度が減少するため使用してはならない。L バイトより長い鍵も受け入れられるが、その余分の長さは、あまり関数の強度を増さないだろう。（その鍵の乱数的性質が弱いとみなされる場合には、より長い鍵が勧められるかもしれない。）
> [HMAC: Keyed\-Hashing for Message Authentication](https://www.ipa.go.jp/security/rfc/RFC2104JA.html)

なので、SHA512 を利用する場合 512bit のランダム値を利用するのが理想的です。

`Get(UUID)` は 128bit なので、4つの `Get(UUID)` を使っておくと安全でしょう。

FileMaker 17 以降の場合は `Get (UUID 番号)` を利用することもできます。こちらは 192 bit となってます。

### ハッシュアルゴリズムなどの変更への対応

上でも述べたように、ハッシュアルゴリズムなどの変更を行う必要が出てくる場合があります。

でも、アルゴリズムの変更には元のパスワードが必要です。元のパスワードがデータベースに保存されていない関係上難しく思うかもしれませんが、元のパスワードを入手する方法があります。

それは、ユーザーがログインする瞬間です。

ログイン処理での認証後に元のパスワードを新しいアルゴリズムでハッシュ化し、データベースにアルゴリズム情報と共にハッシュ値を保存するようにします。

初期の段階ではこの機能は不要なので、必要になったら実装するといいでしょう。

## 終わりに

以上でパスワードのハッシュ化と、その FileMaker での実装方法は終わりです。

思ったよりも長くなってしまい、サンプルファイルを作る時間すらできませんでした。

HMAC を使うのは妥当なのか、ソルトに変わる暗号化キーはどの程度の長さがいいのかなど、投稿するにあたって確かな情報を探しつつ大体間違いないだろうというところまで情報を集めるのに苦労しました。

基本的にはハッシュ化を行うだけでかなり違うので、細かい実装は後回しにしてソルトやアルゴリズムも固定ではじめて行き、後々機能を増やしていくといった実装方法も可能ですので、ぜひパスワード認証系を独自に実装している人は導入を行なってみてください。

**参考**

- [パスワードはハッシュ化するだけで十分? \| NTTデータ](http://www.nttdata.com/jp/ja/insights/blog/20170914.html)
- [2018年のパスワードハッシュ \- Qiita](https://qiita.com/rana_kualu/items/3ef57485be1103362f56)

## 追記

大事な部分を書き忘れておりました…

### 独自実装の必要性について

FileMaker のアカウント機能はファイルを横断して共有することが出来ず、アカウント情報を共有する場合には同期機能を自作する必要があり、ファイル数が多いとこれを行うのが難しいという問題があります。
実装出来たとしてもユーザーの生のパスワードを保存する必要があるなどセキュリティ上のリスクも発生します。

これを回避するために FileMaker Server の「外部サーバーアカウント」を使った認証方法があります。
OpenDirectory や Amazon, Google, Microsoft の OAuth を使って認証することが可能です。
どちらにせよアカウント情報の同期機能は作成する必要があるのですが、パスワードを保存する必要がなくなります。

もし、そういったものが導入できる環境であればそれらを使ったほうが安全で確実でしょう。

### 実際の使い方

では、今回のユーザー認証をどう使うかというと、ユーザー認証後に`再ログイン`スクリプトステップを使います。
このスクリプトステップを使うことで、FileMaker のアカウント機能で認証を行います。

部署ごとなどに FileMaker アカウントを発行しておき、ユーザー情報と照らし合わせて再ログインを行います。
これらのアカウント名、生パスワードをスクリプトやデータベースに保存する必要がありますが、個人の生パスワードの保存は必要なくなります。

個人のパスワードは大事な Google アカウントや Apple ID などと同じものにすることが多いと思われます。
これらの流出はかなりの打撃になるので、細心の注意を払う必要があります。

今回の記事にあるような内容の目的は主にそれです。

### FileMaker Cloud 2

FileMaker Cloud 2 (FileMaker Server 18 Cloud版) では、認証系が `FileMaker ID` というもので一括管理できるような情報がありました。
断片的な情報しかないのでなんとも言えないのですが、こういった実装が不要になりセキュアーになってくれることを願います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/2908/7b64145d-afaa-23dc-0f1e-49daab06dc51.png)
[FileMaker Product Roadmap | FileMaker Community](https://community.filemaker.com/community/platform/product-roadmap) より