Como um servidor pode armazenar e sincronizar nossos segredos sem ter as chaves para lê-los? Seguiremos como Confidex cria suas chaves, protege-as em um cadeado e as utiliza para criptografar dados e verificar alterações.
Cadeado
Só o Escolhido pode abrir a porta
Matrix Reloaded
Imagine que estamos construindo um vault virtual para armazenar dados secretos. Ele deve manter esses dados em um laptop após o fechamento do aplicativo e permitir que os abramos em outro dispositivo. O cofre precisará de suas próprias chaves. Devemos armazená-los de modo que a cópia dos arquivos não permita que alguém de fora possa ler ou alterar seu conteúdo.
Um lock protege as chaves: um pequeno arquivo criptografado. Abri-lo requer uma senha mestra e um código secreto, secret_key; nenhum deles está no próprio arquivo. Poderíamos imprimir o cadeado inteiro no jornal da manhã. Os leitores receberiam uma cópia, mas publicá-la não revelaria as chaves privadas.
Suponha que alguém de fora obtenha esse arquivo e até aprenda a senha mestra. Eles agora podem repetir todos os cálculos que precisam apenas da senha, mas esses cálculos não produzirão a chave que abre a fechadura: secret_key também é uma entrada. O arquivo não contém nenhuma verificação separada apenas da senha. Abri-lo ainda requer o código secreto desconhecido.
Esse código vem de dados criptograficamente aleatórios; uma pessoa não inventa isso. Enquanto secret_key for desconhecido, mesmo uma senha mestra adivinhada corretamente não abre a fechadura: o invasor ainda não possui um segredo aleatório. Portanto, uma senha fraca por si só não transforma a cópia do servidor em um ataque de dicionário prático.
Se o invasor também obtiver secret_key, ele poderá testar tentativas de senha mestra off-line. Isto é caro, mas a eventual dificuldade depende da força da senha.
Chaves
Senhores, o que as chaves fazem? Chaves… desbloqueiam coisas?
Piratas do Caribe – O Baú da Morte
O aplicativo obtém 32 bytes aleatórios do gerador criptográfico do sistema operacional e os prepara para uso como chave. Esses bytes preparados são a chave mestra privada, master_key.
A aplicação sempre deriva as mesmas chaves de master_key: repetir os mesmos cálculos nos mesmos bytes dá o mesmo resultado. Um laptop e um telefone podem, portanto, recuperar chaves idênticas. Para obter master_key em um novo dispositivo, o aplicativo deve primeiro abrir o cadeado com a senha mestra e secret_key.
O primeiro par: criptografia
Eu sou o mestre das chaves.
Caça-fantasmas
Para armazenar dados criptografados, o aplicativo calcula uma chave pública de master_key: secret_key. Ele pode criptografar dados para o cofre, enquanto a descriptografia requer master_key.
“Público” descreve o papel desta chave no algoritmo, não uma obrigação de fornecê-la a todos. Em Confidex fica com o usuário. É também o segundo segredo necessário para abrir o bloqueio, por isso permanece local e não é enviado pela rede. Por si só, secret_key permite que alguém criptografe dados para o cofre, mas não os descriptografa nem assina alterações em seu nome.
X25519 é o algoritmo que pega master_key e calcula secret_key. Recuperar master_key de secret_key é praticamente inviável. Um ataque clássico a um supercomputador imaginário executando um bilhão de bilhões de operações de curva por segundo levaria cerca de 2,3 trilhões de anos em média (estimativa do SafeCurves).
O segundo par: assinaturas
A única coisa original é a assinatura
Pegue-me se puder
O primeiro par ajuda a ocultar o conteúdo. Agora precisamos de uma forma de confirmar se o proprietário do cofre autorizou uma alteração. O servidor também deve ser capaz de verificar assinaturas, por isso precisamos enviar uma chave de verificação pública. A parte pública do primeiro par é secret_key, que o servidor não deve receber. Portanto, criamos um segundo par de chaves permanente para assinaturas.
Em vez de armazenar outro segredo independente, derivamos uma chave de assinatura privada de master_key e a chamamos de signing_key. O aplicativo o utiliza para assinar dados e o bloqueio. Outros dispositivos e o servidor utilizam sua parte pública para verificar assinaturas sem ler o conteúdo.
Podemos recuperar este par do mesmo master_key. O aplicativo deriva dele um valor inicial: 32 bytes que determinam exclusivamente a chave de assinatura.
Para derivar chaves diferentes para trabalhos diferentes, incluímos um rótulo fixo no cálculo: aqui, confidex/vault/ed25519/v1. O mesmo master_key e o mesmo rótulo sempre produzem a mesma chave.
A senha mestra não inseriu esses cálculos. Ambos os pares dependem dos bytes aleatórios de master_key, não de uma string inventada por uma pessoa. Esta distinção será importante mais tarde: a senha protegerá o acesso às chaves que já existem, para que possamos alterá-la sem alterar as próprias chaves.
A derivação usa uma função de derivação de chave baseada em hash: HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex usa-o com SHA-256, daí o nome HKDF-SHA256. Os 32 bytes resultantes são os seed do par Ed25519. A parte pública de signing_key não revela secret_key, então podemos enviá-lo ao servidor para verificação de assinatura.
O que é um arquivo de bloqueio e o que ele faz?
Abra as portas do compartimento das cápsulas, HAL.
Uma Odisseia no Espaço
Sinto muito, Dave. Receio não poder fazer isso.
O bloqueio armazena um master_key criptografado. Abri-lo requer a senha mestra e secret_key. O cabeçalho público contém a versão do formato, o identificador do cofre, a parte pública de signing_key, um salt e os parâmetros de processamento de senha. A versão do formato interno é criptografada junto com master_key.
Ao criar o bloqueio, a aplicação já possui master_key, então ela calcula secret_key antes da criptografia. Ao abri-lo, o usuário fornece secret_key separadamente: ele está ausente da cópia pública do cadeado.
Após a descriptografia, o aplicativo calcula a parte pública de master_key usando X25519 e exige que ela corresponda a secret_key. Em seguida, recupera signing_key e compara sua parte pública com a chave no cabeçalho. Desta forma, a fechadura verifica a si mesma.
Para retardar a adivinhação da senha mestra, cada tentativa deve custar tempo e memória. Confidex usa Argon2id com 64 MiB de memória e três passagens. O processamento de senha também usa um sal aleatório. O salt não é segredo: faz com que a mesma senha produza resultados diferentes em fechaduras diferentes.
Se secret_key já for conhecido, o comprimento da senha altera drasticamente a escala de um ataque. Existem cerca de 12 milhões de combinações de 5 letras latinas minúsculas aleatórias, cerca de 8 bilhões de 7 e cerca de 95 quatrilhões de 12. Passar de 7 para 12 letras aumenta o espaço de pesquisa em quase 12 milhões de vezes. Se a pesquisa de 7 letras levasse em média um dia em uma determinada máquina, 5 levariam cerca de dois minutos e 12 cerca de 33.000 anos.
Para obter uma chave do resultado Argon2id e secret_key, usamos a função HKDF-SHA256 introduzida anteriormente, com o rótulo confidex/lock/wrap/v1. O resultado é wrapping_key, a chave que criptografa a fechadura.
Devemos esconder master_key e protegê-lo contra adulterações. XChaCha20-Poly1305 criptografa-o com wrapping_key e adiciona uma tag de autenticação. A tag vincula o texto cifrado ao cabeçalho público: uma senha errada ou uma alteração nos bytes protegidos faz com que a abertura falhe. A tag e um valor aleatório de uso único, o nonce, são armazenados com o texto cifrado.
Cada vez que o bloqueio é criado ou criptografado novamente, o aplicativo obtém um novo salt e nonce. O conteúdo criptografado inclui um byte de versão e os 32 bytes de master_key; um nonce de 24 bytes e uma tag de 16 bytes são armazenados junto com eles. A documentação do algoritmo descreve a construção.
Uma incompatibilidade entre as chaves recuperadas pode resultar de um bloqueio deliberadamente malformado ou de um bug no programa que o criou. A corrupção aleatória de bytes geralmente é detectada mais cedo, quando a tag é verificada.
O servidor não possui os segredos necessários para verificar esta tag. Para transferir o bloqueio, a aplicação adiciona uma assinatura feita com signing_key. Abrange todo o arquivo finalizado, incluindo o cabeçalho público e o conteúdo criptografado. O servidor verifica a assinatura e, no primeiro upload, fixa toda a parte pública de signing_key e o identificador interno do cofre. Ele verifica as alterações subsequentes em relação a esta chave. O bloqueio e os dados criptografados são assinados com o mesmo signing_key, recuperado de master_key.
O cofre
Agora podemos publicar o bloqueio. Somente alguém que conhece secret_key e master password pode ler master_key, enquanto qualquer pessoa pode pegar a chave de assinatura pública do arquivo de bloqueio e verificar os dados assinados sem ver o conteúdo do bloqueio ou dos dados.
Considere uma senha, SuperPa55worD. Por enquanto, queremos apenas salvá-lo: depois de fechar o aplicativo, poderemos enviar o arquivo de bloqueio e a senha criptografada para qualquer lugar sem que ninguém veja SuperPa55worD, ao mesmo tempo em que permitiremos que verifiquem nossa autoria. A criptografia deve ser reversível quando tivermos secret_key e master password. Uma cifra simétrica pode criptografar e descriptografar com a mesma chave. Como não armazenamos essa chave, devemos ser capazes de recuperá-la usando o cadeado. Vamos chamá-lo de message_key.
Obteremos um message_key separado para cada nova criptografia. Ainda precisamos providenciar sua recuperação: deixar a chave aberta ao lado do texto cifrado permitiria que qualquer pessoa com uma cópia pudesse ler tudo.
A partir de novos bytes aleatórios, o aplicativo cria um par X25519 temporário apenas para esta criptografia: record_key. Em seguida, ele pega a parte privada de record_key e a chave pública permanente do cofre, secret_key, e calcula um segredo compartilhado. Uma função separada deriva message_key desse segredo; esta chave criptografa nossa senha.
Ao ler, o aplicativo segue outro caminho. Ele usa master_key e a parte pública de record_key. Estas duas chaves produzem o mesmo segredo compartilhado. Matematicamente, ambas as rotas multiplicam o mesmo ponto inicial da curva por dois números privados, em ordem diferente. A ordem não altera o resultado. Podemos, portanto, recuperar a chave de descriptografia sem manter a parte privada de record_key.
Obter este segredo a partir das duas chaves públicas é praticamente impossível: qualquer uma das rotas necessita da chave privada correspondente. Os pontos públicos não nos permitem recuperar de forma eficiente os números privados que os produziram. Este truque matemático sustenta a segurança do cofre. Mas isso não impede que alguém nos dê um cofre diferente com a nossa fechadura presa a ele.
Agora podemos armazenar o texto cifrado junto com a parte pública de record_key. A parte privada de record_key e o usado message_key não são mais necessários para leituras futuras. Posteriormente, o aplicativo recupera a chave de descriptografia de master_key e a parte pública armazenada de record_key. Um segredo de cofre permanente pode, portanto, descriptografar muitos valores criptografados separadamente, cada um com seu próprio message_key recuperado quando necessário.
Não basta ocultar o texto: é preciso também detectar adulterações ao lê-lo. A criptografia, portanto, adiciona uma etiqueta de autenticação, um valor curto verificado junto com os dados criptografados. Se a verificação falhar, o aplicativo rejeitará o resultado. Não deve exibir texto aleatório ou alterado como uma senha descriptografada com sucesso.
Descrevemos uma construção na qual um par assimétrico ajuda a estabelecer um segredo compartilhado, enquanto message_key criptografa o conteúdo. Isso é chamado de Hybrid Public Key Encryption ou HPKE e é especificado em RFC 9180. Confidex usa X25519 para concordar com o segredo, HKDF-SHA256 para derivar chaves e ChaCha20-Poly1305 para criptografia com verificação de integridade. Aqui, HKDF pega o segredo compartilhado acordado usando record_key e o par master_key; este é um trabalho separado de derivar o Ed25519 seed. RFC 5869 descreve HKDF.
Para a próxima criptografia, o aplicativo cria um novo contexto record_key e um novo contexto HPKE. Reutilizar uma combinação de message_key e nonce quebra a proteção da cifra. Em Confidex, a chave e nonce são derivadas de HPKE, usadas para uma mensagem, e então o contexto é descartado.
O próprio ChaCha20 usa um message_key de 256 bits. Para ter uma ideia de um ataque direto, imagine um supercomputador fictício testando um bilhão de bilhões de candidatos por segundo. Pesquisar as 2²⁵⁶ chaves possíveis levaria cerca de 1,8 × 10⁵¹ anos. RFC 8439 descreve o algoritmo e sua análise.
Um recorde
Estamos felizes?
Ficção Pulp
Salvamos a senha, mas o login em um site geralmente também requer um nome de usuário e um endereço. Às vezes precisamos de uma nota ao lado deles. Se armazenarmos esses valores separadamente, o aplicativo deverá de alguma forma lembrar que eles pertencem um ao outro. Coloquemos esses “papéis” num envelope. Combinamos os campos em uma coleção e a chamamos de record.
Um registro pode conter qualquer combinação de campos. O aplicativo os reúne em um pacote, compacta-o e criptografa tudo usando a construção que acabamos de descrever. Valores de campo, nomes, tipos e tags são todos protegidos. Alterar o endereço do site, mesmo mantendo a mesma senha, cria uma nova versão de todo o registro, criptografada com um novo record_key.
Cadeia de hash
SEU PROJETO DEVE USAR UM BLOCKCHAIN? → NÃO
XKCD2267
Alguém que conheça secret_key pode preparar um registro criptografado para nosso cofre. Para evitar aceitá-lo como nosso, o aplicativo assina cada versão do registro com signing_key. Sem esta chave, alguém de fora não pode criar uma versão com uma assinatura que o servidor e outros dispositivos aceitem.
A versão criptografada precisa de algumas informações: a qual cofre e registro ela pertence, quando foi alterada e quais versões a precedem. Isso forma o cabeçalho público. Ele não contém a senha, mas ainda precisa de proteção: caso contrário, alguém poderá alterar a propriedade e os relacionamentos dos objetos sem tocar no texto cifrado. O aplicativo assina os bytes exatos do cabeçalho junto com o texto cifrado. Alterar os dados assinados invalida a assinatura.
O servidor lembra um armazém de pacotes lacrados. Ele vê o cabeçalho, o conteúdo criptografado e a assinatura, mas não a senha interna, e sabe que o destinatário existe na cidade. Ele verifica a assinatura na parte pública fixada de signing_key e decide se aceita a nova versão. Não precisa de senha mestra, secret_key nem de qualquer chave privada. Ao ler, o cliente verifica a mesma assinatura e, em seguida, verifica novamente a ligação entre o conteúdo e o cabeçalho durante a descriptografia, usando a tag de autenticação HPKE.
A assinatura também protege os links entre as versões. Um hash SHA-256 dos bytes exatos do cabeçalho e do texto cifrado se torna o identificador da versão. O cabeçalho da próxima versão contém o identificador da versão anterior. Quando alteramos a senha de um site, o aplicativo cria e assina um novo objeto com este link. O objeto antigo e sua assinatura permanecem inalterados. Substituir o antecessor preservando a assinatura da nova versão irá, portanto, falhar. Se dois dispositivos fizerem alterações independentes, ambas as ramificações sobreviverão; uma versão mesclada pode referir-se a ambos.
Lembra do blockchain? Aí está :-).
O cabeçalho público nos permite verificar os links sem descriptografá-los, mas algumas informações permanecem visíveis. O servidor conhece os identificadores, links de versão, o tempo de alteração fornecido pelo cliente, o número de campos e o tamanho codificado de cada valor de campo. A criptografia oculta o conteúdo do registro, não esta informação estrutural.
Sincronização
“Não é um vazamento de dados, é um backup distribuído gratuitamente.”
Memes da Internet.
Para funcionar em vários dispositivos, precisamos transferir o bloqueio e registrar o histórico entre eles. O formato do objeto não muda: transferimos os mesmos bytes e assinaturas criptografados que são armazenados localmente.
Para transferência manual, o aplicativo cria um arquivo ZIP contendo o bloqueio, todas as versões de registros criptografados armazenados localmente e uma lista de arquivos assinados. Nem a senha nem secret_key estão incluídos. Em outro dispositivo, o aplicativo primeiro abre o bloqueio, depois verifica as assinaturas, descriptografa os registros e restaura uma cópia do cofre local. A importação destina-se a um dispositivo onde este cofre ainda não existe; não mescla o histórico com uma cópia existente.
Um servidor de sincronização troca alterações entre cópias de trabalho. No primeiro upload, ele recebe o cadeado assinado, verifica a assinatura e calcula o endereço do cofre público da parte pública de signing_key. Em seguida, ele lembra essa chave e o identificador do cofre do cabeçalho: isso registra o cofre no servidor. As alterações subsequentes devem corresponder a este identificador e conter uma assinatura da mesma chave.
Um novo dispositivo precisa do endereço do cofre e de secret_key, então o aplicativo os combina em um código de transferência com uma soma de verificação. O usuário transfere este código para o dispositivo e insere a senha mestra separadamente; apenas o endereço público é enviado ao servidor.
Depois de abrir o bloqueio baixado, o aplicativo compara as partes públicas das chaves recuperadas com secret_key e o cabeçalho e verifica o endereço computado em relação ao código de transferência. Em seguida, ele salva a parte pública verificada de signing_key para verificar os registros.
Durante a sincronização, o aplicativo busca as versões ausentes e carrega suas alterações. O servidor verifica assinaturas e links para versões anteriores, armazena objetos aceitos e os passa para outros dispositivos. A verificação dos dados recebidos, a descriptografia e a resolução de conflitos acontecem nos dispositivos do usuário.
O registro não solicita nome, endereço de e-mail ou senha separada. O servidor vê o endereço público, a chave de verificação de assinatura, os cabeçalhos públicos, os tamanhos dos objetos e os links de versão. Ele também vê endereços IP e horários de solicitação. A senha mestra, secret_key, as chaves privadas e o conteúdo do registro de texto simples permanecem nos dispositivos do usuário. Qualquer pessoa que conheça o endereço público pode baixar os objetos criptografados, mas a leitura deles exige os segredos do usuário.
Essas verificações detectam objetos falsificados, mas não provam que o servidor mostrou o histórico completo e seu estado mais recente. Ele pode ocultar uma ramificação que o cliente nunca viu ou retornar um conjunto de dados mais antigo e consistente. Uma assinatura confirma a autenticidade de um objeto individual, não a integridade da resposta do servidor. O usuário pode notar alterações ausentes em outro dispositivo, mas um histórico antigo consistente passará na verificação hash-chain.
Alterando a senha mestra
Então, sua santidade gostaria de mudar a senha dela?
Hackers (1995)
A alteração da senha mestra criptografa novamente apenas o bloqueio; master_key por dentro permanece o mesmo. Os registros, suas assinaturas, secret_key e o endereço do cofre permanecem inalterados. O novo bloqueio é assinado com o mesmo signing_key.
Uma cópia de bloqueio antiga ainda abre com a senha antiga e a mesma secret_key; alterar a senha não revoga chaves que já foram expostas.
A imagem completa
O bloqueio protege master_key, message_key criptografa registros e signing_key cria assinaturas. ZIP arquivos e o servidor recebem objetos criptografados com assinaturas.