はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】xorを使った暗号化の基礎からPlayerPrefsの暗号化クラス作成まで

はじめに

今回はxor(排他的論理和)についてみていきます。

xorを使った暗号化はかなり簡単な分類の暗号化アルゴリズムとして知られているので、すぐに習得できると思います。

またその応用としてUnityのデータのセーブ・ロードのために用いられるPlayerPrefsを暗号化してみたいと思います。

早速みていきましょう。

xorの基本

まずxorは演算子の一つで、+, -, *, /と同様にC#では^と書くことで使えます。

Debug.Log(4 ^ 5);    // 1

ここで誰しもがはじめに思うでしょう、どこから1が出てきたのかと。

これを知るためには2進数の世界に入る必要があります。

4(10) = 100(2)
5(10) = 101(2)

これを縦にみていって、0 ^ 0 -> 00 ^ 1 -> 11 ^ 0 -> 11 ^ 1 -> 0という法則にしたがって計算をしていきます。
f:id:hanaaaaaachiru:20200622170106p:plain

xorの性質

xorには以下のような性質があります。

A ^ B = C
C ^ B = A

これを分かりやすいように日本語にして置き換えてみるとこんな感じ。

平文 ^ 鍵 = 暗号文
暗号文 ^ 鍵 = 平文 

なんだか今回の実装方針が見えてきたのではないでしょうか。

このように暗号化の鍵と複合化の鍵が共通の暗号方式を共通鍵暗号方式と呼ぶそうです。

文字列の場合

数字の暗号化は自明ですが、文字列の暗号化はどうするのでしょうか。

文字列は文字コードを使ってバイト列に相互変換することができるので、これを利用します。

f:id:hanaaaaaachiru:20200622174159p:plain

string text = "こんにちは";

byte[] data = System.Text.Encoding.UTF8.GetBytes(text);

// この間に暗号化・復元のコードを書く

string result = System.Text.Encoding.UTF8.GetString(data);

Debug.Log(result);      // こんにちは

実装

今までの知識を活用しながらコードをざっくりと書いてみました。

void Start()
{
    var text = "こんにちは";
    var key = "password";

    // string -> byte[]
    var textBytes = System.Text.Encoding.UTF8.GetBytes(text);
    var keyBytes = System.Text.Encoding.UTF8.GetBytes(key);

    // 暗号化をした後、全てのbyteを3桁に固定して文字列に変換する
    var encryptedText = textBytes
        .Select((t, i) => (byte)(t ^ keyBytes[i % keyBytes.Length]))
        .Aggregate("", (x, y) => x + string.Format("{0:000}", y));

    // 出力: 147224224144245252145229219130242210148238221
    Debug.Log(encryptedText);

    // 暗号化された文字列をbyte[]に戻し、復元
    var target = Enumerable.Repeat<byte>(0, encryptedText.Length / 3)
        .Select((t, i) => byte.Parse(encryptedText.Substring(i * 3, 3)))
        .Select((t, i) => (byte)(t ^ keyBytes[i % keyBytes.Length]))
        .ToArray();

    // byte[] -> string
    var result = System.Text.Encoding.UTF8.GetString(target);

    // 出力: こんにちは
    Debug.Log(result);
}

PlayerPrefsの暗号化

先ほどのコードはベタ書きですが、応用としてPlayerPrefsを暗号化してくれるコードを書いてみました。

using System;
using System.Linq;
using UnityEngine;

public static class EncryptedPlayerPrefs
{
    private static Encrypter _encrypter;

    static EncryptedPlayerPrefs()
        => _encrypter = new Encrypter();

    /// <summary>
    /// 鍵(パスワード)を設定する
    /// </summary>
    /// <param name="key">鍵の文字列</param>
    public static void SetKey(string key)
    {
        if (string.IsNullOrEmpty(key)) throw new ArgumentException("keyにはなにかしらの文字を指定してください");
        _encrypter = new Encrypter(key);
    }

    public static void DeleteAll()
        => PlayerPrefs.DeleteAll();

    public static void DeleteKey(string key)
    {
        key = _encrypter.Encipher(key);
        PlayerPrefs.DeleteKey(key);
    }

    public static float GetFloat(string key)
        => GetFloat(key, default);

    public static float GetFloat(string key, float defaultValue)
    {
        if (key == null) throw new ArgumentNullException("keyにはnull以外の値を指定してください.");

        key = _encrypter.Encipher(key);
        var value = PlayerPrefs.GetString(key);

        if (!string.IsNullOrEmpty(value)) return _encrypter.DecryptFloat(value);
        else return defaultValue;
    }

    public static int GetInt(string key)
        => GetInt(key, default);

    public static int GetInt(string key, int defaultValue)
    {
        if (key == null) throw new ArgumentNullException("keyにはnull以外の値を指定してください.");

        key = _encrypter.Encipher(key);
        var value = PlayerPrefs.GetString(key);

        if (!string.IsNullOrEmpty(value)) return _encrypter.DecryptInt(value);
        else return defaultValue;
    }

    public static string GetString(string key)
        => GetString(key, default);

    public static string GetString(string key, string defaultValue)
    {
        if (key == null) throw new ArgumentNullException("keyにはnull以外の値を指定してください.");

        key = _encrypter.Encipher(key);
        var value = PlayerPrefs.GetString(key);

        if (!string.IsNullOrEmpty(value)) return _encrypter.Decrypt(value);
        else return defaultValue;
    }

    public static bool HasKey(string key)
    {
        key = _encrypter.Encipher(key);
        return PlayerPrefs.HasKey(key);
    }

    public static void Save()
        => PlayerPrefs.Save();

    public static void SetFloat(string key, float value)
        => SetString(key, value.ToString());

    public static void SetInt(string key, int value)
        => SetString(key, value.ToString());

    public static void SetString(string key, string value)
    {
        if (key == null) throw new ArgumentNullException("keyにはnull以外の値を指定してください.");

        key = _encrypter.Encipher(key);
        var str = _encrypter.Encipher(value);
        PlayerPrefs.SetString(key, str);
    }
}

public class Encrypter
{
    private readonly byte[] _key;

    public Encrypter(string key = "password")
        => _key = String2Bytes(key);

    /// <summary>
    /// 暗号化する
    /// </summary>
    public string Encipher(string value)
    {
        if (string.IsNullOrEmpty(value)) return "";

        var target = String2Bytes(value);

        return target.Select((t, i) => (byte)(t ^ _key[i % _key.Length]))
            .Aggregate("", (x, y) => x + string.Format("{0:000}", y));
    }

    /// <summary>
    /// 復元する
    /// </summary>
    public string Decrypt(string value)
    {
        if (string.IsNullOrEmpty(value)) return "";

        var target = Enumerable.Repeat<byte>(0, value.Length / 3)
            .Select((t, i) => byte.Parse(value.Substring(i * 3, 3)))
            .Select((t, i) => (byte)(t ^ _key[i % _key.Length]))
            .ToArray();

        return Bytes2String(target);
    }

    public int DecryptInt(string value)
        => int.Parse(Decrypt(value));

    public float DecryptFloat(string value)
        => float.Parse(Decrypt(value));

    private byte[] String2Bytes(string value)
        => System.Text.Encoding.UTF8.GetBytes(value);

    private string Bytes2String(byte[] value)
        => System.Text.Encoding.UTF8.GetString(value);
}

ブログに貼るには少し長くなってしまいましたが、一応めちゃ雑テストをしておいたのでおそらく正常に動くと思います。

公式のPlayerPrefsにあるメソッドは全て実装した(シグネチャも全部一緒)ので、ほぼ同じ要領で使えるようになっているはずです。
UnityEngine.PlayerPrefs - Unity スクリプトリファレンス

使い方

[Test]
public void EncryptedPlayerPrefsTest()
{
    // これをやらないと「password」というパスワードになりますが、設定しておいたほうが吉
    EncryptedPlayerPrefs.SetKey("ここにパスワードを書く");

    // "hp"というキーでfloatをセーブ,ロードする
    EncryptedPlayerPrefs.SetFloat("hp", 10.5f);
    var hp = EncryptedPlayerPrefs.GetFloat("hp");
    Assert.AreEqual(hp, 10.5f);

    // "mp"というキーでintをセーブ,ロードする
    EncryptedPlayerPrefs.SetInt("mp", 5);
    var mp = EncryptedPlayerPrefs.GetInt("mp");
    Assert.AreEqual(mp, 5);

    // "hp"というキーでstringをセーブ,ロードする
    EncryptedPlayerPrefs.SetString("name", "ももんじゃ");
    var name = EncryptedPlayerPrefs.GetString("name");
    Assert.AreEqual(name, "ももんじゃ");

    // keyが存在していたらtrueを返す
    var flag = EncryptedPlayerPrefs.HasKey("hp");
    Assert.AreEqual(flag, true);

    // 変更された値をディスクへと保存する
    EncryptedPlayerPrefs.Save();

    // 指定したキーと対応する値を削除する
    EncryptedPlayerPrefs.DeleteKey("hp");
    flag = EncryptedPlayerPrefs.HasKey("hp");
    Assert.AreEqual(flag, false);

    // 全てのキーと値を削除する
    EncryptedPlayerPrefs.DeleteAll();
}

[Test]Asset.~~はテスト用のものなので無視していただいてOKですが、PlayePrefsの一通りの操作が同様にできます。

一番気をつけないといけないことは、一度SetKeyでパスワードを変更してしまうと別のパスワードで保存していた値は取得できない(前のパスワードに設定SetKeyすれば取得できる)箇所です。

またSetKeyにて設定したパスワードはディスクに保存されるわけではない(キャッシュされるがゲーム終了時に破棄される)ので、Awakeなどで毎回呼び出すようにしてください。

それぞれのメソッドがどんな意味かみたいかたは公式ドキュメントを一読ください。
docs.unity3d.com

さいごに

ここまででxorについて取り上げていきましたが、悲しいことに解読方法はネット上で探せばいくらでもあります。

少しでも抵抗できるのは価値があると思いますが、AssetStoreにはAESというアメリカで標準規格として採用されているようなガチガチの暗号化をお手軽にしてくれるアセットなんかも存在します。

私は基本的にこちらを利用することにしているので、暗号化に困ったことはありません。

ただし有料なので、暗号化をする必要性とお財布と相談をしてみてください。

またeasy saveが一体どんなものかを知るために日本語での解説サイトと公式ドキュメントを貼っておきます。
簡単にセーブ&ロード&暗号化が実装できるEasy Save【Unity】【アセット】【Easy Save】 - (:3[kanのメモ帳]
https://docs.moodkie.com/product/easy-save-3/

今回はこれくらいで。

ではまた。