Hoje estamos no Product Hunt! Deixar um comentário

O Authenticator 1.16.0 chegou. Ver as novidades

Como funciona

Uma visão técnica para quem avalia segurança. Tudo aqui pode ser conferido no código-fonte — inclusive as partes menos favoráveis, que declaramos em vez de omitir.

Arquitetura

A extensão é uma extensão do Chrome em Manifest V3 que gera códigos TOTP localmente com OTPAuth. Gerar códigos, guardar contas e cifrá-las não envolve backend nenhum, e não existem contas de usuário. Mantemos um único servidor, api.authenticator.sh, para o plano gratuito e o Pro — descrito abaixo —, e ele nunca recebe um segredo, um nome de conta, um emissor ou um código.

Esse servidor não guarda material de chave das contas de ninguém: não há custódia de chaves nem qualquer caminho de recuperação sob nosso controle. Se alguém esquece a senha e perde o código de recuperação, não temos como ajudar — é proposital.

Criptografia

A proteção por senha é opcional e vem desativada. Com ela ativada:

Cifra
AES-256-GCM, IV aleatório de 96 bits por registro e por reescrita
Derivação de chave
PBKDF2-HMAC-SHA256, 600.000 iterações, sal aleatório de 128 bits (orientação da OWASP)
Chave mestra
256 bits, de crypto.getRandomValues, gerada uma única vez; a senha apenas a encapsula
Impressões digitais
HMAC-SHA256 sob uma subchave derivada por HKDF, usada para mesclar registros entre dispositivos
Armazenamento da chave destravada
chrome.storage.session — só em memória, nunca gravada em disco, apagada ao fechar o navegador e inalcançável por content scripts
Bloqueio automático
A cada abertura / após 5, 15 ou 60 minutos de inatividade / até o navegador fechar
Implementação
Apenas WebCrypto. Nenhuma primitiva é feita à mão e nenhuma biblioteca de criptografia é embutida.

Por que dois níveis de chave

Os dados nunca são cifrados diretamente com uma chave derivada da senha. Uma chave mestra aleatória cifra os registros; a saída do PBKDF2 apenas encapsula essa chave mestra. Duas consequências decorrem daí, e as duas importam:

  • Trocar a senha reescreve 32 bytes em vez de recifrar cada registro, cada backup e cada exportação. É justamente nessa reescrita em massa que sistemas de armazenamento perdem dados.
  • Um código de recuperação de 160 bits pode encapsular a mesma chave mestra de forma independente, então uma senha esquecida tem conserto. O código é exibido uma vez, precisa ser digitado de volta para confirmação e é renovado a cada uso. Nada é gravado em disco até o usuário confirmá-lo.

O que é cifrado e o que não é

Com a proteção por senha ativada, todo o registro da conta é cifrado, exceto seu identificador e sua impressão digital — inclusive o nome do serviço, de modo que um perfil roubado não revela em quais serviços o usuário tem conta. Ao ativar, a extensão cifra os registros, verifica-os decifrando e comparando e só então remove todas as cópias em texto claro: armazenamento local do navegador, sincronização e os sete instantâneos rotativos de backup.

Dito sem rodeios, porque pesa na sua avaliação:

  • Com a proteção por senha desativada, as contas ficam armazenadas sem criptografia e, se o Chrome Sync estiver ligado, replicadas pela própria conta Google do usuário. Nunca chegam a nós, mas ficam com o Google. A sincronização pode ser desligada, o que também remove o que já estiver lá.
  • O histórico de uso por site — qual conta é usada em qual domínio — fica armazenado localmente e não é coberto pelo cofre enquanto está sendo coletado. Ativar a proteção por senha o apaga; ele também pode ser desligado separadamente.

Modelo de ameaças

Protege contra

  • Um diretório de perfil do navegador roubado ou copiado
  • Malware infostealer que exfiltra dados do navegador em massa
  • Acesso físico a uma máquina deixada sem supervisão
  • Registros de contas chegando ao Google via Chrome Sync
  • Um arquivo de backup perdido ou roubado (exportação protegida por senha)

Não protege contra

  • Malware rodando como o usuário enquanto o cofre está destravado
  • Um keylogger capturando a senha durante a digitação
  • Um navegador ou sistema operacional comprometido
  • O comprometimento dos serviços de terceiros a que os códigos pertencem
  • Uma exportação sem criptografia que você escolheu gravar — JSON simples, links otpauth:// ou CXF são legíveis por qualquer um que abra o arquivo

Em resumo: isto protege dados em repouso. Não defende, nem tem como defender, um dispositivo que já esteja sob controle de um atacante durante o uso.

Permissões

O manifesto declara cinco permissões e nenhuma permissão de host:

storage
Contas e configurações no armazenamento local, a chave destravada no de sessão e a cópia opcional de sincronização
activeTab
Lê o nome de host da aba ativa para destacar a conta correspondente e captura a aba quando o usuário escaneia um QR code da tela
contextMenus
Adiciona o item “Inserir código 2FA” ao menu de contexto, e apenas em campos editáveis de páginas http(s)
scripting
Injeta a única função que coloca o código no campo — para uma só aba e uma só invocação, apenas depois que esse item de menu é escolhido
sidePanel
Abre o app no painel lateral do Chrome quando essa é a superfície escolhida; não concede acesso a nenhuma página

Não há Não há content scripts, nem permissões tabs, cookies ou webRequest, nem web-accessible resources. Nada nosso existe em uma página até você pedir um código ali; o que roda então é injetado para aquela aba e aquela invocação, sob a autorização que o seu próprio clique concede, e não deixa nada para trás.

Câmera

A leitura de QR pela câmera roda em uma página da extensão aberta numa aba, não no popup — um popup é destruído ao perder o foco, que é exatamente o que um pedido de permissão faz. Para isso não é preciso nenhuma permissão no manifesto: usa-se o pedido de câmera padrão do navegador, concedido por origem da extensão e revogável nas configurações do site, e ele nunca aparece antes de o usuário abrir o leitor. O vídeo é decodificado localmente e nunca é transmitido nem armazenado.

Plano gratuito, Pro e o check-in diário

Desde a 1.14.0, a extensão faz um check-in com api.authenticator.sh uma vez por dia — quando o navegador inicia ou depois que o popup é renderizado, nunca no caminho até exibir um código. A requisição é uma lista fechada de campos, montada em uma única função em src/billing/sync.ts e mantida nessa lista por um teste:

iid
Um ID aleatório criado por esta instalação. Sem vínculo com uma conta Google, um endereço de e-mail ou o navegador — a menos que o usuário compre o Pro: aí o registro da compra vincula a instalação de onde ele comprou, e qualquer instalação em que a chave de licença esteja ativa, ao e-mail usado para pagar
ver, ui_lang, browser_lang
Versão da extensão, idioma da interface, idioma do navegador
installed_at, legacy, legacy_src
Quando a instalação surgiu e se ela é anterior à cobrança
accounts, groups
Quantas contas ela guarda e em quantos grupos — números, nunca quais contas nem os nomes dos grupos
vault, sync, open_mode, view_mode, style, dark, os
Se a proteção por senha e a sincronização estão ativadas, e como a extensão abre, como a lista de contas é exibida (normal, compacta ou com os códigos ocultos), e qual estilo usa, claro ou escuro, e o sistema operacional (Windows, macOS, Linux, ChromeOS)
pid, lid, has_key, key_hint
Qual política assinada e qual licença a instalação tem no momento, se ela já tem a chave de licença e os últimos quatro caracteres dessa chave
opens
Quantas vezes a extensão foi aberta em cada dia desde a última comunicação (no máximo os últimos 14 dias) — uma contagem por dia, nada sobre o que foi visto
events
Eventos de cobrança enfileirados, de uma lista fechada: o limite foi atingido, o paywall foi exibido ou fechado, um plano foi escolhido, houve uma tentativa de checkout ou de ativar uma chave

Nunca enviados: segredos, nomes de contas, emissores, códigos ou os sites em que eles são usados. O servidor obtém o país a partir da requisição, na borda, e não armazena o endereço IP. A página de desinstalação recebe o mesmo ID de instalação como ?i=: ela informa ao servidor que a instalação foi removida e vincula a ela uma resposta opcional à pesquisa.

Guardado em chrome.storage.local para isso: o ID de instalação, a última política e a última licença assinadas, a chave de licença que esta instalação tem (digitada, ou recebida depois de uma compra feita nela), as contagens diárias de aberturas até o servidor recebê-las e os eventos na fila — nenhum dado de conta. Com a sincronização ativada, a informação de que a instalação é anterior à cobrança e essa chave de licença também vão para o Chrome Sync, para que os outros perfis do Chrome do usuário as recebam e ativem o Pro sozinhos; desativar a sincronização remove as duas do Chrome Sync, e reativá-la as coloca de volta.

O que volta e como a extensão trata isso:

Tokens
A política (o limite de contas do plano gratuito) e a licença (Pro) são JWS assinados com ES256 (ECDSA P-256)
Verificação
WebCrypto, com as chaves públicas compiladas na build (src/billing/keys.ts). Uma build da Chrome Web Store confia apenas na chave de produção, e a checagem da build recusa uma que traga qualquer outra
Offline
O limite e a licença são verificados localmente, a partir dos últimos tokens assinados. Até que uma política assinada seja recebida, nada é limitado — um servidor fora do ar nunca consegue bloquear ninguém
O que o limite pode fazer
Impedir que uma conta seja adicionada, e nada mais. Ele não pode ocultar, bloquear, remover nem alterar uma conta, nem bloquear códigos, exportação, backups, restauração ou sincronização
Compras
Stripe Checkout em uma nova aba, com o Link como vendedor responsável (merchant of record). O e-mail do comprador chega até nós pela Stripe, para emitir e recuperar a chave de licença; as chaves ficam guardadas como hash, mais uma cópia cifrada para que uma chave perdida possa ser enviada de novo
Chave de licença
A resposta da comunicação diária traz a própria chave só para a instalação em que o Pro foi comprado, e só até os quatro caracteres que ela informa em key_hint baterem com essa chave — assim, uma chave comprada depois substitui uma anterior; uma instalação em que a chave foi digitada nunca recebe uma

O limite não é DRM: o código é aberto, e um fork pode removê-lo. O que nunca pode ser possível é alguém obter ou usar a licença de outra pessoa, ou fazer com que a instalação de outra pessoa fique limitada — isso é uma vulnerabilidade e está no escopo da nossa política de segurança.

Tráfego de rede de saída

Dois tipos de requisição acontecem por conta própria: a verificação do relógio e o check-in diário de cobrança. Todo o resto abaixo acontece porque o usuário clicou em algo. O Manifest V3 proíbe código remoto, e nada aqui leva contas, chaves secretas ou códigos gerados. A lista completa:

HostQuandoLeva dados do usuário?
time.akamai.com
timeapi.io
cloudflare.com
Verificação de desvio do relógio, em cache; se nenhuma fonte responder, as Configurações avisam. Com o relógio desajustado o TOTP quebra.Não
authenticator.shPágina de boas-vindas na instalação; página de feedback na desinstalação, com o ID de instalação no endereço; páginas de ajuda e suporte quando abertas pelo popup.O ID de instalação, só na página de desinstalação; fora isso, não (logs comuns de servidor web)
api.authenticator.shCheck-in de cobrança uma vez por dia; chamadas de checkout, licença e portal quando o usuário compra o Pro, digita uma chave, restaura uma compra ou gerencia uma assinatura.Os campos listados acima — nunca dados de conta. Uma chave de licença ou um endereço de e-mail só quando o usuário digita um
checkout.stripe.com
billing.stripe.com
Stripe Checkout e portal do cliente, abertos em uma aba quando o usuário compra o Pro ou gerencia uma assinatura.O que o comprador digitar lá. Valem os termos da própria Stripe e do Link
chromewebstore.google.comPágina da loja, aberta em uma aba ao clicar: o formulário de avaliação vindo do aviso de avaliação.Não (o Google vê a visita a uma página da loja como qualquer outra)

Nenhuma fonte, script, folha de estilo ou imagem é carregada de hosts de terceiros. Tudo o que a interface precisa para ser renderizada vem dentro do pacote.

Como verificar o que publicamos

Nada do que está acima precisa ser aceito como fé. A extensão é de código aberto e cada versão é reproduzível:

  1. Baixe o .crx publicado na Chrome Web Store e descompacte-o
  2. Faça checkout da tag git correspondente
  3. Rode npm ci && npm run build no Node 20 LTS
  4. Compare o dist/ resultante com o pacote descompactado

A cada release no GitHub publicamos um SHA-256 de cada arquivo do dist/ produzido, como SHA256SUMS-v<version>.txt e no formato sha256sum, de modo que o passo 4 vira uma única execução de sha256sum -c. As diferenças devem se limitar à ordem dos arquivos dentro dos arquivos compactados e a espaços na saída minificada entre versões de patch do Node.

As dependências de execução são propositalmente poucas — OTPAuth, React, jsQR, Zustand, Framer Motion e Lucide — fixadas por um lockfile versionado. Um SBOM CycloneDX está disponível mediante solicitação.

Ver o código-fonte

O que não temos

Somos um fornecedor pequeno e independente. Se algum dos itens abaixo for requisito obrigatório no seu processo, é melhor saber agora do que três semanas depois de iniciada a avaliação:

  • Sem certificação SOC 2, ISO 27001 ou equivalente
  • Nenhum teste de intrusão externo contratado até hoje
  • Sem programa pago de bug bounty

O que oferecemos em troca é transparência total do código, uma build verificável, uma superfície mínima de permissões e um servidor que, para começo de conversa, nunca recebe seus segredos, nomes de contas ou códigos.

Como falar conosco

Relatos de vulnerabilidade e dúvidas de segurança: security@authenticator.sh. Nossa política de divulgação, escopo, prazos de resposta e termos de safe harbour estão na página de política de segurança; o que é armazenado e o que sai do dispositivo está descrito na política de privacidade.

Temos prazer em responder a um questionário escrito de avaliação de fornecedores ou percorrer o código junto com uma equipe de segurança.

Para qualquer assunto que não seja de segurança — instalação, recuperação de conta, dúvidas de cobrança em uma implantação — veja o suporte. Por favor, não envie relatos de vulnerabilidade para lá: aquela caixa não é tratada como confidencial.