Como pode um servidor armazenar e sincronizar os nossos segredos sem ter as chaves para os ler? Seguiremos a forma como Confidex cria as suas chaves, protege-as num cadeado e utiliza-as para encriptar dados e verificar alterações.
Cadeado
Só o Escolhido pode abrir a porta
Matrix Reloaded
Imagine que estamos a construir um vault virtual para armazenar dados secretos. Deve manter esses dados num portátil após o fecho da aplicação e permitir que os abramos noutro dispositivo. O cofre precisará das suas próprias chaves. Devemos armazená-los de modo a que a cópia dos ficheiros não permita que alguém de fora possa ler ou alterar o seu conteúdo.
Um lock protege as chaves: um pequeno ficheiro encriptado. Abri-lo requer uma palavra-passe mestra e um código secreto, secret_key; nenhum deles está no próprio ficheiro. Podíamos imprimir o cadeado inteiro no jornal da manhã. Os leitores receberiam uma cópia, mas a sua publicação não revelaria as chaves privadas.
Suponha que alguém de fora obtém esse ficheiro e até aprende a palavra-passe mestra. Podem agora repetir todos os cálculos que apenas necessitam da palavra-passe, mas estes cálculos não produzirão a chave que abre a fechadura: secret_key é também uma entrada. O ficheiro não contém nenhuma verificação separada apenas da palavra-passe. Abri-lo ainda requer o código secreto desconhecido.
Este código provém de dados criptograficamente aleatórios; uma pessoa não inventa isso. Enquanto secret_key for desconhecido, mesmo uma palavra-passe mestra adivinhada corretamente não abre a fechadura: o atacante ainda não possui um segredo aleatório. Portanto, uma palavra-passe fraca por si só não transforma a cópia do servidor num prático ataque de dicionário.
Se o atacante também obtiver secret_key, poderá testar as tentativas de palavra-passe mestra offline. Isto é dispendioso, mas a eventual dificuldade depende da força da palavra-passe.
Chaves
Senhores, o que fazem as chaves? Chaves… desbloqueiam coisas?
Piratas das Caraíbas – O Baú da Morte
A aplicação obtém 32 bytes aleatórios do gerador criptográfico do sistema operativo e prepara-os para utilização como chave. Estes bytes preparados são a chave mestra privada, master_key.
A aplicação deriva sempre as mesmas chaves de master_key: repetir os mesmos cálculos nos mesmos bytes dá o mesmo resultado. Um computador portátil e um telefone podem, portanto, recuperar chaves idênticas. Para obter master_key num novo dispositivo, a aplicação deve primeiro abrir o cadeado com a palavra-passe mestra e secret_key.
O primeiro par: encriptação
Eu sou o mestre das chaves.
Caça-fantasmas
Para armazenar dados encriptados, a aplicação calcula uma chave pública de master_key: secret_key. Pode encriptar dados para o cofre, enquanto a desencriptação requer master_key.
“Público” descreve o papel desta chave no algoritmo, e não uma obrigação de a fornecer a todos. Em Confidex fica com o utilizador. É também o segundo segredo necessário para abrir o bloqueio, pelo que permanece local e não é enviado pela rede. Por si só, o secret_key permite que alguém encripte dados para o cofre, mas não os desencripta nem assina alterações no seu nome.
X25519 é o algoritmo que recebe master_key e calcula secret_key. Recuperar master_key de secret_key é praticamente inviável. Um ataque clássico a um supercomputador imaginário que executasse mil milhões de milhares de milhões de operações de curva por segundo levaria cerca de 2,3 × 10¹² anos em média (estimativa do SafeCurves).
O segundo par: assinaturas
A única coisa original é a assinatura
Apanhe-me se puder
O primeiro par ajuda a ocultar o conteúdo. Precisamos agora 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, pelo que precisamos de enviar uma chave de verificação pública. A parte pública do primeiro par é secret_key, que o servidor não deve receber. Assim, criamos um segundo par de chaves permanente para as assinaturas.
Em vez de armazenar outro segredo independente, derivamos uma chave de assinatura privada de master_key e chamamos-lhe signing_key. A aplicação utiliza-o para assinar dados e o bloqueio. Outros dispositivos e o servidor utilizam a sua parte pública para verificar as assinaturas sem ler o conteúdo.
Podemos recuperar este par a partir do mesmo master_key. A aplicação deriva dele um valor inicial: 32 bytes que determinam exclusivamente a chave de assinatura.
Para derivar diferentes chaves para diferentes trabalhos, incluímos um rótulo fixo no cálculo: aqui, confidex/vault/ed25519/v1. O mesmo master_key e o mesmo rótulo produzem sempre a mesma chave.
A palavra-passe mestra não inseriu esses cálculos. Ambos os pares dependem dos bytes aleatórios de master_key, e não de uma string inventada por uma pessoa. Esta distinção será importante mais tarde: a palavra-passe protegerá o acesso às chaves que já existem, para que possamos alterá-la sem alterar as próprias chaves.
A derivação utiliza uma função de derivação de chaves 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, pelo que podemos enviá-lo para o servidor para verificação de assinatura.
O que é um ficheiro de bloqueio e o que faz?
Abre as portas do compartimento das cápsulas, HAL.
Uma Odisseia no Espaço
Peço desculpa, Dave. Receio não o poder fazer.
O bloqueio armazena um master_key encriptado. Abri-lo requer a palavra-passe 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 palavras-passe. A versão do formato interno é encriptada juntamente com master_key.
Ao criar o bloqueio, a aplicação já tem master_key, pelo que calcula secret_key antes da encriptação. Ao abri-lo, o utilizador fornece secret_key separadamente: está ausente da cópia pública do cadeado.
Após a desencriptação, a aplicação calcula a parte pública de master_key utilizando X25519 e exige que corresponda a secret_key. De seguida, recupera signing_key e compara a sua parte pública com a chave no cabeçalho. Desta forma, a fechadura verifica-se a si própria.
Para atrasar a adivinhação da palavra-passe mestra, cada tentativa deve custar tempo e memória. Confidex utiliza Argon2id com 64 MiB de memória e três passagens. O processamento de palavras-passe também utiliza um sal aleatório. O salt não é segredo: faz com que a mesma palavra-passe produza resultados diferentes em fechaduras diferentes.
Se secret_key já for conhecido, o comprimento da palavra-passe 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 mil milhões de 7 e cerca de 9,5 × 10¹⁶ 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 demorasse em média um dia numa determinada máquina, 5 demoraria cerca de dois minutos e 12 cerca de 33.000 anos.
Para obter uma chave do resultado Argon2id e secret_key, utilizamos a função HKDF-SHA256 introduzida anteriormente, com o rótulo confidex/lock/wrap/v1. O resultado é wrapping_key, a chave que encripta a fechadura.
Devemos esconder o master_key e protegê-lo contra as adulterações. XChaCha20-Poly1305 encripta-o com wrapping_key e adiciona uma etiqueta de autenticação. A etiqueta liga o texto cifrado ao cabeçalho público: uma palavra-passe errada ou uma alteração nos bytes protegidos faz com que a abertura falhe. A etiqueta e um valor aleatório de utilização única, o nonce, são armazenados com o texto cifrado.
Cada vez que o bloqueio é criado ou encriptado novamente, a aplicação obtém um novo salt e nonce. O conteúdo encriptado inclui um byte de versão e os 32 bytes de master_key; um nonce de 24 bytes e uma etiqueta de 16 bytes são armazenados juntamente 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 detetada mais cedo, quando a etiqueta é verificada.
O servidor não possui os segredos necessários para verificar esta etiqueta. Para transferir o bloqueio, a aplicação adiciona uma assinatura feita com signing_key. Abrange todo o ficheiro finalizado, incluindo o cabeçalho público e o conteúdo encriptado. O servidor verifica a assinatura e, no primeiro upload, fixa toda a parte pública de signing_key e o identificador interno do cofre. Verifica as alterações subsequentes em relação a esta chave. O bloqueio e os dados encriptados são assinados com o mesmo signing_key, recuperado de master_key.
O cofre
Agora podemos publicar o bloqueio. Apenas alguém que conheça secret_key e master password pode ler master_key, enquanto qualquer pessoa pode obter a chave de assinatura pública do ficheiro de bloqueio e verificar os dados assinados sem ver o conteúdo do bloqueio ou dos dados.
Considere uma palavra-passe, SuperPa55worD. Por enquanto, queremos apenas guardá-lo: depois de fechar a aplicação, poderemos enviar o ficheiro de bloqueio e a palavra-passe encriptada para qualquer lugar sem que ninguém veja SuperPa55worD, ao mesmo tempo que lhes permitiremos verificar a nossa autoria. A encriptação deve ser reversível quando temos secret_key e master password. Uma cifra simétrica pode encriptar e desencriptar com a mesma chave. Como não armazenamos esta chave, devemos conseguir recuperá-la usando o cadeado. Vamos chamar-lhe message_key.
Obteremos um message_key separado para cada nova encriptação. Falta-nos ainda providenciar a 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, a aplicação cria um par X25519 temporário apenas para esta encriptação: record_key. De seguida, pega na parte privada de record_key e na chave pública permanente do cofre, secret_key, e calcula um segredo partilhado. Uma função separada deriva message_key deste segredo; esta chave encripta a nossa senha.
Ao ler, a aplicação segue outro caminho. Utiliza master_key e a parte pública de record_key. Estas duas chaves produzem o mesmo segredo partilhado. Matematicamente, ambas as rotas multiplicam o mesmo ponto inicial da curva por dois números privados, por ordem diferente. A ordem não altera o resultado. Podemos, portanto, recuperar a chave de desencriptação 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.
Podemos agora armazenar o texto cifrado juntamente com a parte pública de record_key. A parte privada de record_key e o utilizado message_key já não são necessários para leituras futuras. Posteriormente, a aplicação recupera a chave de desencriptação de master_key e a parte pública armazenada de record_key. Um segredo de cofre permanente pode, portanto, desencriptar muitos valores encriptados separadamente, cada um com o seu próprio message_key recuperado quando necessário.
Não basta ocultar o texto: é também necessário detetar adulterações ao lê-lo. A encriptação adiciona, portanto, uma etiqueta de autenticação, um valor curto verificado juntamente com os dados encriptados. Se a verificação falhar, a aplicação rejeitará o resultado. Não deve apresentar texto aleatório ou alterado como uma palavra-passe desencriptada com sucesso.
Descrevemos uma construção em que um par assimétrico ajuda a estabelecer um segredo partilhado, enquanto message_key encripta o conteúdo. Este é chamado Hybrid Public Key Encryption ou HPKE e está especificado em RFC 9180. Confidex utiliza X25519 para concordar com o segredo, HKDF-SHA256 para derivar chaves e ChaCha20-Poly1305 para encriptação com verificação de integridade. Aqui, HKDF recebe o segredo partilhado acordado usando record_key e o par master_key; este é um trabalho separado de derivar o Ed25519 seed. RFC 5869 descreve HKDF.
Para a encriptação seguinte, a aplicação 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 depois o contexto é descartado.
O próprio ChaCha20 utiliza um message_key de 256 bits. Para ter uma ideia de um ataque direto, imagine um supercomputador fictício a testar mil milhões de milhares de milhões de candidatos por segundo. Pesquisar as 2²⁵⁶ chaves possíveis demoraria cerca de 1,8 × 10⁵¹ anos. RFC 8439 descreve o algoritmo e a sua análise.
Um recorde
Estamos felizes?
Ficção Pulp
Guardamos a palavra-passe, mas o login num site geralmente também requer um nome de utilizador e um endereço. Por vezes precisamos de uma nota ao lado deles. Se armazenarmos estes valores separadamente, a aplicação deverá de alguma forma lembrar-se que eles pertencem um ao outro. Coloquemos estes “papéis” num envelope. Combinamos os campos numa coleção e chamamos-lhe record.
Um registo pode conter qualquer combinação de campos. A aplicação reúne-os num pacote, compacta-o e encripta tudo usando a construção que acabamos de descrever. Os valores de campo, nomes, tipos e tags são todos protegidos. A alteração do endereço do site, mesmo mantendo a mesma palavra-passe, cria uma nova versão de todo o registo, encriptada com um novo record_key.
Cadeia de hash
O SEU PROJETO DEVE UTILIZAR UM BLOCKCHAIN? → NÃO
XKCD2267
Alguém que conheça secret_key pode preparar um registo encriptado para o nosso cofre. Para evitar aceitá-lo como nosso, a aplicação assina cada versão do registo 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 encriptada necessita de algumas informações: a que cofre e registo pertence, quando foi alterada e quais as versões que a precedem. Este forma o cabeçalho público. Não contém a palavra-passe, mas ainda precisa de proteção: caso contrário, alguém poderá alterar a propriedade e as relações dos objetos sem tocar no texto cifrado. A aplicação assina os bytes exatos do cabeçalho juntamente com o texto cifrado. A alteração dos dados assinados invalida a assinatura.
O servidor assemelha-se a um armazém de pacotes selados. Vê o cabeçalho, o conteúdo encriptado e a assinatura, mas não a palavra-passe interna, e sabe que o destinatário existe na cidade. Verifica a assinatura na parte pública fixada de signing_key e decide se aceita a nova versão. Não necessita 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 desencriptação, utilizando a etiqueta de autenticação HPKE.
A assinatura também protege as ligações entre versões. Um hash SHA-256 dos bytes exatos do cabeçalho e do texto cifrado torna-se o identificador da versão. O cabeçalho da versão seguinte contém o identificador da versão anterior. Quando alteramos a palavra-passe de um site, a aplicação cria e assina um novo objeto com este link. O objeto antigo e a 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 fundida pode referir-se a ambos.
Lembra-se do blockchain? Aí está :-).
O cabeçalho público permite-nos verificar os links sem os desencriptar, mas algumas informações permanecem visíveis. O servidor conhece os identificadores, as ligações 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 encriptação oculta o conteúdo do registo, não esta informação estrutural.
Sincronização
“Não é uma fuga de dados, é um backup distribuído gratuitamente.”
Memes da Internet.
Para funcionar em vários dispositivos, precisamos de transferir o bloqueio e registar o histórico entre eles. O formato do objeto não se altera: transferimos os mesmos bytes e assinaturas encriptadas que são armazenados localmente.
Para transferência manual, a aplicação cria um ficheiro ZIP contendo o bloqueio, todas as versões de registos encriptados armazenados localmente e uma lista de ficheiros assinados. Nem a palavra-passe nem secret_key estão incluídos. Noutro dispositivo, a aplicação abre primeiro o bloqueio, depois verifica as assinaturas, desencripta os registos 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 funde o histórico com uma cópia existente.
Um servidor de sincronização troca alterações entre cópias de trabalho. No primeiro upload, 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, recorda essa chave e o identificador do cofre do cabeçalho: isto regista 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, pelo que a aplicação os combina num código de transferência com uma soma de verificação. O utilizador transfere este código para o dispositivo e introduz a palavra-passe mestra separadamente; apenas o endereço público é enviado para o servidor.
Depois de abrir o bloqueio descarregado, a aplicação 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, guarda a parte pública verificada de signing_key para verificar os registos.
Durante a sincronização, a aplicação procura as versões em falta e carrega as suas alterações. O servidor verifica as assinaturas e as ligações para versões anteriores, armazena os objetos aceites e passa-os para outros dispositivos. A verificação dos dados recebidos, a desencriptação e a resolução de conflitos acontecem nos dispositivos do utilizador.
O registo não solicita nome, endereço de e-mail ou palavra-passe 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 as ligações de versão. Também vê endereços IP e horários de solicitação. A palavra-passe mestra, secret_key, as chaves privadas e o conteúdo do registo de texto simples permanecem nos dispositivos do utilizador. Qualquer pessoa que conheça o endereço público pode descarregar os objetos encriptados, mas a sua leitura exige os segredos do utilizador.
Estas verificações detetam objetos falsificados, mas não provam que o servidor mostrou o histórico completo e o seu estado mais recente. Pode ocultar uma ramificação que o cliente nunca viu ou devolver um conjunto de dados mais antigo e consistente. Uma assinatura confirma a autenticidade de um objeto individual, e não a integridade da resposta do servidor. O utilizador pode notar alterações em falta noutro dispositivo, mas um histórico antigo consistente passará a verificação hash-chain.
Alterando a palavra-passe mestra
Então, a vossa santidade gostaria de lhe mudar a palavra-passe?
Hackers (1995)
A alteração da palavra-passe mestra encripta novamente apenas o bloqueio; master_key por dentro permanece o mesmo. Os registos, as 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 password antiga e a mesma secret_key; alterar a palavra-passe não revoga chaves que já foram expostas.
A imagem completa
O bloqueio protege master_key, message_key encripta registos e signing_key cria assinaturas. ZIP ficheiros e o servidor recebem objetos encriptados com assinaturas.