Плагины многопользовательского шифрования
Многопользовательская игра Unity может использовать шифрование Подключаемый модуль Набор кода, созданный вне Unity, который создает функциональные возможности в Unity. . В Unity можно использовать два типа подключаемых модулей: управляемые подключаемые модули (управляемые сборки .NET, созданные с помощью таких инструментов, как Visual Studio) и собственные подключаемые модули (библиотеки собственного кода для конкретной платформы). Подробнее
См. в Словарь , чтобы все данные, которые вы отправляете по сети, через подключаемый модуль шифрования перед отправкой. Это позволяет защитить вашу игру от мошенничества путем манипулирования пакетами и атак на выделенные игровые серверы.
В Unity Multiplayer нет встроенного подключаемого модуля шифрования, поэтому вы должны предоставить свой собственный подключаемый модуль, реализующий алгоритм шифрования и реализующий обязательные функции, перечисленные ниже.
На следующей диаграмме показано, как многопользовательская игра Unity использует подключаемый модуль шифрования, если вы его предоставили.
Поток данных при использовании подключаемого модуля шифрования с многопользовательской игрой Unity
Как использовать подключаемый модуль шифрования
Чтобы указать вашей игре или приложению использовать подключаемый модуль шифрования, вы должны вызвать UnityEngine.Networking.NetworkTransport.LoadEncryptionLibrary(path) , где path — это путь к вашему скомпилированному плагину. Обычно в Windows это будет string.Format(«<0>/Plugins/UnetEncryption.dll», Application.dataPath) .
При вызове этой функции Unity проверяет, существует ли файл и реализуются ли в нем все обязательные функции (перечислены ниже). Это функции, которые будет вызывать сама многопользовательская система Unity. Если вы создаете собственный подключаемый модуль шифрования, вам, вероятно, потребуется добавить дополнительные функции, которые вы вызываете из кода C#. Например, чтобы инициализировать ваш алгоритм или предоставить вашему плагину значения ключей. Вы можете сделать это обычным способом для нативных подключаемых модулей, вызываемых из C#.
Примечание. Расположение плагина в собранной версии игры не обязательно совпадает с расположением в папке «Активы» и может различаться на разных целевых платформах. Возможно, вам потребуется написать код, который определяет текущую среду выполнения и выбирает правильный путь на ее основе.
Вы можете получить образец подключаемого модуля шифрования и пример проекта Unity, использующего его, на GitHub Unity. Это сделано для того, чтобы проиллюстрировать отправную точку для реализации вашего собственного подключаемого модуля.
Обязательные функции
Любой подключаемый модуль шифрования, который вы создаете или используете, должен обеспечивать следующие функции. Unity не сможет загрузить плагин, если он не определяет их. Это функции, которые будут вызываться самой средой выполнения Unity. Плагины обычно предоставляют дополнительные функции, которые можно вызывать из пользовательского кода C#, например, для регистрации ключей.
Для шифрования данных
Эта функция выполняет шифрование. Это вызывается сетью Unity всякий раз, когда пакет должен быть отправлен по сети.
Параметры
- полезная нагрузка — это данные, которые необходимо зашифровать.
- payload_len — длина буфера payload в байтах.
- dest — это буфер, в который подключаемый модуль должен записывать зашифрованные данные.
- dest_len — емкость буфера назначения в байтах. Подключаемый модуль должен заменить это значение количеством байтов, фактически записанных в место назначения.
- connection_id — это локальный идентификатор соединения.
- isConnect имеет значение true, если этот пакет является запросом на подключение. Когда это так, плагину должно быть заранее сказано (кодом игры), какой ключ использовать. Если это значение ложно, ожидается, что подключаемый модуль уже имеет сопоставление этого значения с ключом для использования. См. пример плагина для реализации.
Возвращаемое значение
Encrypt должен возвращать ноль в случае успеха. При любом другом возвращаемом значении среда выполнения отбрасывает пакет, не отправляя его.
Для расшифровки данных
Эта функция выполняет расшифровку. Это вызывается сетью Unity всякий раз, когда пакет получен из сети.
Параметры
- payload — это полученный пакет.
- payload_len — длина в байтах буфера полезной нагрузки.
- dest — это буфер, в который подключаемый модуль должен записывать расшифрованные данные.
- dest_len — емкость буфера назначения в байтах. Подключаемый модуль должен заменить это значение количеством байтов, фактически записанных в место назначения.
- key_id – целочисленный идентификатор. Плагин должен записать значение, которое однозначно идентифицирует используемый ключ дешифрования. На сервере это значение будет передано обратно в ConnectionIdAssigned , если будет принято новое подключение.
Возвращаемое значение
Decrypt должен возвращать ноль в случае успеха. При любом другом возвращаемом значении пакет отбрасывается без дальнейшей обработки.
SafeMaxPacketSize
Эту функцию следует вызвать из игры, чтобы изменить ConnectionConfig.PacketSize (также известную как максимальная единица передачи или MTU) перед вызовом NetworkTransport.AddHost .
Например, ваша игра обычно использует MTU 1000 байт. Если для ConnectionConfig.PacketSize установлено значение 1000 байт перед передачей его в NetworkTransport.AddHost (через HostConfig.DefaultConfig ), то уровень NetworkTransport будет отправлять не более 1000 байт открытого текста в одном пакете.
Подключаемый модуль шифрования обычно увеличивает нагрузку из-за того, что информация заголовка помещается перед полезной нагрузкой, а также округляет полезную нагрузку до размера блока шифрования. Например, если вы отправляете 18 байт открытого текста, а подключаемому модулю необходимо добавить 49 байт заголовка и использовать AES для шифрования данных с размером блока 16 байт, тогда алгоритм создаст пакет размером 81 байт (18 байт). байты открытого текста округляются до 32 байтов зашифрованного текста, а затем еще 49 байтов заголовка).
Unity вызывает эту функцию, чтобы убедиться, что пакеты, которые должны быть отправлены, не превышают предела возможной отправки, учитывая сетевой MTU и расширение и заполнение зашифрованного текста вашего алгоритма шифрования.
Параметры
- mtu — максимальная единица передачи. Максимальный размер пакета, который должен генерировать подключаемый модуль.
Возвращаемое значение
Максимальное количество открытого текста, которое должно быть предоставлено для одного вызова Encrypt, чтобы подключаемый модуль мог генерировать пакеты, размер которых не превышает MTU.
Вы должны установить максимальный размер пакета в конфигурации подключения, чтобы указать Unity Multiplayer разделить данные, чтобы они соответствовали вашим требованиям к шифрованию. Если вы заметили, что некоторые из ваших сообщений не могут успешно передаваться по сети, это может быть связано с тем, что они были отброшены из-за превышения максимального размера пакета.
ConnectionIdAssigned
Это вызывается на сервере, когда новое соединение принято и ему присвоен идентификатор.
Защита игр и мобильных приложений от взлома для чайников (Unity, C#, Mono)
Всем снова здравствуйте! Дошли руки написать крутую статью на весьма важную тему для разработчиков игр. Итак, поговорим о защите ваших драгоценных игр и приложений, которые вы пилите на Unity в надежде заработать на буханку хлеба, от взлома злобными школьниками. Почему школьниками? Потому что надежной на 100% защиты априори быть не может. И кто захочет, все равно взломает. Вопрос лишь в том, сколько времени и сил он на это потратит. И как любят шутить безопасники — терморектальный криптоанализ никто не отменял.
- защита данных приложения (сейвов)
- защита памяти приложения
- защита внутриигровых покупок (Google Play)
1. Подготовка
Для начала нужно научиться преобразовывать игровые данные (типы, классы) в строки. Стоит изучить JSON или XML сериализацию. Начинать с XML не советую, т.к. возникнут проблемы с iOS. Лучше изучить JSON, вот ссылка wiki.unity3d.com/index.php/SimpleJSON. К сожалению, это тема отдельной статьи и я не буду на этом останавливаться. Если лениво разбираться — можно по старинке лепить строку вручную с помощью сепараторов. Например:
Еще нужно уметь преобразовывать строки в массивы байт и обратно. Тут все просто:
Далее строку можно завуалировать, применив к ней base64 преобразование. Особо отмечу, что base64 не является шифрованием, он не имеет ключа шифрования и все такое. base64 преобразует вашу строку в новую строку, состоящую только из ASCII символов. Наглядно посмотреть, как это происходит, можно по ссылке base64.ru. Я просто приведу код реализации:
Также отмечу, что base64 работает быстро и сравним по скорости с операцией сложения. Выполнять такие преобразования можно даже в цикле Update.
2. Защита игровых данных (сейвов)
- Application.persistentDataPath может измениться при обновлении приложения (например, приложение переместится на SD карту). Соответственно, файл сохранения будет not found, а пользователь потеряет весь прогресс
- Это не будет работать в web-плеере и windows phone
О да, детка, супер! Теперь нужно зашифровать наши сохранения. Тут можно по-быстрому выполнить base64 преобразование, это уже защитит сохранения от редактирования через большинство программ для взлома. Но по хардкору самое время прикрутить нормальное шифрование. Сразу к делу, берем AES и шифруем. Копипастим файл AES.cs и не задаемся вопросом, как это работает:
3. Защита памяти приложения
Все помнят такую PC программу, как ArtMoney? Она умела искать значения в RAM и за несколько итераций отсеивания позволяла стать миллионером в игре. Сейчас для Android и iOS подобных программ развелось очень много, например самая популярная — GameKiller.
Защититься от таких программ довольно просто — нужно шифровать значения в памяти приложения. Шифровать КАЖДЫЙ РАЗ при записи и дешифровать КАЖДЫЙ РАЗ при чтении. И так как операция довольно частая, нет смысла использовать тяжелый AES и нам нужен супербыстрый алгоритм. Я предлагаю несколько модифицировать наш base64 и реализовать свое шифрование — эффективное, быстрое, с блэкджеком и XOR:
Теперь, как только мы прочитали и расшифровали AES-ом профиль из сохранения, сразу шифруем все значения этим B64X (название я сам придумал). И расшифровываем каждый раз, когда нужно узнать, сколько денег у игрока, какой у него уровень и т.д. B64X может использовать ключ (пароль) для шифрования, а может использовать рандомный сессионный ключ, чтобы мы не парились, где и как его хранить.
4. Защита внутриигровых покупок
Для многих разработчиков эта тема не актуальна и мало кто реализует защиту. В принципе, если у вас многопользовательская игра, то нужно подумать над защитой ее экономики. Есть такая программа — Freedom. Требует рут и, если в двух словах, подменяет сервис внутриигровых покупок. Короче — игрок может совершать покупки за бесплатно.
Опустим рассмотрение механизма проверки покупок на сервере разработчика, ведь не у всех он есть. Расскажу, что предлагает Google в таких случаях.
UPD: Unity реализовал механизм покупок и их проверку (http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html), поэтому информация ниже теперь имеет только теоретическую нагрузку.
При создании приложения в консоли разработчика Google генерирует пару ключей для алгоритма RSA — открытый и закрытый ключ. Если не знаете, что это такое — погуглите асиметричное шифрование. Открытый ключ можно получить в консоли разработчика:
Вы его еще используете при реализаци игрового магазина в приложении.
Закрытый ключ Google вам никогда не покажет и будет использовать его для цифровой подписи покупок. Соответственно закрытым ключом можно только зашифровать подпись, а открытым ключом можно только расшифровать подпись.
Механизм защиты получается довольно простой — Google подписывает все json-ответы сервера покупок, и никто другой такую подпись подделать не может. Разработчик, зная открытый ключ, может проверить цифровую подпись ответов сервера. И если сервер был сфабрикован с помощью Freedom, то цифровая подпись будет неправильная.
Перейдем к реализации. Для начала нужно выполнить одну неприятную операцию. Нужно преобразовать открытый base64 ключ из консоли разработчика в xml-ключ, который подойдет для дешифрования подписи. Свиду кажется, что достаточно просто раскодировать его base64. Но это не так. Предлагаю воспользоваться онлайн сервисом и сразу прикопать xml-ключ в приложении. Особо заморачиваться о его защите не стоит — это же открытый ключ. Его могут сфабриковать, но это уже другая история. Итак, сервис вот, вставляем туда свой base64 ключ и получаем xml-ключ: superdry.apphb.com/tools/online-rsa-key-converter
В нижнем поле и есть наш xml-ключ. Сохраняем его в игре или приложении. А дальше все просто. Google возвращает нам покупку. Если использовать в приложении бесплатный плагин для реализации покупок OpenIAB, то это объект класса Purchase, у него есть 2 нужных нам поля:
Теперь приведу реализацию механизма проверки подписи:
Ну и теперь, когда от Google пришел ответ, что покупка совершена, проверяем ее подпись и показываем игроку фигу, если подпись не совпадает:
Хочу отметить, что при совершении покупки лучше добавить рандомный payload к запросу, это защитит от атак man-in-the-middle, когда вам могут повторно подпихивать корректный ответ сервера с правильной, но одной и той же цифровой подписью. Это необязательный аргумент в реализации OpenIAB, на который большинство кладут болт:
Unity как шифровать данные в json
Многопользовательская игра Unity может использовать шифрование Подключаемый модуль Набор кода, созданный вне Unity, который создает функциональные возможности в Unity. . В Unity можно использовать два типа подключаемых модулей: управляемые подключаемые модули (управляемые сборки .NET, созданные с помощью таких инструментов, как Visual Studio) и собственные подключаемые модули (библиотеки собственного кода для конкретной платформы). Подробнее
См. в Словарь , чтобы все данные, которые вы отправляете по сети, через подключаемый модуль шифрования перед отправкой. Это позволяет защитить вашу игру от мошенничества путем манипулирования пакетами и атак на выделенные игровые серверы.
В Unity Multiplayer нет встроенного подключаемого модуля шифрования, поэтому вы должны предоставить свой собственный подключаемый модуль, реализующий алгоритм шифрования и реализующий обязательные функции, перечисленные ниже.
На следующей диаграмме показано, как многопользовательская игра Unity использует подключаемый модуль шифрования, если вы его предоставили.
Поток данных при использовании подключаемого модуля шифрования с многопользовательской игрой Unity
Как использовать подключаемый модуль шифрования
Чтобы указать вашей игре или приложению использовать подключаемый модуль шифрования, вы должны вызвать UnityEngine.Networking.NetworkTransport.LoadEncryptionLibrary(path) , где path — это путь к вашему скомпилированному плагину. Обычно в Windows это будет string.Format(« /Plugins/UnetEncryption.dll», Application.dataPath) .
При вызове этой функции Unity проверяет, существует ли файл и реализуются ли в нем все обязательные функции (перечислены ниже). Это функции, которые будет вызывать сама многопользовательская система Unity. Если вы создаете собственный подключаемый модуль шифрования, вам, вероятно, потребуется добавить дополнительные функции, которые вы вызываете из кода C#. Например, чтобы инициализировать ваш алгоритм или предоставить вашему плагину значения ключей. Вы можете сделать это обычным способом для нативных подключаемых модулей, вызываемых из C#.
Примечание. Расположение плагина в собранной версии игры не обязательно совпадает с расположением в папке «Активы» и может различаться на разных целевых платформах. Возможно, вам потребуется написать код, который определяет текущую среду выполнения и выбирает правильный путь на ее основе.
Вы можете получить образец подключаемого модуля шифрования и пример проекта Unity, использующего его, на GitHub Unity. Это сделано для того, чтобы проиллюстрировать отправную точку для реализации вашего собственного подключаемого модуля.
Обязательные функции
Любой подключаемый модуль шифрования, который вы создаете или используете, должен обеспечивать следующие функции. Unity не сможет загрузить плагин, если он не определяет их. Это функции, которые будут вызываться самой средой выполнения Unity. Плагины обычно предоставляют дополнительные функции, которые можно вызывать из пользовательского кода C#, например, для регистрации ключей.
Для шифрования данных
Эта функция выполняет шифрование. Это вызывается сетью Unity всякий раз, когда пакет должен быть отправлен по сети.
Параметры
- полезная нагрузка — это данные, которые необходимо зашифровать.
- payload_len — длина буфера payload в байтах.
- dest — это буфер, в который подключаемый модуль должен записывать зашифрованные данные.
- dest_len — емкость буфера назначения в байтах. Подключаемый модуль должен заменить это значение количеством байтов, фактически записанных в место назначения.
- connection_id — это локальный идентификатор соединения.
- isConnect имеет значение true, если этот пакет является запросом на подключение. Когда это так, плагину должно быть заранее сказано (кодом игры), какой ключ использовать. Если это значение ложно, ожидается, что подключаемый модуль уже имеет сопоставление этого значения с ключом для использования. См. пример плагина для реализации.
Возвращаемое значение
Encrypt должен возвращать ноль в случае успеха. При любом другом возвращаемом значении среда выполнения отбрасывает пакет, не отправляя его.
Для расшифровки данных
Эта функция выполняет расшифровку. Это вызывается сетью Unity всякий раз, когда пакет получен из сети.
Параметры
- payload — это полученный пакет.
- payload_len — длина в байтах буфера полезной нагрузки.
- dest — это буфер, в который подключаемый модуль должен записывать расшифрованные данные.
- dest_len — емкость буфера назначения в байтах. Подключаемый модуль должен заменить это значение количеством байтов, фактически записанных в место назначения.
- key_id – целочисленный идентификатор. Плагин должен записать значение, которое однозначно идентифицирует используемый ключ дешифрования. На сервере это значение будет передано обратно в ConnectionIdAssigned , если будет принято новое подключение.
Возвращаемое значение
Decrypt должен возвращать ноль в случае успеха. При любом другом возвращаемом значении пакет отбрасывается без дальнейшей обработки.
SafeMaxPacketSize
Эту функцию следует вызвать из игры, чтобы изменить ConnectionConfig.PacketSize (также известную как максимальная единица передачи или MTU) перед вызовом NetworkTransport.AddHost .
Например, ваша игра обычно использует MTU 1000 байт. Если для ConnectionConfig.PacketSize установлено значение 1000 байт перед передачей его в NetworkTransport.AddHost (через HostConfig.DefaultConfig ), то уровень NetworkTransport будет отправлять не более 1000 байт открытого текста в одном пакете.
Подключаемый модуль шифрования обычно увеличивает нагрузку из-за того, что информация заголовка помещается перед полезной нагрузкой, а также округляет полезную нагрузку до размера блока шифрования. Например, если вы отправляете 18 байт открытого текста, а подключаемому модулю необходимо добавить 49 байт заголовка и использовать AES для шифрования данных с размером блока 16 байт, тогда алгоритм создаст пакет размером 81 байт (18 байт). байты открытого текста округляются до 32 байтов зашифрованного текста, а затем еще 49 байтов заголовка).
Unity вызывает эту функцию, чтобы убедиться, что пакеты, которые должны быть отправлены, не превышают предела возможной отправки, учитывая сетевой MTU и расширение и заполнение зашифрованного текста вашего алгоритма шифрования.
Параметры
- mtu — максимальная единица передачи. Максимальный размер пакета, который должен генерировать подключаемый модуль.
Возвращаемое значение
Максимальное количество открытого текста, которое должно быть предоставлено для одного вызова Encrypt, чтобы подключаемый модуль мог генерировать пакеты, размер которых не превышает MTU.
Вы должны установить максимальный размер пакета в конфигурации подключения, чтобы указать Unity Multiplayer разделить данные, чтобы они соответствовали вашим требованиям к шифрованию. Если вы заметили, что некоторые из ваших сообщений не могут успешно передаваться по сети, это может быть связано с тем, что они были отброшены из-за превышения максимального размера пакета.
ConnectionIdAssigned
Это вызывается на сервере, когда новое соединение принято и ему присвоен идентификатор.
Проблема с Шифрованием JSON
Есть две сцены (Меню и игра)
В меню есть скрипт, который сохраняет и загружает игру, обращаясь к скрипту сохранения/загрузки а уже этот обращаеться к скрипту, который это кодирует и декдирует.
Цепь такая: Главный скрипт -> Скрипт Save/Load -> Скрипт шифрования
Так вот всё работает прекрасно, НО на второй сцене (Игре), если вызвать функцию сохранения или загрузки — ничего не произойдёт и если перейти обратно из игры в меню (По UI кнопке), то мне напишет в консоль — "Key Not Exist"
Главный скрипт (Меню) (Так-же и в скрипте, который уже в ИГРЕ находится)
Encrypting Game Data with Unity
It’s a matter of trust. In any context where a game can open, use, or save files where players can also access them, there is always the chance a user, system, or process may change a file. On PC and Mac platforms, there is also a chance a player may simply skip over editing files and change values directly in the RAM itself, subverting any attempt to protect files. Depending on the resources and time available to a player, they could also attempt to decompile parts of the game and figure out where any secret keys are stored within the code or how everything is arranged within its internal data structures.
While some compilation processes can help protect the game’s running memory, and there are approaches to better protecting files such as encrypting them, the root issue is still one of trust. If, as a developer, you can trust your players to not change the game’s files, a more simple solution of reading and writing text files might be the best option. If you anticipate some users might attempt to change values in something like a save file, using something like encryption for sensitive data might be a good option. If you feel you cannot trust your players at all, remember the only perfect solution for complete safety is for no players to ever play your game. All digital interactions come with some level of information risk.
Problems With Existing Game Data File Operations
All of the common solutions for working with game data when using Unity all also come with the risk the player might change the files holding the data. This is true of working with BinaryFormatter and JsonUtility, where game objects are serialized into an easier format to save the data, and with using PlayerPrefs as well. Through using local files on the local system, they all risk a player, or some other user, system, or process, accidently or purposely changing the data files.
One approach to solving part of these issues is to use encryption. This makes the files harder to understand for systems, process, and users who hope to overwrite them with malicious values. However, it will not protect against accidental corruption. There is still the possibility of a file getting corrupted for any numbers of reasons.
Using encryption also comes with greater complexity for the developer. As part of using the data, it must be encrypted before being written to a file and unencrypted before using the values. This introduces extra processing time, and the need for more code as part of the saving and loading operations.
Symmetric and Asymmetric Encryption
The terms “symmetric” and “asymmetric,” as used with encryption, answer the question of “Who is trusted?” In symmetric encryption, at least two parties have the “secret key” used to create the encrypted data and can use it to read and write the data. The use of the “secret key” gives them access. This is different from asymmetric encryption, which is also called public-key encryption. In this case, there are two keys used to generate the encrypted data: a public key and a private key. The public key is “known” and can be used to generate the encrypted data. The private key, when used with the public key, can be used to decrypt the data.
In general, asymmetric encryption is far stronger than symmetric encryption. However, it is also more complex and much slower relative to the data size. Put in very general terms:
- Symmetric: Faster, but less secure.
- Asymmetric: Slower, but more secure.
Generally, because asymmetric encryption depends on a known public key, it is not as useful for game data. It could be used in situations where a server was involved and the public key accessed, but most games install to a local device without access to a public key. This makes symmetric encryption a “good” choice for most use cases where encryption might be needed, but there is not the ability to more widely share a public key when using the game.
DES (Data Encryption Standard) and AES (Advanced Encryption Standard)
The acronyms DES and AES refer to “Standards,” but are also algorithms following the standard on which they are based. Because of some known attacks on DES, many developers use AES instead of DES. However, DES can still be used for many situations.
While C# supports both DES and AES, AES will be used in this post as part of example code.
Understanding AES
The class Aes is part of the namespace System.Security.Cryptography.
Because Aes works on data itself, all of of its operations work on byte arrays, reading or writing data without specific data types.
What is key and IV?
AES encryption works through using two pieces of data: a key and an IV. The first is the secret key used as part of the encryption and decryption process. Whomever has the key can access the data. The second, the IV, is the initialization vector. This tells the algorithm where to “start” as it encrypts the data. Based on different combinations of key and IV values, the resulting encrypted output will be different.
Because the IV is not protected data, it will often be stored with or as part of the file itself. As long as at least one part of the pair, usually the key itself, is private, parties can encrypt and decrypt the data.
In C#, unless explicitly overwritten, the use of the Create() method of the Aes class will generate new random key and IV values. For the purposes of decryption, both should be saved. As this example code will show, the key can be saved as part of the application and the IV saved as part of the encrypted game data file.
Streams Wrapping Streams Wrapping Streams
C# uses streams for all input and output operations. This also includes working with encryption and decryption as part of a special class called CryptoStream. This stream can often “wrap” other streams classes like FileStream, providing a way to encrypt or decrypt data as it passes from one stream to the next. Often, to help the process of working with streams themselves, a third stream class is used, StreamReader or StreamWriter.
The effect of having three different streams is each “wrapping” the other. Data is read or written to a StreamReader or StreamWriter, which reads or writes data to a CryptoStream, encrypting or decrypting the data as asked, and then, finally, working with a FileStream to read or write the data to a file.
Code Fragment of Multiple Streams
With three different streams, they should be used in a specific order:
- Writing: FileStream –> CryptoStream –> StreamWriter.
- Reading: FileStream –> CryptoStream –> StreamReader
Working with GameData and GameDataManager
According to the concept of encapsulation in object-oriented programming, similar values should be kept as a single unit. This means having one class, GameData, holding the values to be used as part of working with the application and a separate class, GameDataManager, to work with the data operations, “managing” the game data.
System.Serializable
C# uses the concept of attributes to let the run-time know extra information about some data. In Unity, the attribute Serializable or System.Serializable signals to the C# run-time used within Unity some code, usually a class, can be “serialized” (converted from run-time data to another format). It is frequently used as part of other data saving techniques such as working with the JsonUtility class.
In the above example, the class GameData has two fields, Lives and HighScore. Because these are public, they can be accessed by other code.
Where are files in Unity?
While a Unity project is running, it provides a field called Application.persistentDataPath set with the current “persistent” directory. This is set when the project starts and can only be used as part of the Initialization System within Unity as part of messages such as the method Awake() or Start() within a Scripting Component.
JSON File Example
The field Application.persistentDataPath only contains a directory. To use a file, an additional filename is needed such as the above example where a “/” and the name of the file is used.
Writing and Reading IV
Working with Aes adds the extra complication of needing to save the IV used to create the encrypted data in order to read it. As this is not private data, it can be added to the start of a file and then read from the same place.
Code Fragment for Writing IV
Code Fragment for Reading IV
Working with JsonUtility
The use of the Aes class provides a way encrypt or decrypt data. When working with streams, data can be read or written using “layers” of streams. However, none of these operations provide a way to serialize and deserialize data from files into game data. For that purpose, the JsonUtility class can be used.
As the JsonUtility class works on String values, this makes it ideal for use with reading and writing values using the existing StreamReader and StreamWriter classes.
Updated GameDataManager Code
In the new version of the GameDataManager code, the JsonUtility class and its methods are used to serialize the GameData class and its fields into and out of a String format. This is also combined with the existing stream “wrapping” usages to decrypt and deserialize, for reading a file, and serialize and encrypting, for writing to a file.
What do we do with the secret key?
The existing example code shared in this post will encrypt and decrypt game data files using AES. It will also handle all of the streams and work to serialize or deserialize data from a class as needed. However, there is one remaining problem: how should the secret key be handled? Within the current code, the key is generated by the Create() method and then saved. As long as the code is running, this value would exist within the application. It can easily use the value for all encryption related tasks. However, as soon as the application is closed, any attempt to read the files will fail. The key is not saved across sessions.
It’s a matter of trust. The starting issue of this post remains. The central question in any encryption usage centers around issues of trust. If players can be trusted to not change files or the use of their values can be closely checked, encryption may not be needed. If game data should be protected from players, encrypting the data might be a useful step. However, as with the beginning concerns, any use of symmetric encryption means parties need a key to access the data. It has to be saved somewhere.
Saving Symmetric Key in PlayerPrefs
The PlayerPrefs class gives access to the “preferences” data as saved by Unity for the player. This is usually data such as resolution, accessibility, or other settings. It could also be used as a place to save the key as generated by the Aes class.
Saving Key (using Base64 String and Byte[] Conversion) as “Key” Example
In the above example, each time the writeFile() method is used, it generates a new AES key and saves it in the PlayerPrefs (overwriting the previous key). Each new save operation, as a side effect, changes the key used and allows, as long as the key exists as part of PlayerPrefs, for the code to read the encrypted file.
Using Hardcoded Secret Key
It is also possible to “hardcode” the secret in the GameDataManager class, using the same key for all operations as saved within the class itself as a private field.
Hardcoded Secret Key
In the above code, the secretKey field is used for both encryption and decryption processes, referencing the key as part of the class itself.
Защита игр и мобильных приложений от взлома для чайников (Unity, C#, Mono)
Всем снова здравствуйте! Дошли руки написать крутую статью на весьма важную тему для разработчиков игр. Итак, поговорим о защите ваших драгоценных игр и приложений, которые вы пилите на Unity в надежде заработать на буханку хлеба, от взлома злобными школьниками. Почему школьниками? Потому что надежной на 100% защиты априори быть не может. И кто захочет, все равно взломает. Вопрос лишь в том, сколько времени и сил он на это потратит. И как любят шутить безопасники — терморектальный криптоанализ никто не отменял.
- защита данных приложения (сейвов)
- защита памяти приложения
- защита внутриигровых покупок (Google Play)
1. Подготовка
Для начала нужно научиться преобразовывать игровые данные (типы, классы) в строки. Стоит изучить JSON или XML сериализацию. Начинать с XML не советую, т.к. возникнут проблемы с iOS. Лучше изучить JSON, вот ссылка wiki.unity3d.com/index.php/SimpleJSON. К сожалению, это тема отдельной статьи и я не буду на этом останавливаться. Если лениво разбираться — можно по старинке лепить строку вручную с помощью сепараторов. Например:
Еще нужно уметь преобразовывать строки в массивы байт и обратно. Тут все просто:
Далее строку можно завуалировать, применив к ней base64 преобразование. Особо отмечу, что base64 не является шифрованием, он не имеет ключа шифрования и все такое. base64 преобразует вашу строку в новую строку, состоящую только из ASCII символов. Наглядно посмотреть, как это происходит, можно по ссылке base64.ru. Я просто приведу код реализации:
Также отмечу, что base64 работает быстро и сравним по скорости с операцией сложения. Выполнять такие преобразования можно даже в цикле Update.
2. Защита игровых данных (сейвов)
- Application.persistentDataPath может измениться при обновлении приложения (например, приложение переместится на SD карту). Соответственно, файл сохранения будет not found, а пользователь потеряет весь прогресс
- Это не будет работать в web-плеере и windows phone
О да, детка, супер! Теперь нужно зашифровать наши сохранения. Тут можно по-быстрому выполнить base64 преобразование, это уже защитит сохранения от редактирования через большинство программ для взлома. Но по хардкору самое время прикрутить нормальное шифрование. Сразу к делу, берем AES и шифруем. Копипастим файл AES.cs и не задаемся вопросом, как это работает:
3. Защита памяти приложения
Все помнят такую PC программу, как ArtMoney? Она умела искать значения в RAM и за несколько итераций отсеивания позволяла стать миллионером в игре. Сейчас для Android и iOS подобных программ развелось очень много, например самая популярная — GameKiller.
Защититься от таких программ довольно просто — нужно шифровать значения в памяти приложения. Шифровать КАЖДЫЙ РАЗ при записи и дешифровать КАЖДЫЙ РАЗ при чтении. И так как операция довольно частая, нет смысла использовать тяжелый AES и нам нужен супербыстрый алгоритм. Я предлагаю несколько модифицировать наш base64 и реализовать свое шифрование — эффективное, быстрое, с блэкджеком и XOR:
Теперь, как только мы прочитали и расшифровали AES-ом профиль из сохранения, сразу шифруем все значения этим B64X (название я сам придумал). И расшифровываем каждый раз, когда нужно узнать, сколько денег у игрока, какой у него уровень и т.д. B64X может использовать ключ (пароль) для шифрования, а может использовать рандомный сессионный ключ, чтобы мы не парились, где и как его хранить.
4. Защита внутриигровых покупок
Для многих разработчиков эта тема не актуальна и мало кто реализует защиту. В принципе, если у вас многопользовательская игра, то нужно подумать над защитой ее экономики. Есть такая программа — Freedom. Требует рут и, если в двух словах, подменяет сервис внутриигровых покупок. Короче — игрок может совершать покупки за бесплатно.
Опустим рассмотрение механизма проверки покупок на сервере разработчика, ведь не у всех он есть. Расскажу, что предлагает Google в таких случаях.
UPD: Unity реализовал механизм покупок и их проверку (http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html), поэтому информация ниже теперь имеет только теоретическую нагрузку.
При создании приложения в консоли разработчика Google генерирует пару ключей для алгоритма RSA — открытый и закрытый ключ. Если не знаете, что это такое — погуглите асиметричное шифрование. Открытый ключ можно получить в консоли разработчика:
Вы его еще используете при реализаци игрового магазина в приложении.
Закрытый ключ Google вам никогда не покажет и будет использовать его для цифровой подписи покупок. Соответственно закрытым ключом можно только зашифровать подпись, а открытым ключом можно только расшифровать подпись.
Механизм защиты получается довольно простой — Google подписывает все json-ответы сервера покупок, и никто другой такую подпись подделать не может. Разработчик, зная открытый ключ, может проверить цифровую подпись ответов сервера. И если сервер был сфабрикован с помощью Freedom, то цифровая подпись будет неправильная.
Перейдем к реализации. Для начала нужно выполнить одну неприятную операцию. Нужно преобразовать открытый base64 ключ из консоли разработчика в xml-ключ, который подойдет для дешифрования подписи. Свиду кажется, что достаточно просто раскодировать его base64. Но это не так. Предлагаю воспользоваться онлайн сервисом и сразу прикопать xml-ключ в приложении. Особо заморачиваться о его защите не стоит — это же открытый ключ. Его могут сфабриковать, но это уже другая история. Итак, сервис вот, вставляем туда свой base64 ключ и получаем xml-ключ: superdry.apphb.com/tools/online-rsa-key-converter
В нижнем поле и есть наш xml-ключ. Сохраняем его в игре или приложении. А дальше все просто. Google возвращает нам покупку. Если использовать в приложении бесплатный плагин для реализации покупок OpenIAB, то это объект класса Purchase, у него есть 2 нужных нам поля:
Теперь приведу реализацию механизма проверки подписи:
Ну и теперь, когда от Google пришел ответ, что покупка совершена, проверяем ее подпись и показываем игроку фигу, если подпись не совпадает:
Хочу отметить, что при совершении покупки лучше добавить рандомный payload к запросу, это защитит от атак man-in-the-middle, когда вам могут повторно подпихивать корректный ответ сервера с правильной, но одной и той же цифровой подписью. Это необязательный аргумент в реализации OpenIAB, на который большинство кладут болт:
Encrypting Game Data with Unity
It’s a matter of trust. In any context where a game can open, use, or save files where players can also access them, there is always the chance a user, system, or process may change a file. On PC and Mac platforms, there is also a chance a player may simply skip over editing files and change values directly in the RAM itself, subverting any attempt to protect files. Depending on the resources and time available to a player, they could also attempt to decompile parts of the game and figure out where any secret keys are stored within the code or how everything is arranged within its internal data structures.
While some compilation processes can help protect the game’s running memory, and there are approaches to better protecting files such as encrypting them, the root issue is still one of trust. If, as a developer, you can trust your players to not change the game’s files, a more simple solution of reading and writing text files might be the best option. If you anticipate some users might attempt to change values in something like a save file, using something like encryption for sensitive data might be a good option. If you feel you cannot trust your players at all, remember the only perfect solution for complete safety is for no players to ever play your game. All digital interactions come with some level of information risk.
Problems With Existing Game Data File Operations
All of the common solutions for working with game data when using Unity all also come with the risk the player might change the files holding the data. This is true of working with BinaryFormatter and JsonUtility, where game objects are serialized into an easier format to save the data, and with using PlayerPrefs as well. Through using local files on the local system, they all risk a player, or some other user, system, or process, accidently or purposely changing the data files.
One approach to solving part of these issues is to use encryption. This makes the files harder to understand for systems, process, and users who hope to overwrite them with malicious values. However, it will not protect against accidental corruption. There is still the possibility of a file getting corrupted for any numbers of reasons.
Using encryption also comes with greater complexity for the developer. As part of using the data, it must be encrypted before being written to a file and unencrypted before using the values. This introduces extra processing time, and the need for more code as part of the saving and loading operations.
Symmetric and Asymmetric Encryption
The terms “symmetric” and “asymmetric,” as used with encryption, answer the question of “Who is trusted?” In symmetric encryption, at least two parties have the “secret key” used to create the encrypted data and can use it to read and write the data. The use of the “secret key” gives them access. This is different from asymmetric encryption, which is also called public-key encryption. In this case, there are two keys used to generate the encrypted data: a public key and a private key. The public key is “known” and can be used to generate the encrypted data. The private key, when used with the public key, can be used to decrypt the data.
In general, asymmetric encryption is far stronger than symmetric encryption. However, it is also more complex and much slower relative to the data size. Put in very general terms:
- Symmetric: Faster, but less secure.
- Asymmetric: Slower, but more secure.
Generally, because asymmetric encryption depends on a known public key, it is not as useful for game data. It could be used in situations where a server was involved and the public key accessed, but most games install to a local device without access to a public key. This makes symmetric encryption a “good” choice for most use cases where encryption might be needed, but there is not the ability to more widely share a public key when using the game.
DES (Data Encryption Standard) and AES (Advanced Encryption Standard)
The acronyms DES and AES refer to “Standards,” but are also algorithms following the standard on which they are based. Because of some known attacks on DES, many developers use AES instead of DES. However, DES can still be used for many situations.
While C# supports both DES and AES, AES will be used in this post as part of example code.
Understanding AES
The class Aes is part of the namespace System.Security.Cryptography.
Because Aes works on data itself, all of of its operations work on byte arrays, reading or writing data without specific data types.
What is key and IV?
AES encryption works through using two pieces of data: a key and an IV. The first is the secret key used as part of the encryption and decryption process. Whomever has the key can access the data. The second, the IV, is the initialization vector. This tells the algorithm where to “start” as it encrypts the data. Based on different combinations of key and IV values, the resulting encrypted output will be different.
Because the IV is not protected data, it will often be stored with or as part of the file itself. As long as at least one part of the pair, usually the key itself, is private, parties can encrypt and decrypt the data.
In C#, unless explicitly overwritten, the use of the Create() method of the Aes class will generate new random key and IV values. For the purposes of decryption, both should be saved. As this example code will show, the key can be saved as part of the application and the IV saved as part of the encrypted game data file.
Streams Wrapping Streams Wrapping Streams
C# uses streams for all input and output operations. This also includes working with encryption and decryption as part of a special class called CryptoStream. This stream can often “wrap” other streams classes like FileStream, providing a way to encrypt or decrypt data as it passes from one stream to the next. Often, to help the process of working with streams themselves, a third stream class is used, StreamReader or StreamWriter.
The effect of having three different streams is each “wrapping” the other. Data is read or written to a StreamReader or StreamWriter, which reads or writes data to a CryptoStream, encrypting or decrypting the data as asked, and then, finally, working with a FileStream to read or write the data to a file.
Code Fragment of Multiple Streams
With three different streams, they should be used in a specific order:
- Writing: FileStream –> CryptoStream –> StreamWriter.
- Reading: FileStream –> CryptoStream –> StreamReader
Working with GameData and GameDataManager
According to the concept of encapsulation in object-oriented programming, similar values should be kept as a single unit. This means having one class, GameData, holding the values to be used as part of working with the application and a separate class, GameDataManager, to work with the data operations, “managing” the game data.
System.Serializable
C# uses the concept of attributes to let the run-time know extra information about some data. In Unity, the attribute Serializable or System.Serializable signals to the C# run-time used within Unity some code, usually a class, can be “serialized” (converted from run-time data to another format). It is frequently used as part of other data saving techniques such as working with the JsonUtility class.
In the above example, the class GameData has two fields, Lives and HighScore. Because these are public, they can be accessed by other code.
Where are files in Unity?
While a Unity project is running, it provides a field called Application.persistentDataPath set with the current “persistent” directory. This is set when the project starts and can only be used as part of the Initialization System within Unity as part of messages such as the method Awake() or Start() within a Scripting Component.
JSON File Example
The field Application.persistentDataPath only contains a directory. To use a file, an additional filename is needed such as the above example where a “/” and the name of the file is used.
Writing and Reading IV
Working with Aes adds the extra complication of needing to save the IV used to create the encrypted data in order to read it. As this is not private data, it can be added to the start of a file and then read from the same place.
Code Fragment for Writing IV
Code Fragment for Reading IV
Working with JsonUtility
The use of the Aes class provides a way encrypt or decrypt data. When working with streams, data can be read or written using “layers” of streams. However, none of these operations provide a way to serialize and deserialize data from files into game data. For that purpose, the JsonUtility class can be used.
As the JsonUtility class works on String values, this makes it ideal for use with reading and writing values using the existing StreamReader and StreamWriter classes.
Updated GameDataManager Code
In the new version of the GameDataManager code, the JsonUtility class and its methods are used to serialize the GameData class and its fields into and out of a String format. This is also combined with the existing stream “wrapping” usages to decrypt and deserialize, for reading a file, and serialize and encrypting, for writing to a file.
What do we do with the secret key?
The existing example code shared in this post will encrypt and decrypt game data files using AES. It will also handle all of the streams and work to serialize or deserialize data from a class as needed. However, there is one remaining problem: how should the secret key be handled? Within the current code, the key is generated by the Create() method and then saved. As long as the code is running, this value would exist within the application. It can easily use the value for all encryption related tasks. However, as soon as the application is closed, any attempt to read the files will fail. The key is not saved across sessions.
It’s a matter of trust. The starting issue of this post remains. The central question in any encryption usage centers around issues of trust. If players can be trusted to not change files or the use of their values can be closely checked, encryption may not be needed. If game data should be protected from players, encrypting the data might be a useful step. However, as with the beginning concerns, any use of symmetric encryption means parties need a key to access the data. It has to be saved somewhere.
Saving Symmetric Key in PlayerPrefs
The PlayerPrefs class gives access to the “preferences” data as saved by Unity for the player. This is usually data such as resolution, accessibility, or other settings. It could also be used as a place to save the key as generated by the Aes class.
Saving Key (using Base64 String and Byte[] Conversion) as “Key” Example
In the above example, each time the writeFile() method is used, it generates a new AES key and saves it in the PlayerPrefs (overwriting the previous key). Each new save operation, as a side effect, changes the key used and allows, as long as the key exists as part of PlayerPrefs, for the code to read the encrypted file.
Using Hardcoded Secret Key
It is also possible to “hardcode” the secret in the GameDataManager class, using the same key for all operations as saved within the class itself as a private field.
Hardcoded Secret Key
In the above code, the secretKey field is used for both encryption and decryption processes, referencing the key as part of the class itself.