Isso é tão verdadeiro na vida quanto na programação cliente-servidor: o único segredo que não pode ser comprometido é aquele que o senhor nunca revelou.
Mas, às vezes, isso é inevitável. Se o senhor deve enviar um segredo para o cliente, o senhor pode criptografá-lo. A forma mais comum de criptografia é criptografia simétricaem que a mesma chave é usada tanto para criptografar quanto para descriptografar. A maioria das linguagens tem bibliotecas relativamente fáceis de usar para criptografia simétrica. Veja como estávamos fazendo isso no .NET:
public static string Encrypt(string toEncrypt, string key, bool useHashing) { byte[] keyArray = UTF8Encoding.UTF8.GetBytes(key); byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(toEncrypt); if (useHashing) keyArray = new MD5CryptoServiceProvider().ComputeHash(keyArray); var tdes = new TripleDESCryptoServiceProvider() { Key = keyArray, Mode = CipherMode.ECB, Padding = PaddingMode.PKCS7 }; ICryptoTransform cTransform = tdes.CreateEncryptor(); byte[] resultArray = cTransform.TransformFinalBlock( toEncryptArray, 0, toEncryptArray.Length); return Convert.ToBase64String(resultArray, 0, resultArray.Length); }
É assim que funciona nossa função de criptografia simétrica:
- Começamos com uma string secreta que queremos proteger. Digamos que seja “password123”.
- Escolhemos uma chave. Vamos usar a chave “key-m4st3r”
- Antes de criptografar, vamos prefixar nosso segredo com um salt para evitar ataques de dicionário. Vamos chamar nosso sal de “NaCl”.
Chamamos a função da seguinte forma:
Encrypt("NaCl" + "password123", "key-m4ast3r", true);
A saída é uma string codificada em base64 do criptografado em TripleDES dados de bytes. Esses dados criptografados agora podem ser enviados ao cliente sem nenhum risco razoável de que a string secreta seja revelada. Sempre há irracional do tipo helicóptero silencioso do governo negro, mas, para todos os fins práticos, não há como alguém descobrir que sua senha é “password123”, a menos que sua chave seja revelada.
Em nosso caso, estávamos usando o seguinte Encrypt()
para fazer experiências com o armazenar alguns dados de estado em páginas da Web relacionadas ao processo de login. Pensamos que era seguro, pois os dados estavam criptografados. Claro que estão criptografados! Ele diz Encrypt()
bem ali no nome do método, certo?
Errado.
Há um bug nesse código. Um bug que torna nossos dados de estado criptografados vulneráveis. O senhor está vendo? Meus erros de codificação, deixe-me mostrá-los ao senhor!
string key = "SuperSecretKey"; Debug.WriteLine( Encrypt("try some different" + "00000000000000000000000000000000", key, true).Base64ToHex()); Debug.WriteLine( Encrypt("salts" + "00000000000000000000000000000000", key, true).Base64ToHex()); 3908024fc33b55c3 4e885c8946b80735 704cbe2a41d25f21 81bb6d726bd35152 81bb6d726bd35152 81bb6d726bd35152 1367f10f2584ace3 4ae7661295a98e46 81bb6d726bd35152 81bb6d726bd35152 81bb6d726bd35152 4ee5d23b3b5e3eb4
(Estou usando strings com múltiplos de 8 aqui para facilitar as conversões para Base64).
O senhor está vendo o erro agora? É uma viagem curta daqui até a adulteração ilimitada de dados, especialmente porque os dados de estado do processo de login continham strings inseridas pelo usuário. Um invasor poderia simplesmente enviar o formulário quantas vezes quisesse, cortar os valores de ataque criptografados do meio e inseri-los na próxima solicitação criptografada. que será felizmente descriptografada e processada como se nosso código a tivesse enviado!
O culpado é esta linha de código:
{ Key = keyArray, Mode = CipherMode.ECB, Padding = PaddingMode.PKCS7 }
O que, para nosso constrangimento, é um incrivelmente parâmetro estúpido para uso em criptografia simétrica:
O modo Electronic Codebook (ECB) criptografa cada bloco individualmente. Isso significa que quaisquer blocos de texto simples que sejam idênticos e estejam na mesma mensagem, ou em uma mensagem diferente criptografada com a mesma chave, serão transformados em blocos de texto cifrado idênticos. Se o texto simples a ser criptografado contiver uma repetição substancial, é possível que o texto cifrado seja quebrado um bloco de cada vez. Além disso, é possível que um adversário ativo substitua e troque blocos individuais sem ser detectado.
É bastante comum que os algoritmos de criptografia simétrica usem o feedback do bloco anterior para semear o próximo bloco. Sinceramente, eu não sabia que isso era possível escolher um modo de cifra que não fizesse algum tipo de encadeamento de blocos! CipherMode.ECB
? Mais como CipherMode.Fail
!
Então, o que aprendemos?
- Se não for o caso tem a ser enviado ao cliente, então não o faça! Os segredos enviados ao cliente podem ser potencialmente adulterados e comprometidos de várias maneiras que não são fáceis de ver ou mesmo prever. No nosso caso, podemos armazenar o estado de login no servidor e evitar a transmissão de qualquer estado para o cliente.
- Não se trata de criptografia até que o senhor dedique um tempo para entender completamente os conceitos por trás do código de criptografia. Especificamente, não notamos que nossa função de criptografia estava usando um código altamente questionável
CipherMode
altamente questionável que permitia a substituição em nível de bloco dos dados criptografados.
Felizmente, essa era uma página um tanto experimental no site, de modo que pudemos voltar rapidamente à nossa abordagem padrão do lado do servidor quando a exploração foi descoberta. Eu não sou Bruce Schneier, mas eu tenho uma compreensão razoável dos conceitos de criptografia. E eu ainda ignorou completamente esse problema.
Portanto, da próxima vez que o senhor se sentar para escrever algum código de criptografia, considere cuidadosamente os dois pontos acima. Caso contrário, como nós, o senhor ficará se perguntando por que sua criptografia não está… criptografia.
(Agradecemos a Daniel LeCheminant por sua ajuda na descoberta desse problema).