youhide@homelab: ~/blog

$cat same-store-same-keys.md

The same store, the same keys

A mesma store, as mesmas chaves

I have used pass for years. It is a hundred lines of shell around gpg and git, the store is a directory of .gpg files, and that design is why it is still here while a dozen password managers with venture funding are not. What I wanted was not a replacement for it. I wanted the parts it leaves to extensions, without giving up the store.

Eu uso o pass há anos. São umas cem linhas de shell em volta do gpg e do git, a store é um diretório de arquivos .gpg, e é esse design que explica por que ele continua aqui enquanto uma dúzia de gerenciadores de senha com capital de risco não está. O que eu queria não era um substituto pra ele. Eu queria as partes que ele deixa pras extensões, sem abrir mão da store.

So hidePass reads and writes the store you already have: the same ~/.password-store, the same gpg keys, the same git history. Rust, MIT, v0.2.0 tagged today.

Então o hidePass lê e escreve a store que você já tem: o mesmo ~/.password-store, as mesmas chaves gpg, o mesmo histórico git. Rust, MIT, v0.2.0 marcado hoje.

$ hidepass
Password Store
├── Email
│   └── work
└── Web
    └── github

$ hidepass -c Web/github
Copied Web/github to clipboard. Will clear in 45 seconds.

$ hidepass otp Web/github
482915

Compatible instead of rewritten

Compatível em vez de reescrito

The whole design rests on one decision: hidePass does not own the format. It writes .gpg files, honours .gpg-id and .gpg-id.sig, respects every PASSWORD_STORE_* variable, and makes the same git commits. pass itself keeps working on the same store, and so do the iOS and Android apps and the browser extensions. You can alias pass=hidepass and also not do that, and nothing cares either way.

O design inteiro se apoia numa decisão: o hidePass não é dono do formato. Ele escreve arquivos .gpg, respeita o .gpg-id e o .gpg-id.sig, respeita toda variável PASSWORD_STORE_*, e faz os mesmos commits git. O próprio pass continua funcionando na mesma store, e os apps de iOS e Android e as extensões de navegador também. Você pode dar alias pass=hidepass e também pode não dar, e nada se importa com a escolha.

Claiming compatibility is cheap, so it is a test rather than a sentence in a README. CI installs the real pass and the real pass-extension-otp on every push and runs a script in which each tool has to read what the other wrote. If I break the format, the build tells me — not somebody whose passwords are in there.

Afirmar compatibilidade é barato, então ela é um teste em vez de uma frase num README. O CI instala o pass de verdade e o pass-extension-otp de verdade a cada push e roda um script em que cada ferramenta tem que ler o que a outra escreveu. Se eu quebrar o formato, o build me avisa — e não alguém cujas senhas estão lá dentro.

A compatibility promise nothing re-runs is a compatibility promise you have already broken and not noticed.
Uma promessa de compatibilidade que nada re-executa é uma promessa de compatibilidade que você já quebrou e não percebeu.

Your clipboard manager is keeping your passwords

Seu gerenciador de clipboard está guardando suas senhas

This is the bug that annoyed me into writing the thing. Copy a secret on macOS and every clipboard manager on the machine — Maccy, Raycast, Alfred, Paste — writes it into its own searchable history, where it outlives the forty-five seconds you thought you were getting. The password manager cleared the pasteboard. The history did not.

Esse é o bug que me irritou o suficiente pra eu escrever a coisa. Copie um segredo no macOS e todo gerenciador de clipboard da máquina — Maccy, Raycast, Alfred, Paste — escreve o segredo no próprio histórico pesquisável, onde ele sobrevive aos quarenta e cinco segundos que você pensou que tinha. O gerenciador de senhas limpou o pasteboard. O histórico, não.

There is a convention for this — the concealed and transient pasteboard types from nspasteboard.org — and clipboard managers do honour it. hidePass marks every copied secret with both, so they decline to record it, and the previous clipboard contents come back after forty-five seconds instead of being replaced by nothing.

Existe uma convenção pra isso — os tipos de pasteboard concealed e transient do nspasteboard.org — e os gerenciadores de clipboard respeitam. O hidePass marca todo segredo copiado com os dois, então eles se recusam a registrar, e o conteúdo anterior do clipboard volta depois de quarenta e cinco segundos em vez de ser substituído por nada.

The things pass leaves to extensions

As coisas que o pass deixa pras extensões

One-time codes, passphrases, named fields, QR codes and a store audit are built in rather than installed. The first line of an entry is the password; everything after it is yours, key: value lines are readable with --field, and an otpauth:// URI is all hidepass otp needs:

Códigos de uso único, passphrases, campos nomeados, QR codes e uma auditoria da store são embutidos em vez de instalados. A primeira linha de uma entrada é a senha; tudo depois dela é seu, linhas key: value são legíveis com --field, e uma URI otpauth:// é tudo que o hidepass otp precisa:

correct horse battery staple
user: alice@example.com
url: https://example.com
otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example

It is the pass-otp format, deliberately, and HOTP is where that matters most. A HOTP code is only valid once, so reading one has to advance the counter: hidePass uses counter N+1, re-encrypts the entry with the new value and commits it, exactly as pass-otp does. Two tools can therefore share a HOTP entry without ever handing out the same code twice — which is the only version of "compatible" that means anything for a counter.

É o formato do pass-otp, de propósito, e o HOTP é onde isso mais importa. Um código HOTP só vale uma vez, então ler um tem que avançar o contador: o hidePass usa o contador N+1, re-encripta a entrada com o valor novo e faz o commit, exatamente como o pass-otp faz. Duas ferramentas podem então compartilhar uma entrada HOTP sem nunca entregar o mesmo código duas vezes — que é a única versão de "compatível" que significa algo para um contador.

Passphrases come from the EFF long wordlist, which has exactly 7776 words — five rolls of a six-sided die, and 12.925 bits per word. Six words is about 77 bits, which is the number I care about rather than how many symbols the thing contains:

As passphrases saem da lista longa de palavras da EFF, que tem exatamente 7776 palavras — cinco lançamentos de um dado de seis faces, e 12,925 bits por palavra. Seis palavras dão cerca de 77 bits, que é o número com que eu me importo, e não quantos símbolos a coisa tem:

$ hidepass generate --words 6 Email/work
cubicle-unfold-dripping-tribune-ample-reveal

Plaintext and the disk it should never touch

O texto puro e o disco em que ele não deveria encostar

Editing an entry means decrypting it somewhere, and "somewhere" must not be a file on a disk that keeps it after the editor exits. pass solves this with a ramdisk built by hdiutil — and on macOS 27 I watched hdiutil attach -nomount print a deprecation warning at me. hidePass uses diskutil image for edit instead.

Editar uma entrada significa descriptografar em algum lugar, e esse "algum lugar" não pode ser um arquivo num disco que o guarda depois que o editor fecha. O pass resolve isso com um ramdisk construído pelo hdiutil — e no macOS 27 eu vi o hdiutil attach -nomount cuspir um aviso de deprecação na minha cara. O hidePass usa o diskutil image para o edit.

The better answer, though, was to need the ramdisk less often. hidepass git does not use one at all, because git only ever sees encrypted files — there is no plaintext in that path to protect. And when a ramdisk genuinely cannot be had, hidePass asks before falling back to an ordinary temp directory rather than quietly deciding for you.

A resposta melhor, porém, foi precisar do ramdisk menos vezes. O hidepass git não usa nenhum, porque o git só vê arquivos encriptados — não existe texto puro nesse caminho pra proteger. E quando um ramdisk realmente não está disponível, o hidePass pergunta antes de cair num diretório temporário comum, em vez de decidir por você em silêncio.

Boring defaults, on purpose

Defaults chatos, de propósito

Four decisions that cost nothing to make and are miserable to retrofit:

Quatro decisões que não custam nada pra tomar e são miseráveis de colocar depois:

  • Entries are written atomically. A gpg interrupted halfway through cannot leave a truncated entry where a password used to be.
  • Recipients are validated before anything is re-encrypted. Re-encrypting to a key that turns out not to exist is how you lock yourself out of your own store.
  • Passwords come from the OS CSPRNG, without modulo bias. Taking a random byte modulo the alphabet length makes some characters likelier than others; it is a one-line bug and it quietly costs entropy.
  • Decrypted data is zeroed once it is no longer needed, rather than left in a freed allocation for whatever reads it next.
  • As entradas são escritas atomicamente. Um gpg interrompido no meio não consegue deixar uma entrada truncada onde antes havia uma senha.
  • Os recipients são validados antes de qualquer coisa ser re-encriptada. Re-encriptar para uma chave que acaba não existindo é assim que você se tranca fora da sua própria store.
  • As senhas vêm do CSPRNG do sistema, sem viés de módulo. Pegar um byte aleatório módulo o tamanho do alfabeto torna alguns caracteres mais prováveis que outros; é um bug de uma linha e custa entropia em silêncio.
  • Os dados descriptografados são zerados quando não são mais necessários, em vez de ficarem numa alocação liberada à espera de quem ler depois.

What it deliberately does not implement

O que ele deliberadamente não implementa

hidePass shells out to your gpg and your git. It does not link a crypto library and it does not speak the git protocol, which means gpg-agent, pinentry, smartcards and YubiKeys, SSH remotes and signed commits all work exactly as they already do — because they are exactly what they already were. The temptation to reimplement either one is the temptation to own a second, worse copy of a problem other people have been fixing for decades.

O hidePass chama o seu gpg e o seu git. Ele não linka uma biblioteca de criptografia e não fala o protocolo do git, o que significa que gpg-agent, pinentry, smartcards e YubiKeys, remotes SSH e commits assinados funcionam exatamente como já funcionavam — porque são exatamente o que já eram. A tentação de reimplementar qualquer um dos dois é a tentação de ser dono de uma segunda cópia, pior, de um problema que outras pessoas vêm consertando há décadas.

Three things do differ from pass, and they are in the README rather than in a footnote: pass extensions in .extensions are not run, git's commit summaries go to stderr so stdout carries only what you asked for, and listings show entries and folders rather than every file in the store.

Três coisas diferem do pass, e elas estão no README em vez de numa nota de rodapé: extensões do pass em .extensions não são executadas, os resumos de commit do git vão para o stderr de modo que o stdout carrega só o que você pediu, e as listagens mostram entradas e pastas em vez de todo arquivo da store.

Where it is now

Onde está agora

It is new — v0.2.0 is from today. Twelve modules, about 3,100 lines of Rust, 26 #[test] functions plus the end-to-end suite and the pass cross-check, and a minimum Rust of 1.88. The release pipeline builds macOS and Linux binaries and updates the formula, so installing is one line:

Ele é novo — o v0.2.0 é de hoje. Doze módulos, cerca de 3.100 linhas de Rust, 26 funções #[test] mais a suíte ponta a ponta e o cross-check com o pass, e um Rust mínimo de 1.88. O pipeline de release compila binários de macOS e Linux e atualiza a formula, então instalar é uma linha:

brew install youhide/youhide/hidepass

The source is at github.com/youhide/hidePass. Point it at a copy of your store before you point it at the real one — not because I expect it to eat anything, but because that is the advice I would want from someone whose password manager is two commits old.

O código está em github.com/youhide/hidePass. Aponte ele para uma cópia da sua store antes de apontar para a de verdade — não porque eu espere que ele coma alguma coisa, mas porque é o conselho que eu ia querer de alguém cujo gerenciador de senhas tem dois commits de idade.