Java / Erlang: обмен ключами Диффи Хеллмана не работает

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

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

Вот что я пробовал:

  • Использовались встроенные средства, предоставляемые обоими языками. Не мог понять, как преобразовать необработанный открытый ключ в форму, которую могла бы использовать Java.
  • Поцарапал это и пошел с простыми формулами для расчета открытых и закрытых ключей для каждой стороны. (Статистически это могло сработать примерно в 25% случаев ... к счастью для меня, нет.)
  • Погрузился в документацию по ASN.1 от ITU и отправил открытый ключ Erlang, закодированный аналогично ключам Java. Определял это сохранением ключа Java в файл и использованием шестнадцатеричного редактора. Я не стал возвращаться к подробному тестированию. Он избавился от java.security.spec.InvalidKeySpecException: Inappropriate key specification. Думаю, здесь статистика тоже не в мою пользу. Секреты все еще не совпадали.
  • Отправил все числа из Java на сторону Erlang для вычисления ключей, общий секрет с использованием чисел Java ... Те же числа. Есть надежда!!!
  • Начали внимательно изучать передаваемые данные. Это заняло немного времени, поскольку в Erlang данные организованы в байты без знака. Eclipse IDE (возможно, есть параметр, который нужно изменить) использует байты со знаком в байтовых массивах и массив целых чисел со знаком в BigInteger.

Здесь я начал видеть вещи. Все это вводилось вручную в течение многих итераций, чтобы убедиться, что я нашел правильный шаблон событий. В Erlang я вижу свой открытый ключ, начинающийся с <<215, 101, 208, 153,. Первый элемент BigInteger на стороне Java - это 681193318. Буфер, в который были прочитаны байтовые данные, читает: [-41, 101, -48, -103. (То же, что и у Эрланга). Однако, потратив время, преобразуйте первые четыре элемента двоичной строки в целое число ...

<<I:32/signed-integer>> = <<215,101,208,153>>.

Это дает -681193319 по сравнению с большим целым числом 681193318

Код, который я использовал, был довольно простым:

Erlang "Сервер":

-module(echo).
-export([start/0]).

start() ->
    crypto:start(),
    spawn(fun () -> {ok, Sock} = gen_tcp:listen(12321, [binary, {packet, raw}]),
    echo_loop(Sock)
    end).

echo_loop(Sock) ->
    {ok, Conn} = gen_tcp:accept(Sock),
    io:format("Got connection: ~p~n", [Conn]),
    Handler = spawn(fun () -> handle(Conn) end),
    gen_tcp:controlling_process(Conn, Handler),
    echo_loop(Sock).

p() ->
    16#ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff.

g() ->
    2.

handle(Conn) ->
    receive
        {tcp, Conn, Yc} ->
            Xs = crypto:strong_rand_bytes(64),
            Ys = crypto:mod_pow(g(),Xs,p()),
            S = crypto:mod_pow(Yc, Xs, p()),

            AESKey = crypto:hash(sha256, S),

            gen_tcp:send(Conn, Ys),%KeyCert),
            handle(Conn);
        {tcp_closed, Conn} ->
            io:format("Connection closed: ~p~n", [Conn])
    end.

Java "Клиент":

public class MyProgram {
    private static Socket s;
    private static OutputStream out;
    private static InputStream in;
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MessageDigest hash;
        byte buffer[] = new byte[1024];
        byte buf2[];
        int len = 0;
        byte[] aeskey;

        try {
            hash = MessageDigest.getInstance("SHA-256");
            byte    keybuffer[] = new byte[64];
            SecureRandom srnd = SecureRandom.getInstance("SHA1PRNG");
            BigInteger Xc, Yc, Sc, Ys;

            srnd.nextBytes(keybuffer);
            Xc = new BigInteger(keybuffer);
            Yc = new BigInteger("2").modPow(Xc, DiffieHellman.Group2.P);

            s = new Socket("localhost",12321);
            out = s.getOutputStream();
            in = s.getInputStream();

            out.write(Yc.toByteArray());
            out.flush();

            len = in.read(buffer);
            buf2 = new byte[len];
            System.arraycopy(buffer, 0, buf2, 0, len);

            Ys = new BigInteger(buf2);          
            Sc = Ys.modPow(Xc, DiffieHellman.Group2.P);
            aeskey = hash.digest(Sc.toByteArray());

            out.close();
            in.close();
            s.close();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }       
    }
}

Что случилось?


person Nolan Robidoux    schedule 22.07.2018    source источник
comment
Когда вы отправляете данные через сокет, у вас нет возможности узнать, на сколько кусков они будут разделены. Когда вы указываете {packet, raw} (или эквивалентно {packet, 0}), вы сообщаете erlang, что вы позаботитесь о сборке неопределенного количества фрагментов в полные данные, поэтому erlang просто помещает каждый фрагмент в отдельное сообщение. См. Здесь: stackoverflow.com/questions/43957164 /. Я не знаю, вызывает ли вышеперечисленное какие-либо из ваших проблем, но это то, что вам нужно решить.   -  person 7stud    schedule 22.07.2018
comment
@ 7stud Спасибо. Достаточно легко изменить. Я думал, что TCP справится с этим. Я считаю, что TCP должен быть реализован в максимальной степени в ОС. Однако я заметил две вещи. 1) Ключ передается правильно (всего 128 байт) 2) Когда я использую {packet, 2}, я думаю, что erlang, возможно, ожидает чего-то, чего я не предоставил в своем приложении Java, и зависает бесконечно. Я использовал 1, и это привело к успешной передаче, но ключи были на 1 байт длиннее на стороне Java. Больше вещей, которыми нужно управлять.   -  person Nolan Robidoux    schedule 23.07.2018
comment
Я думал, что TCP справится с этим. Я думаю, что TCP обрабатывает разделение фрагмента на пакеты для отправки, а затем повторную сборку пакетов в фрагмент на принимающей стороне, но количество фрагментов, на которые разделяются ваши данные, определяется состоянием сетевых буферов . Прочтите это: docs.python.org/3/howto/sockets.html и это: stackoverflow.com/ questions / 17667903 /   -  person 7stud    schedule 23.07.2018


Ответы (1)


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

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

В моем исходном коде есть два места, где это представляет проблему:

       Ys = new BigInteger(buf2);          
       Sc = Ys.modPow(Xc, DiffieHellman.Group2.P);

Проблема с первой строкой заключается в том, что если бит 8 установлен в первом байте, весь массив buf2 должен быть добавлен к байту 0x00. Есть проблема и со второй строкой ... она не станет очевидной, пока не будет выполнена следующая строка: aeskey = hash.digest(Sc.toByteArray());

Проблема здесь в том, что если бит 8 установлен в первом байте результата ... к нему добавляется 0x00. Он передается в функцию digest(), но его нужно опустить.

Мой код изменился на то, что показано ниже, и работает: :)

    len = in.read(buffer);
    buf2 = new byte[len+1];
    System.arraycopy(buffer, 0, buf2, 1, len);
    buf2[0] = 0;

    if(buf2[1] < 0)
        Ys = new BigInteger(buf2);
    else
        Ys = new BigInteger(Arrays.copyOfRange(buf2, 1, buf2.length));

    Sc = Ys.modPow(Xc, DiffieHellman.Group2.P);
    buffer = Sc.toByteArray();
    if(buffer[0] == 0)
        aeskey = hash.digest(Arrays.copyOfRange(buffer, 1, buffer.length));
    else
        aeskey = hash.digest(buffer);

Эти две строки остались как есть:

    Xc = new BigInteger(keybuffer);
    Yc = new BigInteger("2").modPow(Xc, DiffieHellman.Group2.P);

Это потому, что закрытым ключом может быть «любое случайное число». При необходимости к открытому ключу клиента во второй строке добавляется байт 0x00. Однако Erlang интерпретирует целые числа как обратный порядок байтов, и любые начальные 0x00 байты оказываются неактуальными, поскольку они не влияют на числовое значение и, следовательно, на результат при выполнении crypto:mod_pow().

Комментарии о том, как улучшить код, очень приветствуются.

person Nolan Robidoux    schedule 15.08.2018