DevSecOps

15 abr, 2024

Como implementar criptografia simétrica e assimétrica usando NodeJS

Publicidade

Em um mundo digital, onde cada vez mais dados são transmitidos para todos os lugares, aumenta a preocupação sobre o destino desses dados. Muitas empresas oferecem serviços sem criptografia, deixando dados sensíveis expostos a riscos desnecessários. Anualmente, presenciamos vazamentos de dados pessoais, e por isso na SuperViz, empresa que trabalho atualmente, fizemos um trabalho para garantir a segurança e privacidade dos dados dos nossos usuários.

Depois de muito estudo e de ser responsável por implementar internamente na empresa, eu gostaria de compartilhar este artigo para ajudar a reduzir tais incidentes. No final do artigo você irá ser capaz de criar criptografias utilizando chaves simétricas e utilizando chaves assimétricas.

Chaves simétricas e assimétricas

Antes de entrar no assunto de criptografia, é extremamente válido saber a diferença entre chaves simétricas e assimétricas, são elas:

Criptografia Simétrica:

– Usa uma única chave para tanto criptografar quanto descriptografar os dados;

– A mesma chave é compartilhada entre o remetente e o destinatário da mensagem;

– É mais rápida e eficiente em termos de processamento do que a criptografia assimétrica.

Criptografia Assimétrica (ou criptografia de chave pública):

– Usa um par de chaves: uma chave pública e uma chave privada;

– A chave pública é usada para criptografar os dados, enquanto a chave privada é usada para descriptografar os dados;

– Cada destinatário tem sua própria chave privada e uma chave pública, sendo somente a chave pública compartilhada com outros interessados;

– É mais seguro do que a criptografia simétrica devido à separação das chaves pública e privada;

– É geralmente mais lenta e requer mais recursos computacionais do que a criptografia simétrica.

Em resumo, a principal diferença entre criptografia simétrica e assimétrica está na forma como as chaves são geradas e usadas para criptografar e descriptografar os dados, bem como na eficiência e segurança relativas de cada abordagem.

Criptografando com chaves simétricas

Neste artigo vamos usar a criptografia AES-256-CBC,  sendo CBC a sigla para Cipher Block Chaining.

O CBC (Cipher Block Chaining, ou em português Criptografia de Blocos Encadeados) é uma maneira de fazer a criptografia de informações. Imagine que você tem uma mensagem que quer manter segura, com o CBC funciona assim:

1. Divida a mensagem em pedaços pequenos, chamados de blocos;

2. Em vez de simplesmente criptografar cada bloco separadamente, como acontece em alguns métodos, no CBC cada bloco é misturado com o bloco anterior antes de ser criptografado. É como se cada bloco dependesse do que veio antes dele;

3. Isso cria uma espécie de “corrente” de blocos, onde cada bloco é ligado ao anterior;

4. Essa ligação entre os blocos torna mais difícil para alguém que não tem a chave correta decifrar a mensagem. Mesmo se alguém tentar mexer em um bloco, isso afetará todos os blocos seguintes, tornando a decifração muito mais difícil.

Sabendo como o CBC funciona, vamos criar nossa primeira mensagem criptografada com AES-256-CBC! Imagine que vamos criar uma função chamada encrypt, ela será responsável por criar a chave simétrica e fazer a criptografia, ela terá como parâmetros: salt, passphrase, text.

O passphrase será necessário para usar na criptografia para descriptografar o conteúdo. Além disso, vamos precisar de um salt, que é um texto aleatório utilizado como base para criptografar os dados. Este salt aumentará a segurança da criptografia, tornando mais difícil para terceiros decifrarem o conteúdo protegido.

Na primeira linha da função, vamos gerar a chave simétrica. Essa chave será a única chave utilizada tanto para criptografar quanto para descriptografar o conteúdo. Ela será compartilhada entre o remetente e o destinatário.

import * as crypto from 'crypto'

const salt = randomBytes(32).toString('hex');
const passphrase = 'password';

const encrypt = (passphrase, salt, text) => {
  const key = scryptSync(passphrase, salt, 32)
  ...
}

Na linha 7, criamos a nossa chave simétrica. Na próxima linha, precisaremos gerar o Initialization Vector. O Initialization Vector (IV) é tipo um ingrediente secreto na criptografia, ele é um valor aleatório que se junta com a chave para embaralhar os dados, o que torna a cifragem mais segura e difícil de quebrar.

const iv = crypto.randomBytes(16)

Agora vamos cifrar a mensagem usando nossa key e iv.

1. Criaremos nossa cifra passando 3 parâmetros: o algoritmo, a key e o iv;

2. Adicionamos o texto a cifra e trocamos o formato do texto de utf-8 para hex;

3. Obtemos o conteúdo final cifrado.

const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
let encrypted = cipher.update(text, 'utf8', 'hex')

encrypted += cipher.final('hex')

Estamos nos aproximando do final, mas surge um desafio. Para descriptografar o conteúdo da mensagem, precisamos do IV (Initialization Vector), no entanto, é crucial que cada IV seja único e aleatório. Armazenar o IV ao lado da mensagem criptografada comprometeria a segurança. Uma solução comumente adotada é mesclar o IV com a mensagem criptografada em locais estratégicos, mantendo sua posição confidencial.

const part1 = encrypted.slice(0, 17)
const part2 = encrypted.slice(17)

return `${part1}${iv.toString('hex')}${part2}`

É importante salientar que, nas partes 1 e 2, nosso texto criptografado contém apenas 32 caracteres.

E no final, nossa função de criptografia ficou assim:

import * as crypto from 'crypto'

const salt = crypto.randomBytes(32).toString('hex');
const passphrase = 'password';

const encrypt = (passphrase, salt, text) => {
  const key = crypto.scryptSync(passphrase, salt, 32)
  const iv = crypto.randomBytes(16)

  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv)
  let encrypted = cipher.update(text, 'utf8', 'hex')

  encrypted += cipher.final('hex')

  const part1 = encrypted.slice(0, 17)
  const part2 = encrypted.slice(17)

  return `${part1}${iv.toString('hex')}${part2}`
}

Descriptografando com chaves simétricas

Agora que temos nosso conteúdo criptografado, em algum momento vamos precisar descriptografar o conteúdo. Alguns passos serão semelhantes ao que fizemos ao criptografar o conteúdo. Primeiramente vamos recuperar nossa key

import * as crypto from 'crypto'

const salt = crypto.randomBytes(32).toString('hex');
const passphrase = 'password';

const decrypt = (passphrase, salt, text) => {
  const key = crypto.scryptSync(passphrase, salt, 32)
  ...
}

Em seguida vamos definir as posições que colocamos na hora de cifrar nosso conteúdo que no caso deste artigo estamos utilizando a posição 17:

onst ivPosition = {
  start: 17,
  end: 17 + 32
}

const iv = Buffer.from(text.slice(ivPosition.start, ivPosition.end), 'hex')

Tendo o IV em mãos, vamos pegar apenas o conteúdo cifrado:

const part1: string = text.slice(0, ivPosition.start)
const part2: string = text.slice(ivPosition.end)

const encryptedText = `${part1}${part2}`

Agora temos tanto o conteúdo cifrado e o IV, podemos então iniciar o processo de decifrar o conteúdo. O processo é muito semelhante ao de cifrar:

1. Criaremos nosso decifrador passando 3 parâmetros: o algoritmo, a key e o iv;

2. Adicionamos o texto cifrado e trocamos o formato do texto de hex para utf-8;

3. Obtemos o conteúdo final decifrado.

const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
let decrypted = decipher.update(encryptedText, 'hex', 'utf8')
decrypted += decipher.final('utf8')

return decrypted

No final temos um código semelhante a isso:

import * as crypto from 'crypto'

const salt = crypto.randomBytes(32).toString('hex');
const passphrase = 'password';

const decrypt = (passphrase, salt, text) => {
  const key = crypto.scryptSync(passphrase, salt, 32)
  const ivPosition = {
    start: 17,
    end: 17 + 32
  }

  const iv = Buffer.from(text.slice(ivPosition.start, ivPosition.end), 'hex')
  const part1: string = text.slice(0, ivPosition.start)
  const part2: string = text.slice(ivPosition.end)

  const encryptedText = `${part1}${part2}`

  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv)
  let decrypted = decipher.update(encryptedText, 'hex', 'utf8')
  decrypted += decipher.final('utf8')

  return decrypted
}

Criando chaves assimétricas

As chaves assimétricas, ou criptografia de chave pública, transformaram a maneira como protegemos informações sensíveis online. Isso garante segurança extra, embora seja mais lento e demande mais recursos.

O NodeJS oferece suporte a uma variedade de tipos de chaves assimétricas para atender às diversas necessidades de criptografia. Esses incluem:

RSA: Um algoritmo amplamente utilizado para criptografia assimétrica, conhecido por sua segurança e eficiência;

DSA: Digital Signature Algorithm, frequentemente usado para assinaturas digitais e autenticação;

EC (Elliptic Curve): Algoritmo de criptografia de curva elíptica, que oferece um alto nível de segurança com chaves menores em comparação com RSA;

ed25519: Um sistema de assinatura digital de alto desempenho baseados em curvas elípticas;

ed448: Similar ao ed25519, mas com um nível mais alto de segurança devido ao tamanho maior da chave;

x25519 e x448: Protocolos de troca de chaves baseados em curvas elípticas, oferecendo segurança e eficiência em ambientes restritos.

Essa variedade de opções permite que os desenvolvedores escolham o algoritmo mais adequado para suas necessidades específicas, equilibrando segurança, desempenho e eficiência.

Para este artigo, iremos utilizar a implementação do algoritmo RSA, dada sua ampla utilização. Na etapa inicial, é crucial gerar as chaves assimétricas: a Chave Pública e a Chave Privada. Para isso, é essencial especificar os tipos e formatos dessas chaves. Optaremos pelo formato PEM (Privacy-Enhanced Mail) para ambas as chaves, garantindo assim uma abordagem consistente e segura.

generateKeyPair(passphrase: string): KeyPair {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: 4096,
    publicKeyEncoding: {
      type: 'spki',
      format: 'pem'
    },
    privateKeyEncoding: {
      type: 'pkcs8',
      format: 'pem',
      cipher: 'aes-256-cbc',
      passphrase: passphrase
    }
  })

  return {
    publicKey,
    privateKey
  }
}

Primeiramente, é importante entender a função generateKeyPairSync. Ela é responsável por gerar um par de chaves: uma pública e uma privada.

Na geração dessas chaves, alguns parâmetros são fundamentais:

modulusLength: Este parâmetro define o comprimento do módulo. No caso do RSA, o comprimento mínimo é de 4096;

publicKeyEncoding: Esse objeto estabelece o tipo e o formato da chave pública. O tipo é definido como spki (Subject Public Key Info), um formato padrão para chaves públicas. Já o formato é pem, que é um tipo de codificação base64;

privateKeyEncoding: Este objeto define o tipo, formato, cifra e frase secreta da chave privada. O tipo é definido como pkcs8, também um formato padrão para chaves privadas. O formato é pem, semelhante à chave pública. A cifra é definida como aes-256-cbc, um algoritmo de criptografia como vimos anteriormente. A frase secreta, a nossa passphrase, é a senha utilizada para criptografar a chave privada.

No NodeJS, existem dois tipos de chave pública (pkcs1 e spki) e dois de chave privada (pkcs1 e pkcs8) ao utilizar o RSA.

Aqui, optamos pelo formato spki / pkcs8. Para adicionar uma camada extra de segurança, também utilizamos o algoritmo AES-256-CBC (Cipher Block Chaining) como mencionado anteriormente.

Como resultado, a função generateKeyPairSync retorna um objeto contendo as chaves pública e privada geradas.

Diferenças entre PKCS1 e PKCS8

PKCS é a sigla para “Public Key Cryptography Standards”, que é um conjunto de normas desenvolvidas para auxiliar a implementação da criptografia de chave pública. Dentre essas normas, temos PKCS1 e PKCS8, que descrevem formatos para a codificação de chaves privadas.

PKCS1 é o primeiro padrão PKCS, usado especificamente para a codificação de chaves RSA. Ele detalha o processo de codificação de uma chave privada RSA, que é relativamente simples, pois uma chave RSA requer apenas alguns números inteiros.

Em contraste, PKCS8 é um padrão mais abrangente, empregado para codificar qualquer tipo de chave privada. Ele pode ser usado não só para chaves RSA, mas também para outros tipos de chaves, como DSA ou EC (Elliptic Curve). Assim, PKCS8 é um formato mais geral em comparação com PKCS1, pois inclui informações sobre o tipo de chave na estrutura de dados da chave privada.

Resumindo, a diferença entre PKCS1 e PKCS8 é que PKCS1 é específico para chaves RSA, enquanto PKCS8 pode ser usado para codificar qualquer tipo de chave privada.

Criptografando com chaves assimétricas

Com nossa Public Key à disposição, é muito fácil criptografar qualquer mensagem.

1. Primeiro, criamos uma função que requer dois parâmetros: text e publicKey;

2. Em seguida, usamos a função publicEncrypt, que será usada para criptografar o texto;

3. Depois disso, usamos Buffer.from(text, 'utf8') para converter o texto em um buffer com codificação UTF-8;

Por fim, usamos toString('base64') para converter o buffer para base64.

const encrypt = (text, publicKey) => {
  return publicEncrypt(publicKey, Buffer.from(text, 'utf8')).toString('base64');
}

Com isso, temos nossa mensagem criptografada pronta para ser armazenada em um banco de dados ou algo semelhante.

Descriptografando com chaves assimétricas

Para descriptografar um conteúdo, precisamos apenas da nossa Private Key e a passphrase especificada ao criar as chaves assimétricas.

1. Primeiro, criamos uma função que requer dois parâmetros: text e PrivateKey;

2. Em seguida, usamos a função privateDecrypt, que será usado para descriptografar o texto;

3. Passamos para a função a PrivateKey e a Passphrase;

4. Depois disso, usamos Buffer.from(text, 'base64') para converter o texto em um buffer de base64;

Por último usamos .toString('utf8') para converter todo o conteúdo para utf8

const decrypt = (encryptedText, privateKey) => {
  return privateDecrypt({
    key: privateKey,
    passphrase
  }, Buffer.from(encryptedText, 'base64')).toString('utf8');
}

Após essas etapas, temos nossa mensagem original de volta. Com isso, completamos o ciclo completo de criptografia e descriptografia usando chaves assimétricas.

Em conclusão, a criptografia desempenha um papel crucial na proteção de dados e informações sensíveis na era digital atual.

Espero que, por esse artigo, você tenha entendido mais aprofundada as diferenças e aplicações entre criptografia simétrica e assimétrica. Além disso, que com os exemplos de código fornecidos, você deve consiguir implementar suas próprias funções de criptografia e descriptografia em NodeJS.

Lembre-se, a segurança dos dados é uma responsabilidade importante e a criptografia é uma das ferramentas mais eficazes que podemos usar para garantir essa segurança.