Confidex
Seções

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).

public bytes Random bytes32 bytes master_key secret_key public bytes Random bytes32 bytes master_key secret_key
De bytes aleatórios às chaves de criptografia.

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.

public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification public bytes HKDF-SHA256keymaster_keyinfo"confidex/vault/ed25519/v1" signing_key Signature verification
Uma chave de assinatura separada, derivada da mesma chave mestra.

O que é um arquivo de bloqueio e o que ele faz?

Abra as portas do compartimento das cápsulas, HAL.
Sinto muito, Dave. Receio não poder fazer isso.

Uma Odisseia no Espaço

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.

public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket public bytes Fresh random bytes record_key secret_key Shared secret Derive message key message_key Encrypted passwordpacket
Um novo par de chaves para cada criptografia.

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.

Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision Header andparent IDs Encrypted recordpacket Sign revision signing_key Signed revision
O cabeçalho e o texto cifrado são assinados juntos.

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.

public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server public bytes public bytes public bytes master_key Fresh randombytes secret_key record_key Master password wrapping_key signing_key message_key lock Encrypted records Signed lockand records Signatureverification ZIP archive /sync server
As chaves, o que elas protegem e o que pode ser compartilhado.