SecKeyChain周り、触れば触るほどややこしいので分かったこと取り敢えずまとめ。
- アプリケーションパスワード
- インターネットパスワード
の形で、ユーザーが入力したパスワードを端末に保存できる。
※多分他にもあるっぽいですが今の所この2つしか活用出来てません……しかも異なる仕様だと思ってた部分が同じだったので(後述)違いもあまりわからない。。
パスワード保存する
何はともあれパスワードを保存する
SecKeyChain.AddGenericPassword(serviceName, accountName, password);
SecKeyChain.AddInternetPassword(serverName, accountName, password);
AddGenericPasswordはアプリケーションパスワードとして、AddInternetPasswordはインターネットパスワードとしてキーチェーンアクセスに保存する。
serviceName,serverName,accountNameはいずれもstring。
serviceNameとserverNameの違いこそあれど、結局はキーチェーンアクセスに表示される名前なのであまり差異はない(と思う)。
ちなみにブラウザなどで登録したパスワードについてはこのserverNameにWebサイトのホスト名が入るので、アプリケーションの場合はそのままアプリ名をserviceNameに入れれば良い。
passwordのみbyte[]となるので、事前にbyte配列への変換が必要となる。
var bytePassword = Encoding.UTF8.GetBytes(password);
パスワードを取り出す
アプリを再起動した時に前回の値が入力されている……みたいなやつ作るときのための、パスワードの取り出し方。
// 何かしらのアプリケーションパスワードを取り出す想定
// パスワードを取り出すためのクエリ
var passwordQuery = new SecRecord(SecKind.GenericPassword)
{
Service = serviceName,
Account = accountName
};
SecRecordオブジェクトを作成し、キーチェーンアクセスからパスワードを取り出すためのクエリを作成する。
引数にはパスワードの種類を示すSecKind列挙型の値を代入し、レコードを特定するための情報をブロック内に記述する。
ややこしいのはこのSecRecordオブジェクト自体はパスワードそのものではないということ。
取り出すにはこの発行したクエリを元にQueryAsDataメソッドを使う。
// QueryAsData()メソッドはNSData型を返すので文字列に変換する
NSData passwordData = SecKeyChain.QueryAsData(passwordQuery);
string password = passwordData.ToString();
QueryAsData()は引数として受け取ったSecRecordが持つ情報に一致したキーチェーンアクセスのレコードをNSData型で一つだけ返してくれる。最大数を指定することで一致するものを複数返してくれるオーバーロードも存在するっぽい。
保存するときにはbyte[]型に変換したが、取り出す場合はこれで平文のパスワードが取り出せる。
パスワードを更新する
ユーザーがパスワードを書き換えた際などに、新規追加時と同様にAddGenericPassword()を使うと上手くいかない。
これはキーチェーンアクセスの仕様上重複するレコードは作れないから。
→ 恥ずかしながらこれに気付くのに結構時間をかけてしまいました。。Serverが同じでもAccountが異なるレコードは作れるのでInternetPasswordだけの話かと何となく思っていたら、GenericPasswordも同様だったので、余計にこの2つの違いがわからなくなる。。
因みに厄介なことに(?)重複するレコードを作ろうとしてもエラーなどは出ず、只々キーチェーンアクセスに更新がかからないだけとなる。
ただ確かめる術はあるようで、キーチェーンアクセス操作系のメソッドが返してくれるHTTPステータスコードのようなSecStatusCodeという列挙型オブジェクトを参照する。
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を引数として受け取る。これらはそれぞれ既存のレコードを探すためのクエリ、新しい値を表すクエリとして使われる。
// 対象のレコードを特定するためのクエリ
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へと変換する。
補足: 雑に更新する
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