Опубликовав свой первый проект в Steam «любовался» достаточно неплохим количеством скачиваний. Только какой в этом толк, если вся эта движуха происходила на торрент-трекерах...

Поэтому всерьез задумался о защите своих коммерческих проектов от пиратов.

Конечно, универсального способа защиты от пиратов не существует и тема защиты от пиратства является как-никак актуальной: темой постоянных дискуссий и споров.

В рамках данной статьи рассмотрим вариант дополнительной защиты Unity-проекта (под Windows) с использованием библиотеки kernel32.dll. А использовать данный способ защиты в своем проекте, либо не использовать – решать вам. И так приступим.

На нулевой нашей сцене создадим объект с названием SecurityManager и повесим на него скрипт с названием Security.

Подключаем необходимые библиотеки:

using UnityEngine;
using System;
using System.Text;
using System.IO;

Объявим необходимые переменные.

private string sn = "";
public string folder = "/Data";
public string fileName = "Settings.dat";
public string code = "AG7XpyPfmwN28193";

Значение переменной code придумываем любое. Желательно с помощью генератора паролей.

Создаем функцию с именем DebugSave() и пишем следующий код.

private void DebugSave()
    {

        try //обработка исключений
        {

            sn = code;

            //кодируем в двоичный код
            byte[] buf = Encoding.UTF8.GetBytes(sn);
            StringBuilder sb = new StringBuilder(buf.Length * 8);
            foreach (byte b in buf)
            {
                sb.Append(Convert.ToString(b, 2).PadLeft(8, '0'));
            }
            string binaryStr = sb.ToString();


            //создаем папку в директории проекта
            Directory.CreateDirectory(Application.dataPath + folder);

            //сохраняем в файл
            using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Create))
            {
                using (var writer = new BinaryWriter(stream, Encoding.UTF8, false))
                {
                    
                    writer.Write(binaryStr);
                    writer.Close();
                }
            }


        }
        catch
        {
            Debug.Log(message: "Ошибка чтения в файл"); //выводим сообщение об ошибке
        }

    }

И вызовем ее при старте сцены:

    void Start()
    {
        DebugSave();
    }

При выполнении функции DebugSave() в директории проекта создается папка с именем Data. В папке Data создается бинарный файл с именем Settings.dat. Значение переменной code кодируется в двоичный код и записывается в файл Settings.dat. Кодируем в двоичный код для того, чтобы обычный юзер не смог прочитать значение в файле с помощью блокнота

Теперь приступим к реализации непосредственно самой защиты. Код работы с библиотекой kernel32.dll подсмотрел ТУТ.

Конечно, данную защиту можно реализовать с помощью стандартной юнитовской команды SystemInfo.deviceUniqueIdentifier, но в случае апгрейда ПК (замены процессора, прошивки BIOS) наш проект будет ругаться о пиратстве. Нас такой расклад не устраивает.

Подключаем следующую библиотеку:

using System.Runtime.InteropServices;

И объявляем необходимые переменные:

[DllImport("kernel32.dll")]
private static extern long GetVolumeInformation(
string PathName,
StringBuilder VolumeNameBuffer,
UInt32 VolumeNameSize,
ref UInt32 VolumeSerialNumber,
ref UInt32 MaximumComponentLength,
ref UInt32 FileSystemFlags,
StringBuilder FileSystemNameBuffer,
UInt32 FileSystemNameSize);

public string disk; // задать в инспекторе  "C:\"

Для переменной disk в инспекторе указываем "C:\". В самом скрипте не получается – ругается IDE.

Создаем функцию с именем Getvolumeinformation() и пишем следующий код:

    private void Getvolumeinformation() //считываем системную информацию
    {
        string drive_letter = disk;
        drive_letter = drive_letter.Substring(0, 1) + ":\\";

        uint serial_number = 0;
        uint max_component_length = 0;
        StringBuilder sb_volume_name = new StringBuilder(256);
        UInt32 file_system_flags = new UInt32();
        StringBuilder sb_file_system_name = new StringBuilder(256);

        if (GetVolumeInformation(drive_letter, sb_volume_name,
            (UInt32)sb_volume_name.Capacity, ref serial_number,
            ref max_component_length, ref file_system_flags,
            sb_file_system_name,
            (UInt32)sb_file_system_name.Capacity) == 0)
        {
            Debug.Log(message: "Error getting volume information.");
        }
        else
        {
            sn = serial_number.ToString(); //серийный номер
            Debug.Log(message: sn);
        }
    }

Сразу добавим функцию, отвечающую за вывод системных сообщений:

public static class NativeWinAlert
    {
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        private static extern System.IntPtr GetActiveWindow();

        public static System.IntPtr GetWindowHandle()
        {
            return GetActiveWindow();
        }

        [DllImport("user32.dll", SetLastError = true)]
        static extern int MessageBox(IntPtr hwnd, String lpText, String lpCaption, uint uType);

        /// <summary>
        /// Shows Error alert box with OK button.
        /// </summary>
        /// <param name="text">Main alert text / content.</param>
        /// <param name="caption">Message box title.</param>
        public static void Error(string text, string caption)
        {
            try
            {
                MessageBox(GetWindowHandle(), text, caption, (uint)(0x00000000L | 0x00000010L));
                Debug.Log("Игра закрылась");
                Application.Quit();    // закрыть приложение
            }
            catch (Exception ex) { }
        }
    }

Код функции, отвечающую за вывод системных сообщений, позаимствовал ТУТ.

Создаем функцию с именем Save() и пишем следующий код:

    private void Save()
    {

        try //обработка исключений
        {

            //кодируем в двоичный код
            byte[] buf = Encoding.UTF8.GetBytes(sn);
            StringBuilder sb = new StringBuilder(buf.Length * 8);
            foreach (byte b in buf)
            {
                sb.Append(Convert.ToString(b, 2).PadLeft(8, '0'));
            }
            string binaryStr = sb.ToString();

            //сохраняем в файл
            using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Create))
            {
                using (var writer = new BinaryWriter(stream, Encoding.UTF8, false))
                {

                    writer.Write(binaryStr);
                    writer.Close(); //закрываем файл
                }
            }


        }
        catch
        {
            Debug.Log(message: "Ошибка чтения в файл"); //выводим сообщение об ошибке
        }

    }

Функция перевода двоичного когда в текст:

public static string BinaryToString(string data) 
    {
        List<Byte> byteList = new List<Byte>();

        for (int i = 0; i < data.Length; i += 8)
        {
            byteList.Add(Convert.ToByte(data.Substring(i, 8), 2));
        }

        return Encoding.ASCII.GetString(byteList.ToArray());
    }

Создаем функцию с именем Set() и пишем следующий код:

private void Set()
    {
        if (File.Exists(Application.dataPath + folder + "/" + fileName)) //проверяем наличие файла, если его нет выводим сообщение о приатстве.
        {
            using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Open))
            {
                using (var reader = new BinaryReader(stream, Encoding.UTF8, false))
                {
                    string binaryStr = reader.ReadString();
                    reader.Close(); //закрываем файл

                    //двоичный код переобразовываем в строку
                    string resultText = BinaryToString(binaryStr);
                    
                    Getvolumeinformation(); //читаем серийный номер диска

                    if ((resultText == code) || (resultText == sn))
                    {
                        if (resultText == code)
                        {
                            Save();
                        }
                            

                    }
                    else
                    {
                        Debug.Log(message: "Пират!"); //выводим сообщение об ошибке
                        NativeWinAlert.Error("This copy of game is not genuine.", "Error");
                    }

                }
            }

        }
        else
        {
            Debug.Log(message: "Пират!"); //выводим сообщение об ошибке
            NativeWinAlert.Error("This copy of game is not genuine.", "Error");
        }

    }

При выполнении функции Set() вначале проверяем наличие файла Settings.dat, если его нет, то выводим сообщение о пиратстве (вызываем функцию NativeWinAlert). Если файл Settings.dat существует, читаем его, двоичный код преобразовываем в текст, считываем значение серийного номера тома. И выполняем очередную проверку, если значение не равняется значению переменной code или значению серийному номеру тома С, то выводим сообщение о пиратстве (вызываем функцию NativeWinAlert), в противном случае в файл Settings.dat записываем значение серийного номера тома С.

В функции Start() закоментируем вызов функции DebugSave() (она нужна только нам) и добавим вызов функции Set().

    void Start()
    {
        Set(); 
        //DebugSave(); //записываем в файл значение переменной code
    }

Теперь при первом запуске нашего проекта, значение в файле Settings.dat перезаписывается на значение серийного номера тома С. После данной процедуры копию проекта нельзя будет запустить на другой машине.

Исходный файл проекта располагается на сервисе GitHub по следующей ссылке.