Criptografia de ponta a ponta no golfe


16

Esse desafio carrega uma recompensa de 200 pontos para o primeiro responder e permanece invicto por pelo menos 3 dias. Reivindicada pelo usuário3080953 .

Ultimamente, há muita conversa sobre criptografia de ponta a ponta e pressão sobre as empresas para removê-la de seus produtos. Não estou interessado nos erros e desvantagens disso, mas me perguntei: quão curto pode ser o código que levaria uma empresa a ser pressionada a não usá-lo?

O desafio aqui é implementar uma troca de chaves Diffie Hellman entre dois sistemas em rede e permitir que os usuários se comuniquem alternadamente usando a chave simétrica gerada. Para o objetivo desta tarefa, nenhuma outra proteção é necessária (por exemplo, não é necessário alternar a chave, verificar identidades, proteger contra DoS etc.) e você pode assumir uma Internet aberta (todas as portas que você escuta estão disponíveis para todos). O uso de componentes internos é permitido e incentivado!

Você pode escolher um dos dois modelos:

  • Um servidor e um cliente: o cliente se conecta ao servidor, então o servidor ou o cliente pode enviar mensagens para o outro. Terceiros entre os dois devem ser incapazes de ler as mensagens. Um exemplo de fluxo pode ser:
    1. O usuário A inicia o servidor
    2. O usuário B inicia o cliente e o direciona para o servidor do usuário A (por exemplo, via IP / porta), o programa abre uma conexão
    3. O programa do usuário A reconhece a conexão (opcionalmente, solicitando o consentimento do usuário primeiro)
    4. O programa do usuário B inicia a geração de um segredo DH e envia os dados necessários (chave pública, prime, gerador, qualquer outra coisa que sua implementação precise) ao usuário A
    5. O programa do Usuário A usa os dados enviados para concluir a geração do segredo compartilhado e envia de volta os dados necessários (chave pública) ao Usuário B. A partir deste ponto, o Usuário A pode inserir mensagens (por exemplo, via stdin) que serão criptografadas e enviadas ao Usuário. B (por exemplo, stdout).
    6. O programa do usuário B completa a geração do segredo compartilhado. A partir deste ponto, o usuário B pode enviar mensagens para o usuário A.
  • Ou: um servidor com dois clientes conectados a ele: cada cliente conversa com o servidor, que encaminha sua mensagem para o outro cliente. O próprio servidor (e quaisquer terceiros intermediários) deve ser incapaz de ler as mensagens. Além da conexão inicial, o processo é o mesmo descrito na primeira opção.

Regras detalhadas:

  • Você pode fornecer um único programa ou vários programas (por exemplo, servidor e cliente). Sua pontuação é o tamanho total do código em todos os programas.
  • Seu programa deve ser teoricamente capaz de se comunicar através de uma rede (mas, para testar, o host local é bom). Se o seu idioma de escolha não suportar redes, você pode combiná-lo com algo que o faça (por exemplo, um shell script); nesse caso, sua pontuação é o tamanho total do código em todos os idiomas usados.
  • A geração de chaves Diffie Hellman pode usar valores "p" e "g" codificados.
  • A chave compartilhada gerada deve ter pelo menos 1024 bits.
  • Depois que a chave é compartilhada, a escolha da criptografia de chave simétrica depende de você, mas você não deve escolher um método que atualmente é conhecido por ter um ataque prático contra ela (por exemplo, um deslocamento de césar é trivial para reverter sem o conhecimento da chave ) Exemplo de algoritmos permitidos:
    • AES (qualquer tamanho de chave)
    • RC4 (teoricamente quebrado, mas nenhum ataque prático que eu possa encontrar menção, por isso é permitido aqui)
  • Os usuários A e B devem poder enviar mensagens um para o outro (comunicação bidirecional) interativamente (por exemplo, lendo linhas de stdin, solicitações contínuas ou eventos como pressionar um botão). Se facilitar, você pode assumir uma conversa alternada (ou seja, depois que um usuário envia uma mensagem, ele deve aguardar uma resposta antes de enviar a próxima mensagem)
  • Os idiomas embutidos são permitidos (não é necessário escrever seus próprios métodos criptográficos ou de rede, se já tiverem suporte).
  • O formato de comunicação subjacente é com você.
  • As etapas de comunicação fornecidas acima são um exemplo, mas você não precisa segui-las (desde que as informações necessárias sejam compartilhadas e nenhum intermediário seja capaz de calcular a chave ou as mensagens compartilhadas)
  • Se os detalhes necessários para conectar-se ao seu servidor não forem conhecidos antecipadamente (por exemplo, se ele escutar em uma porta aleatória), esses detalhes deverão ser impressos. Você pode assumir que o endereço IP da máquina é conhecido.
  • O tratamento de erros (por exemplo, endereços inválidos, conexões perdidas etc.) não é necessário.
  • O desafio é o código de golfe, portanto o código mais curto em bytes vence.

É codificado pe gpermitido?
somente ASCII

@ Somente ASCII, pelo que sei, codificar valores de p & g de boa qualidade é considerado bom (a menos que o desenvolvedor use maliciosamente valores conhecidos por serem vulneráveis ​​a ataques específicos). Portanto, para este desafio que é OK (contanto que o segredo resultante é pelo menos 1024 bits)
Dave

Respostas:


3

Node.js ( 372 423 + 94 = 517 513 bytes)

Golfe

Quebras de linha adicionadas para "legibilidade".

chat.js ( 423 419 bytes)

Sem quebras de linha

[n,c,p]=["net","crypto","process"].map(require);r="rc4",a="create",h="DiffieHellman",z="pipe",w="write",o=128,g=p.argv;s=e=d=0,y=c[a+h](8*o),k=y.generateKeys();v=n.connect(9,g[2],_=>{g[3]&&(v[w](y.getPrime()),v[w](k));v.on("data",b=>{s||(g[3]||(y=c[a+h](b.slice(0,o)),k=y.generateKeys(),v[w](k),b=b.slice(o)),s=y.computeSecret(b),e=c[a+"Cipher"](r,s),p.stdin[z](e)[z](v),d=c[a+"Decipher"](r,s),v[z](d)[z](p.stdout))})})

Quebras de linha

[n,c,p]=["net","crypto","process"].map(require);
r="rc4",a="create",h="DiffieHellman",z="pipe",w="write",o=128,g=p.argv;
s=e=d=0,y=c[a+h](8*o),k=y.generateKeys();
v=n.connect(9,g[2],_=>{g[3]&&(v[w](y.getPrime()),v[w](k));
v.on("data",b=>{s||(g[3]||(y=c[a+h](b.slice(0,o)),k=y.generateKeys(),
v[w](k),b=b.slice(o)),s=y.computeSecret(b),e=c[a+"Cipher"](r,s),p.stdin[z](e)[z](v)
,d=c[a+"Decipher"](r,s),v[z](d)[z](p.stdout))})})

echo_server.js (94 bytes)

c=[],require("net").createServer(a=>{c.forEach(b=>{a.pipe(b),b.pipe(a)});c.push(a)}).listen(9);

Ungolfed

O nó possui recursos integrados de rede e criptografia. Isso usa o TCP para redes (porque é mais simples que a interface do Node para HTTP e funciona muito bem com fluxos).

Eu uso uma cifra de fluxo (RC4) em vez do AES para evitar ter que lidar com tamanhos de bloco. A Wikipedia parece achar que pode ser vulnerável; portanto, se alguém tiver alguma ideia sobre quais são as cifras preferidas, isso seria ótimo.

Execute o servidor de eco node echo_server.jsque escutará na porta 9. Execute duas instâncias deste programa com node chat.js <server IP>e node chat.js <server IP> 1(o último argumento apenas define qual deles envia um prime). Cada instância se conecta ao servidor de eco. A primeira mensagem lida com a geração de chaves e as mensagens subseqüentes usam a cifra de fluxo.

O servidor de eco apenas envia tudo de volta a todos os clientes conectados, exceto o original.

Cliente

var net = require('net');
var crypto = require('crypto');
var process = require('process');
let [serverIP, first] = process.argv.slice(2);

var keys = crypto.createDiffieHellman(1024); // DH key exchange
var prime = keys.getPrime();
var k = keys.generateKeys();
var secret;

var cipher; // symmetric cipher
var decipher;

// broadcast prime
server = net.connect(9, serverIP, () => {
    console.log('connect')
    if(first) {
        server.write(prime);
        console.log('prime length', prime.length)
        server.write(k);
    }

    server.on('data', x => {
        if(!secret) { // if we still need to get the ciphers
            if(!first) { // generate a key with the received prime
                keys = crypto.createDiffieHellman(x.slice(0,128)); // separate prime and key
                k = keys.generateKeys();
                server.write(k);
                x = x.slice(128)
            }

            // generate the secret
            console.log('length x', x.length);
            secret = keys.computeSecret(x);
            console.log('secret', secret, secret.length) // verify that secret key is the same
            cipher = crypto.createCipher('rc4', secret);
            process.stdin.pipe(cipher).pipe(server);
            decipher = crypto.createDecipher('rc4', secret);
            server.pipe(decipher).pipe(process.stdout);
        }
        else {
            console.log('sent text ', x.toString()) // verify that text is sent encrypted
        }
    });
})

Servidor de eco

var net = require('net');
clients = [];

net.createServer(socket => {
    clients.forEach(c=>{socket.pipe(c); c.pipe(socket)});
    clients.push(socket);
}).listen(9)

Obrigado Dave por todas as dicas + feedback!


1
Não adicione legibilidade à versão com golf, é para isso que serve a versão sem golf. Ou, se você fizer isso, remova o ponto e vírgula antes que a linha quebre, para que tenha o mesmo comprimento.
mbomb007

@ mbomb007 a "legibilidade" é principalmente para evitar ter que rolar. infelizmente, o corpo do código não possui ponto e vírgula, portanto isso não funciona. Achei que uma rápida descoberta e substituição não seria muito onerosa. definitivamente manterá sua dica em mente para comentários futuros!
user3080953

@ Dave obrigado por todo o feedback! Fiz alterações para usar o vanilla DH, que na verdade adicionou um pouco de comprimento, porque você também precisa trocar primos. O AES funciona como um substituto, mas o problema com o AES é que nada é enviado até que você conclua um bloco, e estofamento seria uma dor. Também RC4 é menor do que aes128
user3080953

1
Eu não tinha certeza se funcionaria em uma rede, mas provavelmente não funcionaria, e escrevi em um ônibus para não ter como verificar. a nova versão usa um servidor de eco. Isso também resolve o problema de tempo limite. Eu estava tentando evitar um servidor + cliente, mas é uma forma muito melhor. finalmente, obrigado por esse desafio, aprendi uma tonelada sobre como realmente usar o nó em vez de simplesmente pegar bibliotecas de todos os lugares :)
user3080953

@ user3080953 parece bom. Com essas atualizações, você deve estar concorrendo!
21417 Dave

0

Node.js, 638 607 bytes

Agora que foi bem derrotado (e no mesmo idioma), eis a minha resposta de teste:

R=require,P=process,s=R('net'),y=R('crypto'),w=0,C='create',W='write',D='data',B='hex',G=_=>a.generateKeys(B),Y=(t,m,g,f)=>g((c=y[C+t+'ipher']('aes192',w,k='')).on('readable',_=>k+=(c.read()||'').toString(m)).on('end',_=>f(k)))+c.end(),F=C+'DiffieHellman',X=s=>s.on(D,x=>(x+'').split(B).map(p=>p&&(w?Y('Dec','utf8',c=>c[W](p,B),console.log):P.stdin.on(D,m=>Y('C',B,c=>c[W](m),r=>s[W](r+B)),([p,q,r]=p.split(D),r&&s[W](G(a=y[F](q,B,r,B))),w=a.computeSecret(p,B))))));(R=P.argv)[3]?X(s.Socket()).connect(R[3],R[2]):s[C+'Server'](s=>X(s,a=y[F](2<<9))[W](G()+D+a.getPrime(B)+D+a.getGenerator(B)+B)).listen(R[2])

Ou com embalagem:

R=require,P=process,s=R('net'),y=R('crypto'),w=0,C='create',W='write',D='data',B
='hex',G=_=>a.generateKeys(B),Y=(t,m,g,f)=>g((c=y[C+t+'ipher']('aes192',w,k=''))
.on('readable',_=>k+=(c.read()||'').toString(m)).on('end',_=>f(k)))+c.end(),F=C+
'DiffieHellman',X=s=>s.on(D,x=>(x+'').split(B).map(p=>p&&(w?Y('Dec','utf8',c=>c[
W](p,B),console.log):P.stdin.on(D,m=>Y('C',B,c=>c[W](m),r=>s[W](r+B)),([p,q,r]=p
.split(D),r&&s[W](G(a=y[F](q,B,r,B))),w=a.computeSecret(p,B))))));(R=P.argv)[3]?
X(s.Socket()).connect(R[3],R[2]):s[C+'Server'](s=>X(s,a=y[F](2<<9))[W](G()+D+a.
getPrime(B)+D+a.getGenerator(B)+B)).listen(R[2])

Uso

Esta é uma implementação de servidor / cliente; uma instanciação será o servidor e a outra o cliente. O servidor é iniciado com uma porta específica e, em seguida, o cliente é apontado para a porta do servidor. A DH pode levar alguns segundos para ser configurada se a máquina estiver com pouca entropia, portanto, as primeiras mensagens podem demorar um pouco.

MACHINE 1                       MACHINE 2
$ node e2e.js <port>            :
:                               $ node e2e.js <address> <port>
$ hello                         :
:                               : hello
:                               $ hi
: hi                            :

Demolir

s=require('net'),
y=require('crypto'),
w=0,                                      // Shared secret starts unknown
Y=(t,m,g,f)=>g(                           // Helper for encryption & decryption
  (c=y['create'+t+'ipher']('aes192',w,k=''))
  .on('readable',_=>k+=(c.read()||'').toString(m))
  .on('end',_=>f(k)))+c.end();
X=s=>s.on('data',x=>(x+'').split('TOKEN2').map(p=>
  p&&(w                                   // Have we completed handshake?
    ?Y('Dec','utf8',c=>c.write(p,'hex'),console.log) // Decrypt + print messages
    :                                     // Haven't completed handshake:
     process.stdin.on('data',m=>          //  Prepare to encrypt + send input
       Y('C','hex',c=>c.write(m),r=>s.write(r+'TOKEN2')),(
       [p,q,r]=p.split('TOKEN1'),         //  Split up DH data sent to us
       r&&                                //  Given DH details? (client)
          s.write(
            (a=y.createDiffieHellman(     //   Compute key pair...
              q,'hex',r,'hex')            //   ...using the received params
            ).generateKeys('hex')),       //   And send the public key
       w=a.computeSecret(p,'hex')         //  Compute shared secret
       //,console.log(w.toString('hex'))  //  Print if you want to verify no MITM
))))),
(R=process.argv)[3]                       // Are we running as a client?
  ?X(s.Socket()).connect(R[3],R[2])       // Connect & start chat
  :s.createServer(s=>                     // Start server. On connection:
    X(s,                                  //  Start chat,
      a=y.createDiffieHellman(1024))      //  Calc DiffieHellman,
    .write(                               //  Send public key & public DH details
      a.generateKeys('hex')+'TOKEN1'+
      a.getPrime('hex')+'TOKEN1'+
      a.getGenerator('hex')+'TOKEN2')
  ).listen(R[2])                          // Listen on requested port

O único requisito para os tokens é que eles contenham pelo menos um caractere não hexadecimal, portanto, no código minificado, outras constantes de string são usadas (data e hex).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.