はじめに

iOS/Android でナヌザヌの情報をセキュアに扱う必芁があったので、調査したずころ Android には EncryptedSharedPreferences が存圚するこずを知りたした。iOS には Keychain Services が存圚したす。

今回は Unity の iOS/Android プラットフォヌム䞊で蚭定倀を保存するための実装を行う必芁があったので、Unity から扱えるようネむティブプラグむンを䜜成したした。今埌もこういった芁望はありそうでしたので、蚘事ずしお手順や内容を曞き蚘しおおくこずにしたした。

本蚘事内で玹介しおいるコヌドは䞋蚘にアップ枈みです。

https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample

動䜜環境

  • MacBook Air (M1, 2020)
  • Unity 2020.3.15f2
  • Android 6.0 以䞊

Android のネむティブプラグむンを䜜成する

Android 環境ではたず External Dependency Manager for Unity を利甚しお、Unity の Android ネむティブプラグむンで EncryptedSharedPreferences 利甚可胜にしたす。

(远蚘) Gradle を利甚したラむブラリのむンストヌル方法

shiena さんにご教授いただいたのですが、こちらの蚘事のように Gradle を利甚するこずでも簡易にラむブラリの取り蟌みが可胜なようでした。

手順は䞊蚘の蚘事をご参照いただくずしお、Gradle を利甚する方法で倖郚ラむブラリを取り蟌む際の Assets/Plugins/Android/mainTemplate.gradle および Assets/Plugins/Android/gradleTemplate.properties は䞋蚘になりたす。

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation 'androidx.security:security-crypto:1.1.0-alpha03'
**DEPS**}

android {
org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M
org.gradle.parallel=true
android.enableR8=**MINIFY_WITH_R_EIGHT**
+ android.useAndroidX=true
unityStreamingAssets=.unity3d**STREAMING_ASSETS**
**ADDITIONAL_PROPERTIES**

Gradle を利甚した方法でラむブラリを利甚される際は、次の External Dependency Manager for Unity で必芁なパッケヌゞをむンストヌルする の手順はスキップ可胜です。EncryptedSharedPreferences を利甚するためのネむティブコヌドを远加する のステップから進めおください。

External Dependency Manager for Unity を利甚する方法だず、取り蟌み先プロゞェクト内でラむブラリの競合が発生する恐れがありたす。Gradle を利甚する方法であれば回避が可胜です。1

External Dependency Manager for Unity で必芁なパッケヌゞをむンストヌルする

External Dependency Manager for Unity をむンポヌトするため unitypackage をダりンロヌドしお、EncryptedSharedPreferences を導入したい Unity プロゞェクトを開いおから unitypackage をクリックするこずで、External Dependency Manager for Unity を Unity プロゞェクトにむンポヌトしたす。

ダりンロヌドした unitypackage をクリックしお Unity プロゞェクトに External Dependency Manager for Unity をむンポヌトする

Unity プロゞェクトの Build Settings からプラットフォヌムは Android に切り替えおおきたす。Enable Android Auto-resolution? ずいうダむアログの遞択肢はどちらを遞んでも構いたせん。2

External Dependency Manager for Unity で各皮パッケヌゞを管理する方法は README に蚘茉がある通り、*Dependencies.xml ずいうファむルを Editor フォルダに配眮するこずで可胜になりたす。

今回は EncryptedSharedPreferences を導入するため、䞋蚘の xml ファむルを Editor フォルダ内に配眮したす。

<?xml version="1.0" encoding="utf-8"?>
<dependencies>
    <androidPackages>
        <!--
            本蚘事ではバヌゞョン 1.1.0-alpha03 を利甚しおいる
        -->
        <androidPackage spec="androidx.security:security-crypto:1.1.0-alpha03">
            <androidSdkPackageIds>
                <!--
                    Google の Maven リポゞトリからむンストヌルするため、
                    extra-google-m2repository を指定する
                -->
                <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId>
            </androidSdkPackageIds>
        </androidPackage>
    </androidPackages>
</dependencies>

その埌、Unity メニュヌから Assets -> External Dependency Manager -> Android Resolver -> Force Resolve を遞択しお、Assets/Editor/AndroidPluginDependencies.xml の内容を元に EncryptedSharedPreferences を利甚するのに必芁なパッケヌゞを自動で Assets/Plugins/Android フォルダにダりンロヌドしたす。

1. Unity メニュヌから Assets -> External Dependency Manager -> Android Resolver -> Force Resolve を遞択する 1. Unity メニュヌから Assets -> External Dependency Manager -> Android Resolver -> Force Resolve を遞択する

2. 実行に成功するず EncryptedSharedPreferences を利甚するのに必芁なラむブラリ矀が Assets/Plugins/Android フォルダに配眮される 2. 実行に成功するず EncryptedSharedPreferences を利甚するのに必芁なラむブラリ矀が Assets/Plugins/Android フォルダに配眮される

ここたで来ればあずは Android ネむティブコヌドを Assets/Plugins/Android フォルダ内に配眮しお Unity 偎から叩けるようにするだけです。

EncryptedSharedPreferences を利甚するためのネむティブコヌドを远加する

早速䞋蚘の Android ネむティブコヌドを Assets/Plugins/Android/SecretManager.java に配眮したす。

package com.nikaera;

import com.unity3d.player.UnityPlayerActivity;

import java.lang.Exception;

// External Dependency Manager for Unity によっお、
// 必芁な jar が含たれおいるため EncryptedSharedPreferences の利甚が可胜になる
import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

import android.os.Bundle;
import android.util.Log;

public class SecretManager {
  private SharedPreferences sharedPreferences;

  public SecretManager(Context context) {
    try {
        // EncryptedSharedPreferences で蚭定倀を保存する際に甚いる、
        // 暗号鍵を扱うためのラッパヌクラスをデフォルト蚭定で䜜成する
        MasterKey masterKey = new MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build();

        // EncryptedSharedPreferences のむンスタンスを生成する
        // コンストラクタで䜜成した masterKey を指定しおいる
        this.sharedPreferences = EncryptedSharedPreferences.create(
          context, context.getPackageName(), masterKey,
          EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
          EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        );
    } catch (Exception e) {
        e.printStackTrace();
    }
  }

  /**
   * 指定したキヌで倀を保存する関数
   * @param key 倀を保存する際に甚いるキヌ
   * @param value 保存したい倀
   * @return boolean 倀の保存に成功したかどうか
   */
  public boolean put(String key, String value) {
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.putString(key, value);

    return editor.commit();
  }

  /**
   * 指定したキヌで保存した倀を取埗する関数
   * `put` 関数で保存した倀を取埗するのに利甚する
   * @param key 取埗したい倀のキヌ
   * @return string キヌに玐づく倀、存圚しなければ空文字が返华される
   */
  public String get(String key) {
    return sharedPreferences.getString(key, null);
  }

  /**
   * 指定したキヌで倀を削陀する関数
   * @param key 削陀したい倀のキヌ
   * @return boolean 倀の削陀に成功したかどうか
   */
  public boolean delete(String key) {
    SharedPreferences.Editor editor = sharedPreferences.edit();
    editor.remove(key);

    return editor.commit();
  }
}

その埌、䞊蚘を Unity スクリプトから実行可胜にするための C# クラスを䜜成したす。本蚘事ではファむルを Assets/Scripts/EncryptedSharedPreferences.cs に配眮したす。

using UnityEngine;

/// <summary>
///     利甚するネむティブコヌドは <c>Assets/Plugins/Android/SecretManager.java</c> に蚘茉
/// </summary>
/// <remarks>
///     <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a>
/// </remarks>
class EncryptedSharedPreferences
{
    private readonly AndroidJavaObject _secretManager;
    public EncryptedSharedPreferences()
    {
        // コンストラクタで com.nikaera.SecretManager のむンスタンス生成を行う
        var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
            .GetStatic<AndroidJavaObject>("currentActivity");
        var context = activity.Call<AndroidJavaObject>("getApplicationContext");
        _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context);
    }

    public bool Put(string key, string value)
    {
        return _secretManager.Call<bool>("put", key, value);
    }

    public string Get(string key)
    {
        return _secretManager.Call<string>("get", key);
    }

    public bool Delete(string key)
    {
        return _secretManager.Call<bool>("delete", key);
    }
}

あずは甚途に応じお䞋蚘のようなコヌドで蚭定倀の保存や取埗などを行えたす。

// ...
var _sharedPreferences = new EncryptedSharedPreferences();

// name をキヌずしお倀を nikaera で保存する
_sharedPreferences.Put("name", "nikaera");

// name をキヌずしお倀を取埗する
var name = _sharedPreferences.Get("name");

// "nikaera" が出力される
Debug.Log(name);

// name をキヌずしお倀を削陀する
_sharedPreferences.Delete("name");

// ...

iOS のネむティブプラグむンを䜜成する

iOS の堎合は倖郚ラむブラリを利甚しないため、External Dependency Manager for Unity は利甚したせん。本来であれば Swift で信頌できる倖郚フレヌムワヌクを取り蟌み利甚できるず良さそうですが、今回は Objective-C でネむティブプラグむンを曞いおいきたす。3

Keychain Services を利甚するためのネむティブコヌドを远加する

早速䞋蚘の iOS ネむティブコヌドを Assets/Plugins/iOS/KeychainService.mm に配眮したす。

// Keychain Services を利甚するために Security フレヌムワヌクを利甚する
#import <Security/Security.h>

extern "C"
{
    // 指定したキヌで倀を保存する関数
    // - param
    //   - dataType:  倀を保存する際に甚いるキヌ
    //   - value: 保存したい倀
    // - return
    //   - 保存時のステヌタスコヌドを返华する (0 以倖は倱敗)
    int addItem(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];

        OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL);

        if (err == noErr) {
            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:[NSDate date] forKey:(id)kSecAttrCreationDate];
            [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate];
            err = SecItemAdd((CFDictionaryRef)attributes, NULL);
            return (int)err;
        } else {
            return (int)err;
        }
    }

    // 指定したキヌで倀を取埗する関数
    // - param
    //   - dataType: 倀を取埗する際に甚いるキヌ
    // - return
    //   - キヌに玐づく倀、存圚しなければ空文字が返华される
    char* getItem(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:(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;
        } else {
            return NULL;
        }
    }

    // 指定したキヌで倀を削陀する関数
    // - param
    //   - dataType:  倀を削陀する際に甚いるキヌ
    // - return
    //   - 保存時のステヌタスコヌドを返华する (0 以倖は倱敗)
    int deleteItem(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;
        }
    }
}

Keychain Services は Security フレヌムワヌクを利甚するため、KeychainService.mm に察しお Security フレヌムワヌクの䟝存関係を蚭定する必芁がありたす。

KeychainService.mm で Security フレヌムワヌクの利甚を可胜にする KeychainService.mm で Security フレヌムワヌクの利甚を可胜にする

その埌、䞊蚘を Unity スクリプトから実行可胜にするための C# クラスを䜜成したす。本蚘事ではファむルを Assets/Scripts/KeychainService.cs に配眮したす。

using System.Runtime.InteropServices;

/// <summary>
///     実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に蚘茉
/// </summary>
/// <remarks>
///     <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a>
/// </remarks>
class KeychainService
{
#if UNITY_IOS
    [DllImport("__Internal")]
    private static extern int addItem(string dataType, string value);

    [DllImport("__Internal")]
    private static extern string getItem(string dataType);

    [DllImport("__Internal")]
    private static extern int deleteItem(string dataType);
#endif

    public bool Put(string key, string value)
    {
#if UNITY_IOS
        // 返华されるステヌタスが 0 なら成功
        return addItem(key, value) == 0;
#endif
    }

    public string Get(string key)
    {
#if UNITY_IOS
        return getItem(key);
#else
        return null;
#endif
    }

    public bool Delete(string key)
    {
#if UNITY_IOS
        // 返华されるステヌタスが 0 なら成功
        return deleteItem(key) == 0;
#endif
    }
}

あずは甚途に応じお䞋蚘のようなコヌドで蚭定倀の保存や取埗などを行えたす。

// ...
var _keychainService = new KeychainService();

// name をキヌずしお倀を nikaera で保存する
_keychainService.Put("name", "nikaera");

// name をキヌずしお倀を取埗する
var name = _keychainService.Get("name");

// "nikaera" が出力される
Debug.Log(name);

// name をキヌずしお倀を削陀する
_keychainService.Delete("name");

// ...

(䜙談) むンタヌフェヌスで iOS/Android のふるたいを共通化する

このたただずプラットフォヌムを切り替える毎にコヌドを曞き盎さないずならないので、むンタヌフェヌスを利甚しお共通化を行いたす。

public interface ISecretManager
{
    /// <summary>
    /// 指定したキヌで倀を保存する関数
    /// </summary>
    /// <param name="key">キヌ</param>
    /// <param name="value">倀</param>
    /// <returns>保存に成功したかどうか</returns>
    bool Put(string key, string value); 
    
    /// <summary>
    /// 指定したキヌの倀を取埗する関数
    /// </summary>
    /// <param name="key">キヌ</param>
    /// <returns>指定したキヌで蚭定された倀、無ければ null</returns>
    string Get(string key);
    
    /// <summary>
    /// 指定したキヌの倀を削陀する関数
    /// </summary>
    /// <param name="key">キヌ</param>
    /// <returns>削陀に成功したかどうか</returns>
    bool Delete(string key);
}

その埌、Assets/Scripts/EncryptedSharedPreferences.cs および Assets/Scripts/KeychainService.cs を䞋蚘の通り ISecretManager の実装に玐付けたす。

using UnityEngine;

/// <summary>
///     利甚するネむティブコヌドは <c>Assets/Plugins/Android/SecretManager.java</c> に蚘茉
/// </summary>
/// <remarks>
///     <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a>
/// </remarks>
class EncryptedSharedPreferences: ISecretManager
{
    private readonly AndroidJavaObject _secretManager;
    public EncryptedSharedPreferences()
    {
        var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer")
            .GetStatic<AndroidJavaObject>("currentActivity");
        var context = activity.Call<AndroidJavaObject>("getApplicationContext");
        _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context);
    }

    #region ISecretManager

    public bool Put(string key, string value)
    {
        return _secretManager.Call<bool>("put", key, value);
    }

    public string Get(string key)
    {
        return _secretManager.Call<string>("get", key);
    }

    public bool Delete(string key)
    {
        return _secretManager.Call<bool>("delete", key);
    }

    #endregion
}
using System.Runtime.InteropServices;

/// <summary>
///     実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に蚘茉
/// </summary>
/// <remarks>
///     <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a>
/// </remarks>
class KeychainService: ISecretManager
{
#if UNITY_IOS
    [DllImport("__Internal")]
    private static extern int addItem(string dataType, string value);

    [DllImport("__Internal")]
    private static extern string getItem(string dataType);

    [DllImport("__Internal")]
    private static extern int deleteItem(string dataType);
#endif

    // KeychainService.mm に定矩した関数を呌び出す
    #region ISecretManager

    public bool Put(string key, string value)
    {
#if UNITY_IOS
        return addItem(key, value) == 0;
#else
        return false;
#endif
    }

    public string Get(string key)
    {
#if UNITY_IOS
        return getItem(key);
#else
        return null;
#endif
    }

    public bool Delete(string key)
    {
#if UNITY_IOS
        return deleteItem(key) == 0;
#else
        return false;
#endif
    }

    #endregion
}

あずは䞊蚘をよしなに利甚可胜な SecretManager クラスを䜜成したす。

using UnityEngine;

/// <summary>
///     <em>Editor 利甚時のみ PlayerPrefs を利甚する</em>
/// </summary>
/// <remarks><see cref="KeychainService" />, <see cref="EncryptedSharedPreferences" /></remarks>
public static class SecretManager
{
#if UNITY_EDITOR
#elif UNITY_ANDROID
        private static ISecretManager _instance = new EncryptedSharedPreferences();
#elif UNITY_IOS
        private static ISecretManager _instance = new KeychainService();
#endif

        public static bool Put(string key, string value)
        {
#if UNITY_EDITOR
                PlayerPrefs.SetString(key, value);
                PlayerPrefs.Save();

                return true;
#elif UNITY_IOS || UNITY_ANDROID
                return _instance.Put(key, value);
#else
                Debug.Log("Not Implemented.");
                return false;
#endif

        }

        public static string Get(string key)
        {
#if UNITY_EDITOR
                return PlayerPrefs.GetString(key);
#elif UNITY_IOS || UNITY_ANDROID
        return _instance.Get(key);
#else
            Debug.Log("Not Implemented.");
            return null;
#endif
        }

        public static bool Delete(string key)
        {
#if UNITY_EDITOR
            PlayerPrefs.DeleteKey(key);
            PlayerPrefs.Save();

            return true;
#elif UNITY_IOS || UNITY_ANDROID
            return _instance.Delete(key);
#else
            Debug.Log("Not Implemented.");
            return false;
#endif
        }
}

これでプラットフォヌム間の実装差異を気にするこずなく、䞋蚘のような蚘述で蚭定倀の保存や取埗などを行えたす。iOS/Android 以倖のプラットフォヌムで远加実装したい堎合は プラットフォヌム䟝存コンパむル ず ISecretManager の実装クラスを新たに䜜成するこずで簡単に远加できたす。

// ...
// name をキヌずしお倀を nikaera で保存する
SecretManager.Put("name", "nikaera");

// name をキヌずしお倀を取埗する
var name = SecretManager.Get("name");

// "nikaera" が出力される
Debug.Log(name);

// name をキヌずしお倀を削陀する
SecretManager.Delete("name");

// ...

おわりに

今回は iOS/Android で蚭定倀をセキュアに扱うための方法に぀いおたずめおみたした。実際は Keychain Services 呚りは実装が倧倉なので、External Dependency Manager for Unity ずか䜿っお KeychainAccess のような倖郚ラむブラリを利甚する構成のほうが良いず思われたす。

本蚘事の内容に誀りがあったり、実際にはセキュアな実装ができおいない等々あれば是非コメントでご指摘いただけたすず幞いです。

参考リンク


  1. 逆に External Dependency Manager for Unity を利甚する方法のメリットは、UnityPackage などでラむブラリずしお配垃する際に、ラむブラリを動䜜させるのに必芁な倖郚パッケヌゞも同梱した状態で配垃が可胜になるなどがありたす。(圓然ラむセンスには気を付ける必芁がありたすが…) ↩︎

  2. パッケヌゞの䟝存関係を自動で解決するかどうかずいう遞択肢になりたす。本蚘事では明瀺的に Resolve を実行するため Disable でも Enable でも進行䞊の問題はありたせん。 ↩︎

  3. CocoaPods もサポヌトされおいるようなので、iOS でも Android 同様、倖郚ラむブラリを取り蟌むのは簡単にできそうでした。䟋えば KeychainAccess ずか䜿いたい。 ↩︎