UnityでIOSにセキュアに値を保存するにはKeyChainを使おう

Unityで作成したIOS向けアプリで、平文で端末に保存しちゃよくなさそうなものがあったので暗号化して保存するために、ネイティブのKeyChainを使用したのでそのメモ。
細部まで仕様を把握できていないので何かあったらつっこんでください。

# KeyChainって？
端末に自動で暗号化して値を保存してくれるやつ。
UserDefaultsに入れられないような情報（token,pwd）などを保存するときに便利。
Unityでいうと、PlayerPrefsを使うとIOSだとUserDefaultsが使用されるはずなので、平文保存になってしまう。
KeyChainの細かい仕組みをわかりやすくまとめてくださっている記事があるので、詳しく知りたい方は[こちらの記事](https://qiita.com/sachiko-kame/items/261d42c57207e4b7002a)を参考にしていただければなと思います。

# 早速実装しよう
### 値の保存
```obj-c++:KeyChain


int _Add(const char *dataType, const char *value)
{
    NSMutableDictionary* attributes = nil;
    NSMutableDictionary* query = [NSMutableDictionary dictionary];
    NSData* sata = [[NSString stringWithCString:value encoding:NSUTF8StringEncoding] dataUsingEncoding:NSUTF8StringEncoding];


    [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount];
    [query setObject:SERVICE_NAME forKey:(id)kSecAttrService];
    
    OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL);
    
    if (err == noErr) {
        // update item
        attributes = [NSMutableDictionary dictionary];
        [attributes setObject:sata forKey:(id)kSecValueData];
        [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate];
        
        err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes);
        return (int)err;

    } else if (err == errSecItemNotFound) {

        attributes = [NSMutableDictionary dictionary];
        [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
        [attributes setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount];
        [attributes setObject:sata forKey:(id)kSecValueData];
        [attributes setObject:SERVICE_NAME forKey:(id)kSecAttrService];
        [attributes setObject:[NSDate date] forKey:(id)kSecAttrCreationDate];
        [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate];
        err = SecItemAdd((CFDictionaryRef)attributes, NULL);
        return (int)err;
        
    } else {
        return (int)err;
    }


}

```
- 引数は保存の名前と値。ここでは`dataType,value`で定義。
- `dataType`で端末内を検索する。
- すでに値がある場合は値の更新をかける
- 値がまだない場合は、定義して登録する。

`[query setObject:SERVICE_NAME forKey:(id)kSecAttrService];`
でSERVICE_NAMEをしようしてるので`#define SERVICE_NAME`で上の方に定義して貰えば動くと思います。（これが必要なのかどうか実際わかってないので詳しい人いたら教えてください。）

あと、エラー処理とかもしてなくて成功で0が帰ってくるはずなのでそれ以外が帰ってきた時の処理はC#でよしなに記述してもらえればなーと思います。

### 値の取り出し
```obj-c++:KeyChain
char* _Get(const char *dataType)
{
    NSMutableDictionary* query = [NSMutableDictionary dictionary];
    [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount];
    [query setObject:SERVICE_NAME forKey:(id)kSecAttrService];
    [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
    
    CFDataRef cfresult = NULL;
    OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)&cfresult);
    
    if (err == noErr) {
        NSData* passwordData = (__bridge_transfer NSData *)cfresult;
        const char* value = [[[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding] UTF8String];
        char *str = strdup(value);
        return str;
        NSLog(@"create uuid");
    } else {
        return NULL;
    }
}
```
やってることは`_Add`の前半と同じような感じで、`SecItemCopyMatching`の第二引数が`(CFTypeRef*)&cfresult`になることで、検索した値を取り出せる。

こちらもエラーを握りつぶして全部nullで返しているので、必要によってハンドリングして使ってもらえればなーと思います。

### 値の削除
```obj-c++:KeyChain
int _Delete(const char *dataType)
{
    NSMutableDictionary* query = [NSMutableDictionary dictionary];
    [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
    [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount];

    OSStatus err = SecItemDelete((CFDictionaryRef)query);

    if (err == noErr) {
        return 0;
    } else {
        return (int)err
    }
}
```
queryに値入れて`SecItemDelete`に渡してあげるだけで削除できます。
調査不足でエラー処理がかなり適当ですが成功で0返しときました（追加のとこと同じにしといた）。
ここら辺は調査してまた変えてみるかもしれません。

# 終わりに
動くものが実装できてよかったなと思いつつも、まだまだ理解が足りずエラーハンドリング等もできていないに等しい感じなのでもっと勉強していかなければと思います。
コード自体は[ここに置いておいた](https://github.com/nyhk-oi/unity-ios-keychain/tree/main)ので、全体が見たい方や使う方はあくまで自己責任でお願いします。
改善点等、知見のある方いたら勉強させてもらえるとありがたいです。