Введение
В криптографических реализациях на Delphi, особенно при работе с XOR-шифрованием и одноразовым блокнотом, программисты часто допускают систематические ошибки, коренящиеся в непонимании фундаментального различия между текстовыми (String) и бинарными (TBytes) данными и их представлениями в бинарных строк (Binary string). Это исследование анализирует типичные ошибки, их причины, последствия и предоставляет практические рекомендации для корректной реализации .
1. Криптографические основы: XOR-шифрование и одноразовый блокнот
1.1 XOR-шифрование
XOR-шифрование — это аддитивный шифр, использующий операцию исключающего ИЛИ (XOR) между открытым текстом и ключом. Алгоритм основан на свойствах операции XOR: A⊕0=A, A⊕A=0, A⊕B=B⊕A, (A⊕B)⊕C=A⊕(B⊕C). При использовании повторяющегося ключа этот метод уязвим для частотного анализа, но с полностью случайным ключом, равным по длине сообщению и использованным однократно, он превращается в теоретически невзламываемый одноразовый блокнот .
1.2 Одноразовый блокнот (OTP)
Одноразовый блокнот — это криптографический метод, обеспечивающий абсолютную секретность при соблюдении четырёх условий:
- Ключ должен быть не короче открытого текста
- Ключ должен быть действительно случайным (truly random)
- Ключ никогда не должен повторно использоваться (даже частично)
- Ключ должен оставаться полностью секретным для всех, кроме законных участников обмена
Нарушение любого из этих условий делает систему уязвимой. Математически доказано, что при соблюдении всех условий OTP обеспечивает совершенную секретность .
2. Типы данных в Delphi: фундаментальное различие
2.1 String (UnicodeString в современных версиях)
В Delphi 2009 и выше тип String представляет собой UnicodeString с кодировкой UTF-16. Каждый символ занимает 2 байта (16 бит), что обеспечивает поддержку всех языков мира, включая кириллицу. Однако это текстовый тип, предназначенный для хранения символов, а не произвольных бинарных данных. Путаницу дополнительно вносят руководящие документы, спецификации, стандарты по алгоритмам шифрования - в криптографии под понятием "строка" понимается Binary string (символьное представление массива байтов в виде 1 и 0), а в языках программирования String - буквально "строка" - текстовая строка символов.
Внутреннее представление:
- Хранит символы в кодировке UTF-16LE (Little Endian)
- Автоматически управляет памятью и кодировками
- Индексация начинается с 1
- Функция Length() возвращает количество символов, а не байтов
2.2 TBytes (бинарные данные)
TBytes — это динамический массив байтов (array of Byte), предназначенный для хранения произвольных бинарных данных без преобразований кодировок. Это правильный выбор для криптографических операций, работы с файлами, сетевыми протоколами и другими бинарными форматами. Для визуализации бинарных данных используют "двоичные строки" (Binary string).
Ключевые характеристики:
- Не выполняет автоматических преобразований кодировок
- Сохраняет все байтовые значения (0-255) без изменений
- Индексация начинается с 0
- Length() возвращает количество байтов
2.3 Преобразования между типами
Для корректного преобразования необходимо явно указывать кодировку:
delphi
// String → TBytes
var
Bytes: TBytes;
Text: string;
begin
Bytes := TEncoding.UTF8.GetBytes(Text); // Для UTF-8
Bytes := TEncoding.Unicode.GetBytes(Text); // Для UTF-16
end;
// TBytes → String
var
Text: string;
Bytes: TBytes;
begin
Text := TEncoding.UTF8.GetString(Bytes);
Text := TEncoding.Unicode.GetString(Bytes);
end;
Прямое приведение типов string(MyBytes) или TBytes(MyString) без учёта кодировки приводит к искажению данных и является распространённой ошибкой .
3. Систематизация типичных ошибок
3.1 Категория 1: Ошибки выбора типа данных для хранения зашифрованных данных
Проблема: Использование String вместо TBytes для результатов шифрования.
Конкретные ошибки:
- Хранение шифротекста в переменных типа String
- Возврат String из функций шифрования вместо TBytes
- Использование TStringList.Text для объединения бинарных данных (добавляет символы перевода строк)
Пример некорректного кода:
delphi
function EncryptXOR(Text, Key: String): String; // Ошибка: возвращает String
var
i: Integer;
begin
Result := '';
for i := 1 to Length(Text) do
Result := Result + Chr(Ord(Text[i]) xor Ord(Key[1 + ((i-1) mod Length(Key))]));
end;
Проблемы:
- Результат XOR может содержать байт 0, который интерпретируется как конец строки
- Автоматические преобразования кодировок искажают данные
- Невозможно корректно сохранить или передать бинарные данные
Решение: Всегда использовать TBytes для криптографических данных .
3.2 Категория 2: Ошибки преобразования между текстовыми и бинарными представлениями
Проблема: Неправильные преобразования между String и TBytes без учёта кодировки.
Конкретные ошибки:
- Прямое приведение типов: MyString := string(MyBytes)
- Использование BytesOf()/StringOf() без учёта кодировки UTF-16
- Смешивание TEncoding.UTF8 и TEncoding.Unicode при работе с одним набором данных
Пример: Приведение TBytes к String без нулевого терминатора приводит к чтению случайных данных из памяти .
Решение: Всегда явно указывать кодировку при преобразованиях:
delphi
// Правильное преобразование
Bytes := TEncoding.UTF8.GetBytes(Text);
Text := TEncoding.UTF8.GetString(Bytes);
3.3 Категория 3: Ошибки обработки нулевых байтов
Проблема: При операции XOR вероятность получения байта 0 составляет 1/256. В строковых типах нулевой байт интерпретируется как конец строки.
Конкретные ошибки:
- Отображение зашифрованных данных в TMemo или других текстовых контролах
- Использование строковых функций (Length(), Copy(), Pos()) для обработки шифротекста
- Логирование бинарных данных как строк без предварительного кодирования (Base64, Hex)
Пример: Если szOriginal[i] == szKey[i], результат XOR будет 0, создавая ложный терминатор строки .
Решение: Для хранения и отображения использовать кодировки Base64 или Hex:
delphi
// Кодирование в Base64 для отображения/передачи
function BytesToBase64(const Bytes: TBytes): string;
begin
Result := TNetEncoding.Base64.EncodeBytesToString(Bytes);
end;
3.4 Категория 4: Ошибки работы с кодировками и Unicode
Проблема: В UnicodeString каждый символ занимает 2+ байта, а криптографические алгоритмы работают с байтами.
Конкретные ошибки:
- Шифрование String напрямую без преобразования в TBytes
- Неправильный расчет размеров: j := Length(Str) даёт символы, а нужно байты
- Использование AnsiString в Unicode-версиях Delphi для “совместимости”
Пример ошибки с Unicode:
delphi
// Некорректно для Unicode
for i := 1 to Length(Text) do // Length возвращает символы, не байты
ByteValue := Ord(Text[i]); // Ord от Char возвращает код символа, а не байт
Решение: Всегда работать с байтами внутри алгоритмов шифрования:
delphi
function XorEncrypt(const Data, Key: TBytes): TBytes;
var
i: Integer;
begin
SetLength(Result, Length(Data));
for i := 0 to High(Data) do
Result[i] := Data[i] xor Key[i mod Length(Key)];
end;
3.5 Категория 5: Ошибки обработки ключей
Проблема: Ключи — это бинарные данные, а не текст. Текстовые пароли должны преобразовываться в бинарные ключи.
Конкретные ошибки:
- Хранение ключей в переменных типа String
- Использование текстовых ключей разной длины
- Отсутствие преобразования текстовых паролей в бинарные ключи фиксированной длины
- Повторное использование ключей в одноразовом блокноте
Критическая ошибка: Повторное использование ключа в OTP полностью компрометирует безопасность, позволяя восстановить ключ при наличии нескольких зашифрованных сообщений .
Решение: Использовать Key Derivation Function (KDF) для преобразования паролей:
delphi
// Пример использования PBKDF2 для получения ключа из пароля
function DeriveKey(const Password: string; Salt: TBytes; KeyLength: Integer): TBytes;
begin
// Использовать PBKDF2 или аналогичную функцию
end;
3.6 Категория 6: Ошибки реализации побайтовых операций
Проблема: Неправильные индексные вычисления и приведение типов при побайтовых операциях.
Конкретные ошибки:
- Индексация: в String начинается с 1, в TBytes — с 0
- Операции над Char (2 байта) вместо Byte (1 байт)
- Неправильное использование Ord() и Chr() для байтовых значений
Пример некорректного кода:
delphi
// Несколько ошибок в одном примере
function XorStrings(Str1, Str2: UTF8String): UTF8String;
var
i: Integer;
Ci: Char;
begin
for i := 0 to Length(Str1) - 1 do
begin
Ci := Char(Ord(Str1) xor Ord(Str2)); // Ошибка: Ord от всей строки
Result := Result + Ci;
end;
end;
Решение: Единообразно работать с индексами от 0:
delphi
// Корректная реализация
function XorBytes(const Data, Key: TBytes): TBytes;
var
i: Integer;
begin
SetLength(Result, Length(Data));
for i := 0 to High(Data) do
Result[i] := Data[i] xor Key[i mod Length(Key)];
end;
3.7 Категория 7: Ошибки взаимодействия с внешними системами
Проблема: Шифрованные данные передаются между системами с разными представлениями строк.
Конкретные ошибки:
- Отправка бинарных данных как текст через HTTP/JSON без кодирования
- Несовпадение кодировок по умолчанию в Delphi, PHP, JavaScript
- Ожидание текстового представления там, где нужны бинарные данные
Пример проблемы Delphi-PHP: Разное поведение при XOR-операциях из-за различий в обработке строк и кодировок .
Решение: Использовать Base64 для межсистемного взаимодействия:
delphi
// Delphi → PHP совместимость
function EncryptForPHP(const Text, Key: string): string;
var
DataBytes, KeyBytes, EncryptedBytes: TBytes;
begin
DataBytes := TEncoding.UTF8.GetBytes(Text);
KeyBytes := TEncoding.UTF8.GetBytes(Key);
EncryptedBytes := XorBytes(DataBytes, KeyBytes);
Result := TNetEncoding.Base64.EncodeBytesToString(EncryptedBytes);
end;
4. Практические рекомендации
4.1 Общие принципы
- Всегда использовать TBytes для криптографических данных — ключи, шифротекст, открытый текст
- Явно указывать кодировки при преобразованиях — TEncoding.UTF8 для текста
- Работать только с байтами внутри алгоритмов — преобразовывать в TBytes на входе и обратно на выходе
- Использовать Base64 или Hex для хранения и передачи — кодировать бинарные данные для логов, БД, сетевой передачи
- Тестировать с нулевыми байтами и Unicode-символами — включать в тестовые данные байт 0 и символы за пределами ASCII
4.2 Корректная архитектура функции шифрования
delphi
// Правильная сигнатура функций
function XorEncrypt(const Data, Key: TBytes): TBytes; overload;
function XorEncrypt(const Text, Key: string): string; overload;
// Реализация
function XorEncrypt(const Data, Key: TBytes): TBytes;
var
i: Integer;
begin
if Length(Key) = 0 then
raise Exception.Create('Key cannot be empty');
SetLength(Result, Length(Data));
for i := 0 to High(Data) do
Result[i] := Data[i] xor Key[i mod Length(Key)];
end;
function XorEncrypt(const Text, Key: string): string;
var
DataBytes, KeyBytes, EncryptedBytes: TBytes;
begin
// Явное преобразование с указанием кодировки
DataBytes := TEncoding.UTF8.GetBytes(Text);
KeyBytes := TEncoding.UTF8.GetBytes(Key);
EncryptedBytes := XorEncrypt(DataBytes, KeyBytes);
// Кодирование в Base64 для безопасного возврата как строки
Result := TNetEncoding.Base64.EncodeBytesToString(EncryptedBytes);
end;
4.3 *Учебная демонстрационная реализация одноразового блокнота в Delphi
delphi
type
TOneTimePad = class
private
FKey: TBytes;
FUsed: Boolean;
public
constructor Create(KeyLength: Integer);
function Encrypt(const Data: TBytes): TBytes;
function Decrypt(const Data: TBytes): TBytes;
property Used: Boolean read FUsed;
end;
constructor TOneTimePad.Create(KeyLength: Integer);
begin
SetLength(FKey, KeyLength);
// Генерация действительно случайного ключа
RandomBytes(FKey[0], KeyLength);
FUsed := False;
end;
function TOneTimePad.Encrypt(const Data: TBytes): TBytes;
begin
if FUsed then
raise Exception.Create('Key already used');
if Length(Data) > Length(FKey) then
raise Exception.Create('Data longer than key');
Result := XorEncrypt(Data, FKey);
FUsed := True;
end;
5. Проверка актуальности на 2026 год
Анализ современных источников подтверждает, что основные принципы работы с бинарными данными в Delphi остаются неизменными. Рекомендации по использованию TBytes для криптографических операций актуальны для всех современных версий Delphi, включая Alexandria и более поздние. Проблемы с нулевыми байтами и неправильной интерпретацией бинарных данных как строковых продолжают быть распространёнными ошибками даже в 2026 году .
Ключевые изменения в современных версиях Delphi:
- Улучшенная поддержка кодировок через класс TEncoding
- Расширенные возможности работы с TBytes в RTL
- Более строгая типизация, помогающая избегать некоторых ошибок приведения типов
Заключение
Типичные ошибки программистов на Delphi при реализации XOR-шифрования и одноразового блокнота коренятся в фундаментальной путанице между текстовыми (String) и бинарными (TBytes) данными. Криптографические алгоритмы работают с байтами, а не с символами, и любое некорректное преобразование между этими представлениями приводит к искажению данных и нарушению безопасности.
Ключевые выводы:
- Всегда используйте TBytes для хранения и обработки криптографических данных
- Явно указывайте кодировки при преобразовании между строками и байтами
- Помните о проблеме нулевых байтов и используйте Base64/Hex для хранения и передачи
- Для одноразового блокнота соблюдайте все четыре условия безопасности
- Тестируйте реализации с разнообразными данными, включая нулевые байты и Unicode-символы
Соблюдение этих принципов позволит избежать распространённых ошибок и создавать корректные, безопасные криптографические реализации на Delphi.
*Учебная демонстрационная реализация - не соответствует принципам, изложенными В.А. Котельниковым в "Основные положения автоматической зашифровки" и К. Шенноном в "Теория связи в секретных системах" (One-Time Pad Cipher)