Буквально совсем недавно на хабре опубликована статья про то, что реализация собственной криптографии это очень плохая идея. Я подумал, что было бы интересно сделать контрольный эксперимент. У меня нет глубоких криптографических знаний, но всегда было интересно написать инструмент симметричного шифрования. В этой статье я опишу реализацию и почему я принимал те или иные решения. Очень рассчитываю на то, что в комментариях мне объяснят, что и почему я сделал неправильно.


Для реализации симметричного шифрования я решил использовать простейший способ, когда мы читаем символ из входного потока, ксорим его с символом из генератора случайных чисел и отправляем результат в выходной поток. Таким образом первоочередной задачей становится выбор генератора случайных чисел. Я решил использовать Permuted Congruential Generator. Его реализация очень проста, он производителен и качество генерации достаточно высокое.


uint32_t pcg32_random_r(uint64_t state[]) {
    uint64_t oldstate = state[0];
    // Advance internal state
    state[0] = oldstate * 6364136223846793005ULL + (state[1]|1);
    // Calculate output function (XSH RR), uses old state for max ILP
    uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
    uint32_t rot = oldstate >> 59u;
    return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
}

Для увеличения периода генерируемой последовательности я использую связку из 2х генераторов с независимыми состояниями. Вот функция, которая возвращает случайные байты:


uint8_t get_random_byte(uint64_t *state) {
    static uint32_t random_number = 0;
    uint32_t another_random_number = pcg32_random_r(state) ^ random_number;
    // if upper 4 bits of another_random_number are zero we generate new random_number
    // on average this should happen roughly every 16th call but distribution is pretty wide
    if ((another_random_number >> 28) == 0) {
        random_number = pcg32_random_r(state + 2);
    }
    return another_random_number & BYTE_MASK;
}

Можно было бы инициировать исходное состояние использую ключ шифрования, после чего согласно процедуре, описанной выше, читать входные данные, ксорить с очередным числом выданным генератором и писать в выходной поток. Но так делать не надо, потому что в этом случае при использовании одного и того же ключа мы получим одинаковые последовательности случайных байт. Как следствие если бы мы взяли 2 последовательности зашифрованные одним и тем же ключем можно было взять ксор от обеих последовательностей и получить 2 сообщения заксоренных друг на друга. Подобную последовательность уже можно относительно просто расшифровать. Чтобы этого избежать мы будем строить исходное состояние генератора из имеющихся псевдослучайных данных.


uint64_t generate_initial_state() {
    struct timeval t;
    gettimeofday(&t, NULL);
    return (uint64_t)(t.tv_usec * t.tv_sec * getpid() * getppid());
}

Структура timeval содержит 2 поля: tv_sec — число секунд с начала эпохи и tv_usec — микросекунды текущего времени. Getpid возвращает pid текущего процесса, getppid возвращает pid родительского процесса. Число секунд с начала эпохи дает нам 32 бита энтропии. Число микросекунд лежит между 0 и 1000000, что дает нам чуть меньше 20 бит энтропии. Максимальное значение pid на моей ubuntu 24.04 составляет 4194304, что дает 22 бита энтропии. Стоит отметить, что на других ОС это значение может быть ниже (32767) так что безопаснее будет считать, что pid дает нам только 15 бит энтропии. Таким образом даже при наихудшем раскладе (если максимальный pid ограничен 32767) начальное состояние содержит 82 бит энтропии, что больше, чем 64 бита, которые нам возвращает функция. Главное, чего мы хотим от начального состояния, чтобы оно было уникальным. Для того, чтобы даже в случае использования одного и того же ключа шифрования генератор случайных чисел генерировал различные последовательности. Начальное состояние будет сохранятся вместе с зашифрованными данными для инициализации генератора случайных чисел при расшифровке.


Теперь можно из начального состояния и ключа построить состояние генератора случайных чисел.


void prepare_state(uint8_t *password, uint64_t *state, uint64_t initial_state) {
    int byte_index = 0;
    int word_index = 0;
    uint64_t warm_up_count = 0;
    int i = 0;

    for (i = 0; i < PCG32_STATE_SIZE; i++) {
        state[i] = initial_state ^ (initial_state << (i + 1) | (initial_state >> (BITS_IN_BYTE * BYTES_IN_UINT64_T - (i + 1))));
    }

    for (uint8_t *p = password; *p != 0; p++) {
        word_index = i % PCG32_STATE_SIZE;
        byte_index = i / PCG32_STATE_SIZE;
        state[word_index] ^= ((uint64_t)(*p) << (BITS_IN_BYTE * byte_index));
        warm_up_count = (warm_up_count * 2) + (*p);
        i = (i + 1) % (PCG32_STATE_SIZE * BYTES_IN_UINT64_T);
    }

    for (i = 0; i < warm_up_count; i++) {
        for (int j = 0; j < PCG32_STATE_SIZE; j += 2) {
            pcg32_random_r(state + j);
        }
    }
}

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


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


int generate_head(uint64_t *state, uint8_t *buf, uint64_t initial_state) {
    int head_length = get_random_byte(state) + 1; // 1-256 random bytes at the head of the file

    for (int i = 0; i < BYTES_IN_UINT64_T; i++) {
        buf[i] = (initial_state >> ((BYTES_IN_UINT64_T - 1 - i) * BITS_IN_BYTE)) & BYTE_MASK;
    }
    // to avoid continuous data in the header we skip bytes while filling in the buffer
    for (int i = 0; i < head_length; i++) {
        for (int j = get_random_byte(state); j > 0; j--) {
            get_random_byte(state);
        }
        buf[i + BYTES_IN_UINT64_T] = get_random_byte(state);
    }
    return head_length + BYTES_IN_UINT64_T;
}

В данном случае мы подготавливаем буфер, который содержит начальное состояние, а потом от 1 до 256 случайных байт. Для того, чтобы еще сильнее усложнить жизнь дешифровщикам, байты от генератора случайных чисел мы берем не подряд, а с пропусками.


Теперь, когда у нас готов заголовок для зашифрованных данных мы можем начать шифрование.


ssize_t write_or_die(int fd, void *buf, size_t count, char *message) {
    ssize_t write_count = write(fd, buf, count);
    if (write_count == -1) {
        fprintf(stderr, "%s %s\n", message, strerror(errno));
        exit(1);
    }
    return write_count;
}

void read_xor_write(int in_file, int out_file, uint64_t *state, uint8_t *buf) {
    int bytes_read_count = 0;

    while (1) {
        bytes_read_count = read_or_die(in_file, buf, BUFSIZE, "Error: failed to read data.");
        if (bytes_read_count == 0) break;
        for (int i = 0; i < bytes_read_count; i++) {
            buf[i] ^= get_random_byte(state);
        }
        write_or_die(out_file, buf, bytes_read_count, "Error: failed to write data.");
    }
}

void encrypt(struct params_s *params) {
    uint64_t initial_state = generate_initial_state();
    uint64_t state[PCG32_STATE_SIZE];
    uint8_t buf[BUFSIZE];
    int head_length;

    prepare_state(params->password, state, initial_state);
    head_length = generate_head(state, buf, initial_state);
    write_or_die(params->out_file, buf, head_length, "Error: failed to write head data.");
    read_xor_write(params->in_file, params->out_file, state, buf);
}

При расшифровке мы сперва восстанавливаем начальное состояние.


ssize_t read_or_die(int fd, void *buf, size_t count, char *message) {
    ssize_t read_count = read(fd, buf, count);
    if (read_count == -1) {
        fprintf(stderr, "%s %s\n", message, strerror(errno));
        exit(1);
    }
    return read_count;
}

uint64_t read_initial_state(int in_file) {
    uint64_t initial_state = 0;
    uint8_t buf[BYTES_IN_UINT64_T];
    int bytes_read_count = 0;

    bytes_read_count = read_or_die(in_file, buf, BYTES_IN_UINT64_T, "Error: failed to read initial state.");
    if (bytes_read_count != BYTES_IN_UINT64_T) {
        fprintf(stderr, "Error: initial state should be %d bytes, got %d bytes instead.\n", BYTES_IN_UINT64_T, bytes_read_count);
        exit(1);
    }
    for (int i = 0; i < BYTES_IN_UINT64_T; i++) {
        initial_state = (initial_state << BITS_IN_BYTE) | buf[i];
    }
    return initial_state;
}

После чего пропускаем мусорные данные и расшифровываем сообщение.


void decrypt(struct params_s *params) {
    uint64_t initial_state = read_initial_state(params->in_file);
    uint64_t state[PCG32_STATE_SIZE];
    uint8_t buf[BUFSIZE];
    int head_length;
    int bytes_read_count = 0;

    prepare_state(params->password, state, initial_state);
    // we use same generate_head() function as in encrypt() to have same state
    head_length = generate_head(state, buf, initial_state) - BYTES_IN_UINT64_T; // - BYTES_IN_UINT64_T because of read_initial_state() above
    bytes_read_count = read_or_die(params->in_file, buf, head_length, "Error: failed to read head data.");
    if (bytes_read_count != head_length) {
        fprintf(stderr, "Error: head data should be %d bytes, got %d bytes instead.\n", head_length, bytes_read_count);
        exit(1);
    }
    read_xor_write(params->in_file, params->out_file, state, buf);
}

Полный исходный код доступен на гитхаб.


Давайте теперь обсудим в комментариях в чем я ошибся и какие дыры присутствуют в этом коде.


PS: спасибо sci_nov за дискуссию и предложение по улучшению логики. Seed для инициализации генератора случайных чисел теперь тоже зашифрован.

Комментарии (60)


  1. randomsimplenumber
    17.02.2025 12:21

    Число секунд с начала эпохи дает нам 32 бита энтропии

    Нет. Значительно меньше.


  1. Abstraction
    17.02.2025 12:21

    Known-plaintext attack: приведите сообщение (хотя бы 128 байт) и зашифрованное сообщение и посмотрим, получится ли у нас восстановить ключ.

    (Если этот режим атаки кажется неправдоподобным, приведите первые 128 байт сообщения и зашифрованное сообщение целиком, и посмотрим, получится ли восстановить вторую половину.)


    1. kt97679 Автор
      17.02.2025 12:21

      Это отличная мысль!

      $ hd /tmp/in
      00000000  4e 61 74 75 72 65 27 73  20 66 69 72 73 74 20 67  |Nature's first g|
      00000010  72 65 65 6e 20 69 73 20  67 6f 6c 64 2c 0a 48 65  |reen is gold,.He|
      00000020  72 20 68 61 72 64 65 73  74 20 68 75 65 20 74 6f  |r hardest hue to|
      00000030  20 68 6f 6c 64 2e 0a 48  65 72 20 65 61 72 6c 79  | hold..Her early|
      00000040  20 6c 65 61 66 27 73 20  61 20 66 6c 6f 77 65 72  | leaf's a flower|
      00000050  3b 0a 42 75 74 20 6f 6e  6c 79 20 73 6f 20 61 6e  |;.But only so an|
      00000060  20 68 6f 75 72 2e 0a 54  68 65 6e 20 6c 65 61 66  | hour..Then leaf|
      00000070  20 73 75 62 73 69 64 65  73 20 74 6f 20 6c 65 61  | subsides to lea|
      00000080  66 2e 0a 53 6f 20 45 64  65 6e 20 73 61 6e 6b 20  |f..So Eden sank |
      00000090  74 6f 20 67 72 69 65 66  2c 0a 53 6f 20 64 61 77  |to grief,.So daw|
      000000a0  6e 20 67 6f 65 73 20 64  6f 77 6e 20 74 6f 20 64  |n goes down to d|
      000000b0  61 79 2e 0a 4e 6f 74 68  69 6e 67 20 67 6f 6c 64  |ay..Nothing gold|
      000000c0  20 63 61 6e 20 73 74 61  79 2e 0a                 | can stay..|
      000000cb
      $ PASSWORD=... ./crypt-pcg -i /tmp/in -o /tmp/out -e && hd /tmp/out
      00000000  3b bc f8 ee e8 ea 67 e0  54 af d5 ca ef 30 2f ba  |;.....g.T....0/.|
      00000010  a0 74 1d 3e d0 c2 04 98  03 8c 87 8d 72 cf d0 95  |.t.>........r...|
      00000020  97 d5 6f a1 c9 08 1f fa  c2 c7 ed 37 e9 90 45 ab  |..o........7..E.|
      00000030  7c aa b1 2e 96 e5 f5 ad  0c be 62 8f 79 1a 7b 6a  ||.........b.y.{j|
      00000040  ee 9c 2c 16 ed 20 aa b7  d5 74 9e 0c 42 b3 e1 97  |..,.. ...t..B...|
      00000050  50 77 78 11 fc f2 94 58  2c 5f b2 e8 45 83 3a 65  |Pwx....X,_..E.:e|
      00000060  17 50 fe 75 20 23 1b 1a  d1 68 3d 6f 89 7a f8 a5  |.P.u #...h=o.z..|
      00000070  c6 90 4a 35 84 33 14 44  9a d4 21 a0 14 c8 bf 89  |..J5.3.D..!.....|
      00000080  20 50 40 5c 25 26 8e f0  c2 33 40 00 d8 63 a1 be  | P@\%&...3@..c..|
      00000090  8a ba 13 ad 82 97 0a 32  0a d1 34 36 d1 97 be 04  |.......2..46....|
      000000a0  75 e1 09 12 f6 04 0a 36  8e 55 0c 0f 91 d5 65 f5  |u......6.U....e.|
      000000b0  5f 42 c6 f4 05 b4 ea 9e  7b f6 28 3d aa 20 5f c5  |_B......{.(=. _.|
      000000c0  7b e5 d7 ba 66 42 13 17  79 f4 fa 5f 70 ce 15 60  |{...fB..y.._p..`|
      000000d0  0f 37 6d bf b4 94 5e 39  29 7b 59 5d 6f 80 57 56  |.7m...^9){Y]o.WV|
      000000e0  3e 39 02 85 0b c9 d1 d6  0a 6b ce c3 6d 84 2d a5  |>9.......k..m.-.|
      000000f0  25 da 2b 72 ec 68 cf cd  d8 a7 5d 89 36 b0 8a a1  |%.+r.h....].6...|
      00000100  36 b7 16 d5 27 75 f7                              |6...'u.|
      00000107
      $ PASSWORD=... ./crypt-pcg -i /tmp/out -o /tmp/dec -d && hd /tmp/dec
      00000000  4e 61 74 75 72 65 27 73  20 66 69 72 73 74 20 67  |Nature's first g|
      00000010  72 65 65 6e 20 69 73 20  67 6f 6c 64 2c 0a 48 65  |reen is gold,.He|
      00000020  72 20 68 61 72 64 65 73  74 20 68 75 65 20 74 6f  |r hardest hue to|
      00000030  20 68 6f 6c 64 2e 0a 48  65 72 20 65 61 72 6c 79  | hold..Her early|
      00000040  20 6c 65 61 66 27 73 20  61 20 66 6c 6f 77 65 72  | leaf's a flower|
      00000050  3b 0a 42 75 74 20 6f 6e  6c 79 20 73 6f 20 61 6e  |;.But only so an|
      00000060  20 68 6f 75 72 2e 0a 54  68 65 6e 20 6c 65 61 66  | hour..Then leaf|
      00000070  20 73 75 62 73 69 64 65  73 20 74 6f 20 6c 65 61  | subsides to lea|
      00000080  66 2e 0a 53 6f 20 45 64  65 6e 20 73 61 6e 6b 20  |f..So Eden sank |
      00000090  74 6f 20 67 72 69 65 66  2c 0a 53 6f 20 64 61 77  |to grief,.So daw|
      000000a0  6e 20 67 6f 65 73 20 64  6f 77 6e 20 74 6f 20 64  |n goes down to d|
      000000b0  61 79 2e 0a 4e 6f 74 68  69 6e 67 20 67 6f 6c 64  |ay..Nothing gold|
      000000c0  20 63 61 6e 20 73 74 61  79 2e 0a                 | can stay..|
      000000cb
      $
      


  1. Vasko67
    17.02.2025 12:21

    И номер процесса дает реально всего несколько бит.

    И микросекунды дают не более единиц бит, потому что генератор шифрует данные, получая последовательные метки времени.


    1. kt97679 Автор
      17.02.2025 12:21

      В данном случае это не важно. Я отметил в статье, что от исходного состояния нам нужна уникальность, а не случайность.


  1. sci_nov
    17.02.2025 12:21

    Было бы неплохо нарисовать функциональную схему шифратора.

    Вы начальное состояние передаёте открыто. Для преодоления этого недостатка можно использовать двойное шифрование. В этом случае внутреннее шифрование - то, что Вы описали, а внешнее - надо разрабатывать. Внешнее шифрование закроет параметры внутреннего и, заодно, шифр-текст.

    В библиотеке std:: есть способ получить энтропию, который комбинирует возможности платформы (не только системное время): std::random_device{}.


    1. kt97679 Автор
      17.02.2025 12:21

      То, что я передаю открыто, не состояние генератора, а затравка, из которой при помощи ключа формируется состояние генератора.


      1. sci_nov
        17.02.2025 12:21

        Это начальное состояние, из которого формируется начальное состояние генератора. Фактически это некий seed в дополнение к ключу. Раскрывать seed тоже не очень хорошо. Зависит от размеров ключа и сида.


        1. kt97679 Автор
          17.02.2025 12:21

          seed в данном случае 64 бита. Ключ - зависит только от вас. Можно попросить вас раскрыть мысль какие риски несет открытый seed?


          1. sci_nov
            17.02.2025 12:21

            Смысл сида - увеличить неопределенность (энтропию), то есть усложнить жизнь атакующему. В идеале итоговая энтропия шифра равна сумме энтропий ключа и сида. Если сид открыт, то это равнозначно обнулению его энтропии и уменьшению итоговой энтропии.

            При неизвестном сиде атакующий должен его перебирать (помимо перебора ключа), а при известном - нет. Всё довольно таки тривиально.


            1. kt97679 Автор
              17.02.2025 12:21

              Мне кажется вы не совсем правы. Если вы посмотрите на мехнизм парольной авторизации в юниксе вы увидите, что в /etc/shadow хранится хеш пароля и соль, котоая была использована для генерации этого хеша. Я всего лишь воспользовался тем же подходом.


              1. sci_nov
                17.02.2025 12:21

                У Вас не механизм парольной авторизации, в том-то и дело. Для хеширования паролей это нормально, потому что идеальная хэш-функция необратима. В вашем случае это шифрование, которое естественно обратимо.


                1. kt97679 Автор
                  17.02.2025 12:21

                  В моем коде seed преобразуется при помощи ключа шифрования в состояние генератора случайных чисел. Так что знание seed при разумной длине ключа не помогает взлому. Особенно с учтом того, что добавление одного символа к ключу шифрования удваивает время инициализации генератора.


                  1. sci_nov
                    17.02.2025 12:21

                    Я говорил, естественно, об идеальном преобразовании ключа и сида в начальное состояние ГСЧ. С Вашим надо ещё разбираться. А так да, при идеальном преобразовании и при размере ключа много большем размера сида, открытостью сида можно пренебречь. Но тогда зачем он вообще нужен?.. Можно просто инициализировать генератор ключом, прогреть его и этого достаточно. Правда ещё зависит от качества генератора... В общем, это комплексное дело писать свою криптографию...


                    1. kt97679 Автор
                      17.02.2025 12:21

                      Если не будет сида, то как обеспечить разные случайные последовательности при использовании одного и того же ключа?


                      1. sci_nov
                        17.02.2025 12:21

                        Да, нужен сид, но его надо прикрывать внешним шифрованием, которое должно отличаться от внутреннего по своей природе.


                      1. kt97679 Автор
                        17.02.2025 12:21

                        как бы вы предложили поменять код?


                      1. sci_nov
                        17.02.2025 12:21

                        Дело ваше, тем более код. Ладно бы на блок схеме чего поменять, а тут код... Нет уж)

                        У меня есть проект парольного менеджера (смотри в профиле гитхаб проектAllPass), там есть блок схема шифратора. Но там я заморочился по полной и с нуля, решив выжать из регистров сдвига LFSR по максимуму тем самым реабилитируя их). По отдельности эти регистры слабы, поэтому делал разные навороты.


                      1. kt97679 Автор
                        17.02.2025 12:21

                        Если вывести за скобки открытй сид есть ли у вас замечания по остальной логике?


                      1. sci_nov
                        17.02.2025 12:21

                        В детали я не залезал, поэтому замечаний нет)


                      1. kt97679 Автор
                        17.02.2025 12:21

                        Если у вас будет время был бы вам очень благодарен если бы вы оценили детали :).


                      1. sci_nov
                        17.02.2025 12:21

                        За криптоанализ платят деньги, и это коллективная работа. Я не криптоаналитик, просто немного разбираюсь в криптографии.

                        А вам вообще зачем это? Какой то реальный проект? Если нет, то никто и не возьмётся за детальный анализ...


                      1. sci_nov
                        17.02.2025 12:21

                        Может конечно будет время, но не гарантирую. Я там особенно нового и оригинального ничего не вижу


                      1. kt97679 Автор
                        17.02.2025 12:21

                        Это в чистом виде удовлетворение моего любопытства.


                      1. sci_nov
                        17.02.2025 12:21

                        Эх, я бы ответил, но воздержусь)


                      1. kt97679 Автор
                        17.02.2025 12:21

                        На всякий случай раскрою мысль. Мне действительно очень интересно, что именно я упустил продумывая логику. И как можно взломать зашифрованное этим кодом. Без брутфорса.


                      1. quazycrazy
                        17.02.2025 12:21

                        По странной причине поиск на гитхабе не выдал мне этот проект, пришлось посмотреть ссылки из других статей профиля. Оставлю тут ссылку чтобы, возможно кому-то сэкономить время https://github.com/nawww83/AllPass


                      1. sci_nov
                        17.02.2025 12:21

                        Да, там есть коллизия, я это узнал уже постфактум


  1. NightShad0w
    17.02.2025 12:21

    Не сказывается ли на качестве шифрования 64 битное переполнение в state[0] = oldstate * 6364136223846793005ULL + (state[1]|1); ?

    Насколько безопасен унарный минус для беззнакового целого ((-rot)) без указания целевой платформы, используемого компилятора, и версии стандарта?


    1. kt97679 Автор
      17.02.2025 12:21

      В данном случае я доверяю автору алгоритма. Вот статья, описывающая детали реализации: https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf Вот кртикка алгоритма от автора главного кокурента: https://pcg.di.unimi.it/pcg.php А вот обсуждение на реддите: https://www.reddit.com/r/programming/comments/8jbkgy/the_wrapup_on_pcg_generators/


    1. sci_nov
      17.02.2025 12:21

      Для беззнакового фиксированной ширины безопасно.


  1. kipar
    17.02.2025 12:21

    если начальное состояние 64 бита, то за счет атаки дней рождения через сотню миллионов зашифрованных одним ключом сообщений мы с шансом 1% найдем два с одинаковым состоянием и поксорим. А через пять миллиардов - найдем с вероятностью 50%.
    И это считая что там честные 64 бита - я правильно понял что оно генерируется умножением случайных чисел, т.е. младшие биты будут с большей вероятностью нулевыми? Если один из сомножителей четный то и произведение будет четным, ну и так далее.


    1. kt97679 Автор
      17.02.2025 12:21

      Прошу прощения за путаницу в названиях, надо переименовать. То, что в тексте называется "начальное состояние" это seed, из которого при помощи ключа шифрования строится начальное состояние генератора случайных чисел. Состояние генератора 256 бит, так что атака дней рождений скорее всего не будет эффективной.


      1. kipar
        17.02.2025 12:21

        если ключ одинаковый и seed (начальное состояние) одинаковое, то и 256-битное состояние гсч будет одинаковым, разве нет?


        1. kt97679 Автор
          17.02.2025 12:21

          Вы правы, состояние будет одинаковым. Понять, что seed одинаков в исходной реализации легко. Но я добавил шифрование сида по совету sci_nov. Т.е. если будет 2 сообщения, с одинаковым зашифрованным seed, это не является гарантией, что seed и ключ у обоих сообщений одинаков. Правильно ли я вас понимаю, что вы предлагаете увеличить размер сида?


          1. kipar
            17.02.2025 12:21

            да, 64 бита для nonce считается мало если он случайный а не последовательно увеличивается. И конечно он должен получаться из нормального источника - что-то типа

            On BSD-based systems and macOS/Darwin, it uses arc4random, on Linux getrandom, on Windows RtlGenRandom, and falls back to reading from /dev/urandom on UNIX systems.

            потому что брать не очень случайные числа да еще и перемножать - решение мягко говоря так себе.

            А так - останется еще заменить pcg32 на ChaCha20 (пгсч с исследованной криптостойкостью), добавить HMAC чтоб отличать измененные файлы (если злоумышленник догадывается о содержимом файла то сейчас он может даже не зная ключа изменить в нем байты так чтобы получить в нем требуемое содержимое) и получится почти что monocypher.


            1. kt97679 Автор
              17.02.2025 12:21

              Понял, спасибо.

              Детали работы pcg32 раскрыты вот в этой статье: https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf Можете ли вы выделить явные недостатки pcg32 по сравнению с ChaCha20?


              1. kipar
                17.02.2025 12:21

                Про PCG32 я знаю. Да что там, я сам когда-то сделал PR и теперь это дефолтный рандом в языке Crystal. Недостаток только в том что автор не позиционирует его как криптостойкий, поэтому по-серьезному его не исследовали. Несколько лет назад была опубликована атака на него (https://hal.science/hal-02700791), после чего насколько я помню автор опубликовал PCG с другой хеш-функцией.


                1. kt97679 Автор
                  17.02.2025 12:21

                  Подумав над вашими комментариями я переключился на функцию clock_gettime и теперь с ее помощью генерирую 256 бит состояния: https://github.com/kt97679/misc/commit/4f1157c15cd0d5df9bb8163e7c8ea1c1d80e7836


  1. VoodooCat
    17.02.2025 12:21

    Мне кажется использование getppid излишне: даёт немного, зато результат может удивить, если процесс осиротеет, и с кросс-платформенностью прям плохо (на Windows его получить можно, но только через... затратную операцию).

    getpid - так же плох, но уже, в случае с контейнерами можно не увидеть ожидаемой рандомизации совсем.

    Имхо, лучше pid+ppid заменить любым рандомом от ОС.


    1. kt97679 Автор
      17.02.2025 12:21

      Вы совершенно правы, и я долго думал, что еще можно использовать в качестве источника случайных данных для seed, но так ничего и не придумал. Какой рандом от ОС вы предлагаете использовать? На юникс подобных ОС можно прочитать /dev/urandom, но как тогда быть на Windows?


      1. VoodooCat
        17.02.2025 12:21

        Я не уверен, что стоит лезть в системные API, можно ограничиться rand_s и/или какими-то другими аналогами стандартных библиотек (я уже забыл C++ там вверху или C). Так то и время там берете, которое с секундной точностью, но если идти по хардкору то на всех ОС есть бОльшая точность, главное не переборщить с ней. А так RtlGenRandom и любое другое API которое найдете на MSDN на любой вкус избегая или не избегая депрекейтов. Тут я в целом в этом вопросе не специалист (про ГСЧ и криптографию). :)

        Про PID-ы я возбудился просто потому что работая преимущество с шарпом, для всех ОС надо писать свои запускаторы процессов с блэк-джеком и радостями, которые на мой взгляд обязаны быть везде но редко где реализованы (впрочем и понятно -тОС настолько все разные что смысла обобщать нет). :)

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

        PS: Раньше занимался банкингом (лет 20 назад) и кастомные системы аутентификации были частью работы - их там сам бог послал, прежде всего что удовлетворить гос требования. Сейчас вот "все" упоролись в JWT, вместо непрозрачного токена и молятся на него. А зачем сами не знают, только криптографию подавай ради криптографии. :) {не холивар - умные люди знают зачем}. Но спустя годы - вижу что принципиально ничего не поменялось вообще ничего, поменялось лишь огульное следование моде и трендам. Безопасность должна быть безопасная. А от некоторых CVE или заявлениям "безопасников" о возможной угрозе если два раза выполнить chroot - волосы дыбом встают. Ладно, что-то меня понесло не туда. )


        1. kt97679 Автор
          17.02.2025 12:21

          Спасибо за добрые слова :)!

          У меня основная ос - линукс. Windows вообще в доступе нет. Так что увы MSDN мне особо не поможет. Но буду думать дальше, возможно на базе позикса можно что-то будет придумать.

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


          1. VoodooCat
            17.02.2025 12:21

            Оффтоп: у меня хост на линуксе. В винде обычно на виртуалке где все и происходит. Есть и бут в винду на голое железо - для специфических рабочих нужд. С линуксом я знаком давно и читал красную книгу Unix (впрочем наглухо все забыл). Просто по роду деятельности в винде закопан глубже. Я относительно недавно сменил комп который спокойно позволяет делать все и даже больше и не ограничивает железом, так то я этого хотел долгое время, но не было возможности/времени/жизнь. Поэтому я хоть и работаю с ArchLinux каждый день, но все таки считаю себя "виндузятником". Разобрать крэш-дамп ядра с винды я как-то могу, а с линуксовыми не приходилось иметь дело. В общем, всё относительно. Ну а в программировании - и вообще по сути не важно, оно опять же везде все до боли или одинаковое или одинаково неуклюжее.

            Хотя нет. На виндовс родной механизм заданий (Jobs) и кирпичик для контейнеров (не хуйдокеров) - сильно лучше линуксовых cgroups2 которые более могучи но нужен рут. А у процессов более чем stdio, а еще и дальше выводы и IP вообще не интересен. Дайте блин нормальную возможность, вот как в винде или как в фуксии. И таких классностей в ядре винды - очень много. Но...

            Стараюсь в каждой системе рассматривать возможности, но к сожалению в основном абсолютно везде вижу лишь недостатки. Я знаю - это психологическое.


            1. kt97679 Автор
              17.02.2025 12:21

              Идеальных технологий, увы, не существует. Есть только те, которые все ругают, и те, которыми никто не пользуется. Если посмотреть на историю компьютерной индустрии, то видно, что выигрывают далеко не самые технически совершенные решения, а зачастую наоборот. И это, безусловно, печально.


          1. VoodooCat
            17.02.2025 12:21

            Про безопасников я не имел ввиду отдел ИБ или еще что. Я не работал в банке, я делал софт для них. )) Я больше про страшилки от секьюритилабс и им подобные которые рассказывают порою настолько невероятные вещи и выдают на серьезных щщах за чистую монету, что диву даешься. При чем в конце ж надо всего-то выполнить какие-то команды локально. :) И я тут к инфор ресурсу вопросов не имею, кроме того что статьи поганые гонят. А скорее в общем, на хабре тоже встречаются статьи что вот если злоумышленник выполнит chroot (c).

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

            При этом полностью игнорируется здравый смысл. Вот от этого подгорает.

            PS: Настроение сегодня шаловливое, простите уж за некоторые вольности.


            1. kt97679 Автор
              17.02.2025 12:21

              Когда я сталкивался с вопросами безопасности, то как правило это сводилось к заполнению опросников делаем ли мы 1, 2 , 3... Если крыжики стояли, то что там было на самом деле мало кого волновало. Наверное до момента, когда случался инцидент, но на моей практике такого не было.


              1. VoodooCat
                17.02.2025 12:21

                Я сталкивался больше с административной волокитой, но когда ответственный на той стороне топает ножками и говорит что условно хочу фикс сегодня и все неправильно, но без письма я его не приму... На самом деле мы довольно быстро адаптировали человека к реалиям: пишите оф письмо, в стандартный срок ответим. Учитывая что это карточная/платежная система и если срочные фиксы нужны то вчера - то здравый смысл побеждал. При чем я лично нашел пару багов в спецификации платежной системы/терминального обмена и на эти граничные случаи наступали. Впрочем и наш софт на них наступал по нашей вине.

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

                Ну и ничего, справлялись, в принципе были требования - получите.

                Позже я начал работать на вражеском рынке, и я просто таки охренел как там вообще похеру было на это вот все к чему я привык, а про безопасность вообще такого слова не знали. В общем, считаю, что потратил годы не зря, в первом совершенстве овладел MSSQL (т.е. очень не полностью), но достаточно для отладки методом "аналитического анализа" (а иначе в платежной системе нельзя - вопросы постфактум). Вот и... потом занялся тем, к чему стремился. А душа стремилась к C++ и браузерам. Хых.


      1. sci_nov
        17.02.2025 12:21

        А чем std random_device не устраивает?


        1. kt97679 Автор
          17.02.2025 12:21

          Мой код на си, там я могу использовать long random(void) из stdlib. Но для инициализации генератора надо использовать void srandom(unsigned int seed) при том, что как минимум на моей системе sizeof(unsigned int) это 4. Таким образом сид оказывается 32 бита, что на мой взгляд мало.


          1. sci_nov
            17.02.2025 12:21

            А... так если ваш основной генератор ПСЧ - внешний (сторонний), то для сида можно использовать встроенный генератор и набирать хоть сколько байт в сид. А для инициализации встроенного генератора использовать что-нибудь аппаратное (пусть даже и 32-бита).


            1. kt97679 Автор
              17.02.2025 12:21

              Но разве тогда атака дней рождений не станет много более реальной?


              1. sci_nov
                17.02.2025 12:21

                Про это не могу сказать, потому что не очень понимаю эту атаку.


              1. sci_nov
                17.02.2025 12:21

                ... Видимо, раз у вас выход формируется путем xor гаммы и входных данных. Значит что бы Вы ни делали, система ограничена этим скором. По сути, это просто гаммирование)


      1. sci_nov
        17.02.2025 12:21

        На Windows использовать WSL :)


  1. sci_nov
    17.02.2025 12:21

    Посмотрел репозиторий. Нарисовать диаграмму шифрования очень стоит. Будет легче анализировать уязвимости стороннему человеку, да и Вам тоже


    1. kt97679 Автор
      17.02.2025 12:21

      Добавил: https://github.com/kt97679/misc/blob/master/crypt/diagram.txt Пожалуйста дайте знать если что-то надо уточнить.


      1. sci_nov
        17.02.2025 12:21

        У Вас система ограничена тем, что фактически это просто гаммирование. Можно хотя бы добавить нелинейность: делать циклический сдвиг входного байта перед его ксором. При этом величина сдвига будет зависеть от байта гаммы (по модулю 8).

        Но лучше всё-таки двойное гаммирование: первое обычное, второе - с циклическим сдвигом, потому что при сообщении из нулей на выходе будет голая гамма. Первое гаммирование при этом фактически будет неким скремблером, защищающим от детерминированных данных (известных паттернов).


        1. kt97679 Автор
          17.02.2025 12:21

          Это отличный совет, спасибо! Добавлю в ближайшее время.


        1. kt97679 Автор
          17.02.2025 12:21

          Добавил: https://github.com/kt97679/misc/commit/03df053af10e615c606a360b0bef03abb02d77f6 Еще раз огромное спасибо!