Рейтинг:0

Шифрование одноразовым блокнотом на C

флаг vn

Я возился с криптографией (для развлечения) и создал свой собственный скрипт шифрования одноразового блокнота на C. Теперь, с учетом сказанного, я свободно признаю, что я ни в коем случае не эксперт по криптографии. Я знаю, что Правило № 1 криптографии — не делать это самому. Однако меня искренне интересует, безопасен ли мой сценарий шифрования (теоретически).

Во-первых, вот базовый отчет о том, чего я пытаюсь достичь с помощью моего метода шифрования:

Цель состоит в том, чтобы обеспечить шифрование одноразовым блокнотом, при котором используются (1) хэш-таблица и (2) ключ шифрования. Хэш-таблица (в данном случае) предварительно закодирована со значениями от 01 до 98.

Ключ шифрования рассчитывается следующим образом:

  1. Получить случайный пользовательский ввод (как минимум той же длины, что и сообщение)
  2. Получить соответствующий индекс для char в validChars[] (исходный код ниже)
  3. Получите число в defaultHashTable, которое соответствует индексу #2.

Сообщение шифруется следующим образом:

  1. Возьмите сообщение и преобразуйте его в соответствующие значения defaultHashTable.
  2. Возьмите ранее сгенерированный ключ и добавьте его в преобразованное сообщение
  3. Теперь он зашифрован (при условии, что тот же ключ больше никогда не используется)

Например:

  1. Сообщение: привет
  2. Преобразование в соответствующие значения defaultHashTable: привет -> 0805121215
  3. Получить случайные символы для ключа: abkdh
  4. Преобразование в соответствующие значения defaultHashTable: abkdh -> 0102110408
  5. Добавляем ключ: 0805121215 + 0102110408 = 0907231623
  6. Зашифрованное сообщение: 0907231623

Вот исходный код (ПРИМЕЧАНИЕ: это комбинация функций, которые были в отдельных заголовочных файлах C, поэтому я не публикую функцию main()):

// Ключ (для сообщения) будет максимум [50,000][3] (таким образом, массив, где каждый состоит из 2 символов):
структура typedef {
    символьный ключ[MAX_SIZE][3];
} Ключ;
ключ глобальный ключ;

// В этой структуре будет возвращено зашифрованное сообщение (поскольку это простой способ вернуть двумерный массив из функции на C):
структура typedef {
    char зашифрованное сообщение[MAX_SIZE][3];
} ЗашифрованноеСообщение;

// Хеш-таблица представляет собой предварительно закодированный двумерный массив (который загружается в initDefaultHashTable()), который будет использоваться вместе с ключом для шифрования сообщений:
// ПРИМЕЧАНИЕ. Существует еще один режим шифрования, который я не упомянул (в попытке сделать это кратким), когда вам нужно вручную вводить случайные двузначные числа (97 из них) для хеш-таблицы.
структура typedef {
    хеш-таблица символов[HASHTABLE_CAPACITY][3];
} DefaultHashTable;
DefaultHashTable defaultHashTable; // Объявить глобальную defaultHashTable, в которой будет храниться эта хэш-таблица

// Загружаем defaultHashTable с 1-98 соответственно:
недействительным initDefaultHashTable () {
    for (int i = 0; i < HASHTABLE_CAPACITY; i++){
        символ с[3];
        sprintf(s, "%d", (i+1));

        если (я < 10){
            символ tmp = с [0];
            с[0] = '0';
            с[1] = тмп;
        }

        для (int j = 0; j < 2; j++) {
            defaultHashTable.hashtable[i][j] = s[j];
        }
    }
}

// Допустимые символы, которые может содержать сообщение (из них 97):
char validChars[] = {'a','b','c','d','e','f','g','h','i','j','k', «л», «м», «н», «о», «р», «к», «р», «с», «т», «у», «в», «ш», «х». ','y','z','A','B','C','D','E','F','G','H','I','J', «К», «L», «М», «N», «О», «П», «Q», «R», «S», «Т», «U», «V», «W». ','X','Y','Z','!','@','#','$','%','^','&','*','(', ')','-','+',' ',',','.',':',';','\'','\"','[',']',' {','}','_','=','|','\','/','<','>','?','`','~','\ п','\т','0','1','2','3','4','5','6','7','8','9'};

// Для char возврат невозможен (я чувствую, что есть лучший способ сделать эту часть):
char FAILED = (char) 255;

// Находим индекс допустимого символа (из validChars) или FALSE, если он не существует:
int findChar (символ c) {
    for (int i = 0; i < strlen(validChars); i++){
        если (validChars[i] == c){
            вернуть я;
        }
    }
    вернуть ЛОЖЬ;
}

// Возвращаем char из validChars с заданным индексом:
char returnChar (индекс int) {
    вернуть валидные символы [индекс];
}

// Получить индекс заданного значения хеш-таблицы (из defaultHashTable), а затем, если применимо, соответствующий символ в validChars:
char findHashTableValue(char n[3], хэш-таблица char[][3]){
    for (int i = 0; i < HASHTABLE_CAPACITY; i++){
        если (хэш-таблица[i][0] == n[0] && хэш-таблица[i][1] == n[1])
            вернуть returnChar(i);
    }
    вернуть НЕУДАЧНО;
}

// ГЛАВНАЯ часть кода (основная функция c вызовет это): Шифрование с использованием одноразового шифрования блокнота, но с использованием хеш-таблицы по умолчанию для экономии времени:
void goThroughLightEnryptionProcess(char * str, char * write_to_file){
     // Загружаем defaultHashTable:
    initDefaultHashTable();

    // Использует функцию для создания случайного ключа (на основе случайного пользовательского ввода):
    generateRandomKey(strlen(str), MAX_SIZE, FALSE);

    // Зашифровать сообщение:
    EncryptedMessage encryptMsg = otpEncrypt (defaultHashTable.hashtable, str, globalKey.key);

    // Прокручиваем и печатаем содержимое (если не записываем в файл):
    если (write_to_file == NULL){
        for (int i = 0; i < strlen(str); i++){
            для (int j = 0; j < 2; j++) {
                printf("%c", encryptMsg.encryptedMessage[i][j]);
            }
        }
        printf("\n");
    } еще {
        // Записываем зашифрованное сообщение в файл:
        // ПРИМЕЧАНИЕ: это еще один параметр, который вы можете передать в реальной программе (которая намного больше этой), где вы можете записать его в файл, а не отображать в терминале:
        // ПРИМЕЧАНИЕ. Я не включил сюда этот код массива writeFileWithTwoDimensionalArray(), потому что он не имеет значения, так как он просто записывает в файл.
        writeFileWithTwoDimensionalArray(encryptMsg.encryptedMessage, HASHTABLE_CAPACITY, write_to_file);
    }
}

// (Вспомогательная функция) Загрузите массив из двух символов в ключ:
недействительным loadIntoKeyForRandoKey (целое число, char n [3]) {
    для (целое я = 0; я < 2; я ++) {
        globalKey.key[at][i] = n[i];
    }
}

// Генерируем ключ на основе случайного пользовательского ввода:
void generateRandomKey (int password_length, int max_size, bool use_global) {
    // @Использует globalHashTable | по умолчаниюHashTable
    // @Использует глобальный ключ
    символьный ответ[max_size];

    printf("Введите случайные символы для ключа (a-z,A-Z,!@#$%%^&*()-+=, минимальная длина %d): ", password_length);
    fgets(ответ, max_size, стандартный ввод);

    // Удалить символ '\n' в конце:
    символ * р;
    если ((p = strchr(ответ, '\n')) != NULL){
        *р = '\0';
    } еще {
        scanf("%*[^\n]");
        сканф("%*с");
    }

    // Убедитесь, что пользовательский ввод >= password_length:
    если (strlen(ответ) < длина_пароля){
        printf("\n[ ОШИБКА ] : Случайные символы должны быть больше или равны %d.\n", password_length);
        вернуть generateRandomKey (длина_пароля, максимальный_размер, использование_глобального);
    }

    // Преобразуем случайные символы в их эквиваленты в хеш-таблице соответственно:
    for (int я = 0; я < длина_пароля; я ++) {
        int getCharIndex = findChar (ответ [i]);
        
        // Убедитесь, что он успешно найден:
        если (getCharIndex == FALSE){
            printf("\n[ ОШИБКА ] Символ '%c' недействителен. Повторите попытку.\n", response[i]);
            вернуть generateRandomKey (длина_пароля, максимальный_размер, использование_глобального); // Делаем это снова
        }

        // Загрузить соответствующее значение хеш-таблицы в ключ:
        если (use_global == ИСТИНА){
            loadIntoKeyForRandoKey(i, globalHashTable.hashtable[getCharIndex]);
        } еще {
            loadIntoKeyForRandoKey(i, defaultHashTable.hashtable[getCharIndex]);
        }
    }

    // Записываем случайный ключ в файл:
    createFileWithTwoDimensionalArray(globalKey.key, длина_пароля, "ключ");
}

// (Вспомогательная функция) Для загрузки в структуру EncryptedMessage:
void loadIntoEncryptedMessage (int at, char n [3], EncryptedMessage * encryptedMsg) {
    если (strlen(n) == 1){
        // Добавляем '0':
        символ tmp = n[0];
        п[0] = '0';
        п[1] = тмп;
    }

    для (целое я = 0; я < 2; я ++) {
        зашифрованноесообщение->зашифрованноесообщение[at][i] = n[i];
    }
}

/*
    Шифрует сообщение с учетом допустимой хэш-таблицы и ключа
*/
EncryptedMessage otpEncrypt(хэш-таблица символов[][3], символ * сообщение, ключ символа[MAX_SIZE][3]){
    Зашифрованное сообщение, зашифрованное сообщение;

    for (int i = 0; i < strlen(msg); i++){
        // Преобразование значения ключа в целое число:
        int convertKeyValueIntoInt = safeConvertToInt(key[i]);

        // Убедитесь, что он конвертирован правильно:
        если (convertedKeyValueIntoInt == FALSE){
            printf("[ ОШИБКА ] : Ключ поврежден в %d (значение = %s).\n", i, key[i]);
            выход(1);
        }

        // Преобразуем сообщение пользователя в его эквивалент в хеш-таблице:
        int indexOfMsgChar = findChar(msg[i]);

        // Убедитесь, что findChar() правильно нашла значение:
        если (indexOfMsgChar == FALSE){
            printf("[ ОШИБКА ] : Пароль (msg) поврежден в %d (значение = %s). Это могло произойти из-за того, что символ '%c' не разрешен.\n", i, msg, msg[i ]);
            выход(1);
        }

        char * соответствующийEncryptMsgChars = hashtable[indexOfMsgChar];

        // Преобразование соответствующих символов encryptMsg в int:
        int convertEncryptMsgCharsIntoInt = safeConvertToInt (соответствующий EncryptMsgChars);

        // Убедитесь, что он конвертирован правильно:
        если (convertedEncryptMsgCharsIntoInt == FALSE){
            printf("[ ОШИБКА ] : Хэш-таблица повреждена в %d (значение = %s).\n", indexOfMsgChar, соответствующийEncryptMsgChars);
            выход(1);
        } 

        // Делаем расчет:
        intcryptedFrag = otpeAdd(convertedEncryptMsgCharsIntoInt, convertKeyValueIntoInt);
        
        // Преобразуем его в строку:
        char зашифрованныйFragStr[3];
        sprintf(encryptedFragStr, "%d",cryptedFrag);
        
        loadIntoEncryptedMessage(i,cryptedFragStr, &encryptedMsg);
    }
    вернуть зашифрованное сообщение;
}

Мой непосредственный вопрос: если я использую предварительно закодированную хеш-таблицу (которую любой может сделать вывод), делает ли это шифрование небезопасным (даже если ключ, соответствующий значениям хеш-таблицы, полностью случайный при вводе пользователем)? Будет ли это безопасно только в том случае, если я рандомизирую номера хэш-таблицы (01-98) (и, возможно, рандомизирую validChars[])?

Мне искренне интересно, верна ли моя логика, поэтому любые комментарии, предложения или критика будут высоко оценены.

Paul Uszak avatar
флаг cn
Привет. Отличный выбор для использования одноразовых паролей. Но _"(1) Получить случайный пользовательский ввод (по крайней мере, той же длины, что и сообщение)"_ . Как? Если я кодирую сообщение размером с твит, откуда берется случайный ввод? Помните, что OTP **должен** быть сгенерирован физически с помощью механического или биологического оборудования, **не** программного обеспечения.
флаг vn
@PaulUszak Привет! В настоящее время я получаю случайный пользовательский ввод, когда пользователь спонтанно печатает случайные символы, длина которых больше или больше, чем сообщение. Это безопасно?
флаг zw
Люди бесспорно ужасны в создании случайных данных. OTP *требует, по определению* действительно случайного ввода в качестве ключей.
Рейтинг:1
флаг cn

Сразу всплывают три проблемы:

  1. Старый каштан целостности сообщения. Чистые одноразовые блокноты не имеют никаких средств аутентификации, т.е. податливый.

  2. Ключ (для сообщения) будет максимум [50,000]. Я специально говорил о сообщениях размером с твит по определенной причине.OTP изначально создавались с помощью пишущих машинок и пользовались большим успехом. Но они были маленькими, и машинисток было много. Не существует статистического теста, который мог бы опровергнуть случайность 160 символов (разумно набранных). Но есть и за 50 000. Маловероятно, что 50 000 символов, набранных наугад на клавиатуре, будут именно такими. Частотный анализ и эвристика серьезно понизят предполагаемую информационную безопасность OTP. И действительно ли пользователи будут набирать 50 000 букв? Аппаратное устройство (TRNG) необходимо для ключей такого размера.

  3. Как получатель расшифрует сообщение, если у [местоимения] нет того же ключа, который использовался для его шифрования? Так как же он туда попадет?

3½. Хэш-таблица практически не нужна. Просто используйте значения ASCII, так как безопасность OTP обеспечивается ключом. Стоит прочитать некоторые из вопросов с тегами OTP здесь.

Рейтинг:0
флаг zw

Это много кода для чего-то, что сводится к:

void otp (длина size_t, uint8_t * ключ, uint8_t * сообщение) {
    для (size_t я = 0; я < длина; я ++) {
        сообщение[i] ^= ключ[i];
    }
}

Все остальное (кроме обеспечения len(ключ) == len(сообщение) не только не нужен, но и активно отвлекает от фактической реализации одноразового блокнота. Даже фраза «сводится к» здесь, вероятно, вводит в заблуждение: это не просто упрощенная версия; кроме захвата ключа подходящей длины из действительно случайного источника, это полностью полный. Эта пропущенная часть, вероятно, немного больше, чем

char *key = malloc(длина);

утвердить (ключ! = NULL);
assert(read(fd, key, len) == len);

Я не смотрел, что делает ваша «хеш-таблица», потому что все, что она делает, совершенно ненужно, с большой вероятностью может быть источником ошибок, вызывающих катастрофическую потерю безопасности, и почти наверняка нарушает фундаментальное определение того, что представляет собой одноразовый блокнот. .

Одноразовый блокнот должен имеют действительно случайные ключи. Символы, набранные на клавиатуре, не являются случайными. Результат /dev/случайный не является действительно случайным. Получить действительно случайные биты сложно, но существуют внешние устройства, которые соберут их для вас (при условии, что вы доверяете устройству). Эти ключи должен быть такой же длины, как ваше сообщение (или, я полагаю, длиннее).

И, наконец, хотя это и не является строгой необходимостью, криптографию обычно лучше всего выполнять на биты и байты а не какое-то понятие «персонажи». Попытка сделать последнее означает создание серьезных ошибок, ведущих к катастрофическому отказу.

Mark avatar
флаг ng
Стоит отметить, что «одноразовый блокнот должен иметь действительно случайные ключи» не совсем верно — вы можете заменить случайный источник на PRG (скажем, в режиме счетчика), чтобы получить $\mathsf{CTR}\$$ шифрование. Это, конечно, не совсем безопасно, но, возможно, стоит упомянуть новичку для особенно простых реализаций шифрования.
флаг zw
В этот момент это потоковый шифр, а не одноразовый блокнот. Это не просто академическое отличие, это означает, что суть всей задачи теперь принципиально иная. Это не просто биты XOR, сложная часть — это проектирование и написание CSPRNG.
Mark avatar
флаг ng
Да, но концептуально полезно понимать поточные шифры. OTP концептуально прост, но управление ключами затруднено. CTR$ просто заменяет (жесткое) управление ключами (сложной) проблемой проектирования PRG. К счастью, хотя и то, и другое сложно, мы, кажется, действительно можем проектировать PRG, в то время как фактическое управление ключами OTP в масштабе кажется (в основном) безнадежным.
Paul Uszak avatar
флаг cn
Интересно. Почему вы считаете, что `/dev/random` не является действительно случайным? Я имею в виду тот, который блокирует (редактор) (ы). Ты про новый? Кроме того, это не так уж и сложно, так как вы можете сделать это вообще без какого-либо внешнего комплекта - джиттера процессора - `System.nanoTime()` или `haveged`, или библиотеки энтропии Arduino.
флаг zw
`/dev/random` и `/dev/urandom` [выводятся из одного и того же CSPRNG] (https://www.2uo.de/myths-about-urandom/). Даже если вы хотите возразить, что ядро ​​оценивает отношение входной энтропии к выходной энтропии как 1:1 по сравнению с первым, эта гарантия перестает действовать, как только вы отправляете ее через CSPRNG, который гарантирует только вычислительную безопасность и не имеет доказательств. совершенной безопасности. `haveged` добавляет энтропию к оценщику энтропии ядра, но это по-прежнему сталкивается с той же фундаментальной проблемой.
флаг zw
В любом случае, дело не в том, что невозможно получить действительно случайные числа. Дело в том, что OTP сам по себе принципиально тривиален, «сложная» часть — это получение чисел, которые на самом деле действительно случайны, а не «выглядят хорошо для меня» случайными. И затем, конечно, если у вас есть канал, по которому вы можете безопасно поделиться ими с третьей стороной, вы можете просто передать само сообщение по этому каналу.

Ответить или комментировать

Большинство людей не понимают, что склонность к познанию нового открывает путь к обучению и улучшает межличностные связи. В исследованиях Элисон, например, хотя люди могли точно вспомнить, сколько вопросов было задано в их разговорах, они не чувствовали интуитивно связи между вопросами и симпатиями. В четырех исследованиях, в которых участники сами участвовали в разговорах или читали стенограммы чужих разговоров, люди, как правило, не осознавали, что задаваемый вопрос повлияет — или повлиял — на уровень дружбы между собеседниками.