【Xamarin】SecKeyChain備忘録(基本的なパスワードの取扱)

`SecKeyChain`周り、触れば触るほどややこしいので分かったこと取り敢えずまとめ。

- アプリケーションパスワード
- インターネットパスワード

の形で、ユーザーが入力したパスワードを端末に保存できる。

※多分他にもあるっぽいですが今の所この2つしか活用出来てません……しかも異なる仕様だと思ってた部分が同じだったので(後述)違いもあまりわからない。。

####パスワード保存する
何はともあれパスワードを保存する

```c#
SecKeyChain.AddGenericPassword(serviceName, accountName, password);
SecKeyChain.AddInternetPassword(serverName, accountName, password);
```

`AddGenericPassword`はアプリケーションパスワードとして、`AddInternetPassword`はインターネットパスワードとしてキーチェーンアクセスに保存する。

`serviceName`,`serverName`,`accountName`はいずれも`string`。
`serviceName`と`serverName`の違いこそあれど、結局はキーチェーンアクセスに表示される名前なのであまり差異はない(と思う)。

ちなみにブラウザなどで登録したパスワードについてはこの`serverName`にWebサイトのホスト名が入るので、アプリケーションの場合はそのままアプリ名を`serviceName`に入れれば良い。

`password`のみ`byte[]`となるので、事前にbyte配列への変換が必要となる。

```c#
var bytePassword = Encoding.UTF8.GetBytes(password);
```

####パスワードを取り出す
アプリを再起動した時に前回の値が入力されている……みたいなやつ作るときのための、パスワードの取り出し方。

```c#
// 何かしらのアプリケーションパスワードを取り出す想定

// パスワードを取り出すためのクエリ
var passwordQuery = new SecRecord(SecKind.GenericPassword)
{
    Service = serviceName,
    Account = accountName
};
```
`SecRecord`オブジェクトを作成し、キーチェーンアクセスからパスワードを取り出すためのクエリを作成する。
引数にはパスワードの種類を示す`SecKind`列挙型の値を代入し、レコードを特定するための情報をブロック内に記述する。

ややこしいのはこの`SecRecord`オブジェクト自体は**パスワードそのものではない**ということ。
取り出すにはこの発行したクエリを元に`QueryAsData`メソッドを使う。

```c#
// QueryAsData()メソッドはNSData型を返すので文字列に変換する
NSData passwordData = SecKeyChain.QueryAsData(passwordQuery);
string password = passwordData.ToString();
```
`QueryAsData()`は引数として受け取った`SecRecord`が持つ情報に一致したキーチェーンアクセスのレコードを`NSData`型で一つだけ返してくれる。最大数を指定することで一致するものを複数返してくれるオーバーロードも存在するっぽい。

保存するときには`byte[]`型に変換したが、取り出す場合はこれで平文のパスワードが取り出せる。

####パスワードを更新する
ユーザーがパスワードを書き換えた際などに、新規追加時と同様に`AddGenericPassword()`を使うと上手くいかない。

これはキーチェーンアクセスの仕様上**重複するレコードは作れない**から。
→ 恥ずかしながらこれに気付くのに結構時間をかけてしまいました。。`Server`が同じでも`Account`が異なるレコードは作れるので`InternetPassword`だけの話かと何となく思っていたら、`GenericPassword`も同様だったので、余計にこの2つの違いがわからなくなる。。

因みに厄介なことに(?)重複するレコードを作ろうとしてもエラーなどは出ず、只々キーチェーンアクセスに更新がかからないだけとなる。
ただ確かめる術はあるようで、キーチェーンアクセス操作系のメソッドが返してくれるHTTPステータスコードのような`SecStatusCode`という列挙型オブジェクトを参照する。

```c#
var bytePassword = Encoding.UTF8.GetBytes("password");
var byteNewPassword = Encoding.UTF8.GetBytes("newPassword");

// 返り値のSecStatusCodeを参照するとレコードの追加がうまくいったかどうかが分かる
SecStatusCode code;

// 成功した場合はSuccessが返る
code = SecKeyChain.AddPassword(serviceName, accountName, bytePassword);
Debug.WriteLine(code) //=> Success

// 失敗時はそれに応じたコード
code = SecKeyChain.AddPassword(serviceName, accountName, byteNewPassword);
Debug.WriteLine(code) //=> DuplicateItem
```
この`SecStatusCode`、めちゃくちゃ沢山種類があるので(ドキュメント参照)`Success`とそれ以外で分岐させるのが無難……かもしれない。

本題に戻ると、ユーザーの入力などによって特定のレコードのパスワードだけを更新したい、という場合は`SecKeyChain.Update()`メソッドを使う。
→ これを想定する場合は`DuplicateItem`による分岐を考えても良い。と思う。

`Update`メソッドは2つの`SecRecord`を引数として受け取る。これらはそれぞれ既存のレコードを探すためのクエリ、新しい値を表すクエリとして使われる。

```c#
// 対象のレコードを特定するためのクエリ
var passwordQuery = new SecRecord(SecKind.GenericPassword)
{
    Service = serviceName,
    Account = accountName
};

// 新しいパスワードを持つクエリ
var newPasswordQuery = new SecRecord(SecKind.GenericPassword)
{
    ValueData = NSData.FromArray(byteNewPassword)
};

// Update()メソッドもSecStatusCodeを返してくれる
SecStatusCode code = SecKeyChain.Update(passwordQuery, newPasswordQuery);
Debug.WriteLine(code) //=> Success
```
こうしてみるとSQLに似通っている部分がありますが、クエリをオブジェクトとして作るって所が自分には少しややこしかった。。(しかも`SecRecord`って名前なのが余計に……)

因みに`SecRecord`オブジェクトにおいて実際のパスワードの情報が代入されるのは上記でも示している通り`ValueData`という`NSData`型のプロパティ。
パスワードはバイト配列として保存されているので、これまで通り`byte[]`型に変換したものを用意し`NSData.FromArray()`でNSDataへと変換する。

####補足: 雑に更新する
```c#
var serviceName = "hogeService";
var accountName = "hogeAccount";

SecStatusCode code;

var bytePassword = Encoding.UTF8.GetBytes("password");
code = SecKeyChain.AddGenericPassword(serviceName, accountName, bytePassword);
Debug.WriteLine(code) //=> Success

var byteNewPassword = Encoding.UTF8.GetBytes("newPassword");
var passwordQuery = new SecRecord(SecKind.GenericPassword)
{
    Service = serviceName,
    Account = accountName
};

code = SecKeyChain.Remove(passwordQuery);
Debug.WriteLine(code) //=> Success

code = SecKeyChain.AddGenericPassword(serviceName, accountName, byteNewPassword);
Debug.WriteLine(code) //=> Success
```
`SecKeyChain.Remove()`メソッドを用いて、一回消して追加し直すという形でも更新は可能です。
というかキーチェーンアクセスの様子を見る限り`Update()`でも内部的には一回消して追加し直す……をやっているような気がするので、強ち雑とも言えないかもしれない。。

そもそも検索用のクエリを立てなきゃいけないのでコードの量も大して変わらないという。

####参考(主に公式ドキュメント)
https://docs.microsoft.com/en-us/dotnet/api/security.seckeychain?view=xamarin-ios-sdk-12
https://docs.microsoft.com/en-us/dotnet/api/security.seckeychain.queryasdata?view=xamarin-ios-sdk-12#Security_SecKeyChain_QueryAsData_Security_SecRecord_
https://docs.microsoft.com/en-us/dotnet/api/security.secstatuscode?view=xamarin-ios-sdk-12