a prática e o estudo de técnicas para comunicação segura na presença de terceiros
a construção e análise de protocolos que previnam terceiros de ler mensagens privadasWikipedia
Texto cifrado
42a14c854faed
só o destinatário autorizado deve ser capaz de extrair o conteúdo da mensagem da sua forma cifrada
o destinatário deverá ser capaz de verificar se a mensagem foi alterada durante a transmissão
o destinatário deverá ser capaz de verificar que se o remetente é realmente quem diz ser
não deverá ser possível ao remetente negar a autoria de sua mensagem
Usa a mesma chave para os dois processos
Portanto, essa chave deve permanecer secreta
Operam com tamanho fixos de blocos de informação
Trabalham com qualquer stream de bits
Processo que será responsável por executar a cifra em cada um dos blocos
O Vetor de Inicialização (IV) é um valor aleatório responsável por oferecer o estado inicial ao algoritmo e não pode ser reutilizado ou previsível
Principais algoritmos
Não oferecem integridade e autenticidade
Ou seja, não é possível saber se alguém alterou ou forjou a mensagem
Iremos saber mais ao falar de hashing
Algoritmos
$iv = random_bytes(
openssl_cipher_iv_length('aes-256-gcm')
);
$ciphertext = openssl_encrypt(
$data, // plaintext
'aes-256-gcm', // cipher
$key, // key
OPENSSL_RAW_DATA, // flags
$iv, // iv (nonce)
$tag, // tag
$aad, // AAD
16 // tag length
);
$message = base64_encode($iv . $tag . $ciphertext);
PHP
const { readFileSync } = require('fs');
const { createCipheriv, randomBytes } = require('crypto');
const key = readFileSync('app.key').toString().trim();
const nonce = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, nonce, {
authTagLength: 16
});
const ciphertext = cipher.update(plaintext, 'utf8');
cipher.final();
const tag = cipher.getAuthTag();
const message = Buffer
.concat([nonce, tag, ciphertext])
.toString('hex');
scripts/symmetric/node-gcm-encrypt.js
Indicadas quando não se sabe o tamanho do dado
Melhor opção é usar solução de hardware, mas pode ser caro e não estar disponível na infraestrutura
Exemplos: HSM (Hardware Security Module) e TPM (Trusted Platform Module)
Serviços do seu provedor de Cloud
Exemplos: AWS Key Management Service e Google Cloud Key Management
Para Kubernetes ou Swarm, checar as features de Secrets
PS: o Kubernetes não criptografa os dados por padrão, então siga o artigo Encrypting Secret Data at Rest
git-secret também é uma alternativa e acompanha o versionamento do seu código
Dependendo da criticidade da aplicação, você pode dividir a chave e colocar cada parte em uma localização diferente
Utiliza duas chaves, sendo cada uma responsável por um dos processos
Pode ser distribuída para outros
Deve permanecer com o portador
Também conhecida como Criptografia de chave pública
Se você quer que terceiros se comuniquem com você...
Usada por eles para escrevem para você
↓
Encriptação
Usada por você para ler o texto recebido
↓
Decriptação
Se você quiser se comunicar com outros...
Usada por eles para verificar se o texto realmente veio de você
↓
Decriptação
Usada por você para escrever textos
↓
Encriptação
O RSA, algoritmo bastante conhecido, depende de números semi-primos (que são aqueles com exatamente dois números primos como fatores) muito grandes
A segurança desse algoritmo consiste na dificuldade computacional em fatorar esses números
Outros algoritmos (como ED25519) são baseados em curvas elípticas: pontos
gerados por equações cúbicas como y² = x³ + ax + b
Nesses casos, o cálculo do logaritmo discreto ainda é muito custoso computacionalmente
Em ambos os casos, a segurança se baseia no fato das operações reversas serem muito custosas com o poder computacional existente
Já existem estudos de criptografia pós-quântica para resistirem a ataques de computadores quânticos
Na prática, por causa de limitações do tamanho do texto a ser cifrado em alguns algoritmos (como no RSA, que com chaves de 2048 bits só permite cifrar blocos de até 214 bytes) e por serem mais lentos na maioria dos casos, o que geralmente acontece é:
const crypto = require('crypto');
const publicKey = crypto.createPublicKey(
fs.readFileSync('/path/to/public.key').toString()
);
const encryptedData = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha512',
},
Buffer.from(message)
);
console.log(encryptedData.toString('hex'));
scripts/asymmetric/node-rsa-encrypt.js
const crypto = require('crypto');
const privateKey = crypto.createPrivateKey(
fs.readFileSync('/path/to/private.key').toString()
);
const decryptedData = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha512',
},
Buffer.from(message, 'hex')
);
console.log(decryptedData.toString);
scripts/asymmetric/node-rsa-decrypt.js
é uma função que mapeia dados de tamanho arbitrário para dados de tamanho fixoWikipedia
Os algoritmos mais comuns são:
Para fins de segurança, devemos optar por:
Apenas criptografar sem fazer o hash não é o suficiente, pois algum agente malicioso pode alterar a mensagem
Demonstração simples de bit flipping
function login(username, password) {
const user = this.findByUsername(username);
if (!user) {
return false;
}
if (!this.verifyPassword(password, user.password)) {
return false;
}
return true;
}
É possível saber que usuários existem em sua aplicação através de Timing attacks
ataque no qual um atacante tenta comprometer um sistema criptográfico analisando o tempo gasto para executar determinados algoritmosWikipedia
Será que não estamos exagerando?
É realmente possível ver essa diferença?
function login(username, password) {
const user = this.findByUsername(username);
// this.generateFakePassword = () => 'abcdefg123456'
let storedPassword = this.generateFakePassword();
if (user !== null) {
storedPassword = user.password;
}
return (this.verifyPassword(password, storedPassword))
&& (user !== null);
}
Código resistente a timing attacks
Exercício: criar um algoritmo para comparar duas strings
false
false
true
Exercício: criar um algoritmo para comparar duas strings
Cada uma das comparações do item 4. irá levar um certo tempo, que chamaremos de
1u
(que provavelmente serão alguns milésimos de segundo)
Então, para comparar as strings abcdef
e abcdef
...
Comparação | Tempo decorrido |
---|---|
Tamanhos são iguais | - |
a é igual a a |
1u |
b é igual a b |
2u |
c é igual a c |
3u |
d é igual a d |
4u |
e é igual a e |
5u |
f é igual a f |
6u |
Agora, para comparar as strings abcdef
e abcdxf
...
Comparação | Tempo decorrido |
---|---|
Tamanhos são iguais | - |
a é igual a a |
1u |
b é igual a b |
2u |
c é igual a c |
3u |
d é igual a d |
4u |
e é igual a x |
5u |
Use comparações de tempo constante entre hashes
// Ao invés de if ($hash1 === $hash2) {
if (hash_equals($hash1, $hash2)) {
// ...
}
PHP
Use comparações de tempo constante entre hashes
const { timingSafeEqual } = require('crypto');
const buffer1 = Buffer.from('hash1');
const buffer2 = Buffer.from('hash2');
// Ao invés de if (buffer1.equals(buffer2)) {
let check = false;
try {
check = timingSafeEqual(buffer1, buffer2);
} catch (err) {
// false
}
Node.js
Você deve usar algoritmos de hashing ao invés de encriptação
Em ordem de recomendação:
// Para salvar
public function setPassword(string $password): string {
return \password_hash($password, PASSWORD_ARGON2ID);
}
// Para verificar
public function checkPassword(User $user, string $password): bool {
return \password_verify($password, $user->getPassword());
}
PHP
const argon2 = require('argon2');
try {
// Para salvar
const hash = await argon2.hash(
password,
{ type: argon2.argon2id }
);
// Para verificar
if (await argon2.verify(hash, password)) {
// ...
}
} catch (err) {
// algum erro
}
Node.js (pacote argon2)