We're on Product Hunt today! Leave a comment
A technical overview for security reviewers. Everything here is verifiable against the source — including the parts that are less flattering, which we state rather than omit.
The extension is a Manifest V3 Chrome extension that generates TOTP codes locally with OTPAuth. Generating codes, storing accounts and encrypting them involve no backend, and there are no user accounts. We run one server, api.authenticator.sh, for the free plan and Pro — described below — and it never receives a secret, an account name, an issuer or a code.
That server holds no key material for anyone's accounts: there is no key escrow and no recovery path controlled by us. If a user forgets their password and loses their recovery code, we cannot help them — by design.
Password protection is optional and off by default. With it on:
Data is never encrypted with a password-derived key directly. A random master key encrypts the records; PBKDF2 output only wraps that master key. Two consequences follow, and both matter:
With password protection on, the entire account record is encrypted except its identifier and fingerprint — including the service name, so a stolen profile does not reveal which services the user holds accounts with. Enabling it encrypts the records, verifies them by decrypting and comparing, and only then removes every cleartext copy: browser local storage, sync, and all seven rolling backup snapshots.
Stated plainly, because it matters for your assessment:
In short: this protects data at rest. It does not, and cannot, defend a device that is already under an attacker's control while in use.
The manifest declares five permissions and no host permissions:
There are no content scripts, no tabs, cookies or webRequest permissions, and no web-accessible resources. Nothing of ours exists in a page until you ask for a code there; what runs then is injected for that one tab and that one invocation, under the grant your own click provides, and it leaves nothing behind.
QR scanning by camera runs on an extension page opened in a tab, not in the popup — a popup is destroyed when it loses focus, which a permission prompt does. This needs no manifest permission: it uses the browser's standard camera prompt, granted per extension origin and revocable in site settings, and it is never requested until the user opens the scanner. Video is decoded locally and never transmitted or stored.
Since 1.14.0 the extension checks in with api.authenticator.sh once a day — when the browser starts, or after the popup has rendered, never on the way to showing a code. The request is a closed list of fields, built in one function in src/billing/sync.ts and held to that list by a test:
Never sent: secrets, account names, issuers, codes, or the sites they are used on. The server takes the country from the request at its edge and does not store the IP address. The uninstall page receives the same install id as ?i=: it tells the server the installation was removed, and links an optional survey answer to it.
Kept in chrome.storage.local for this: the install id, the last signed policy and license, the license key this installation holds (typed in, or received after buying on it), the daily open counts until the server has them, and the queued events — no account data. While sync is on, whether the installation predates billing and that license key also go into Chrome Sync, so the user's other Chrome profiles pick them up and turn Pro on by themselves; turning sync off removes both from Chrome Sync, and turning it back on puts them back.
What comes back, and how the extension treats it:
The limit is not DRM: the code is open source, and a fork can remove it. What must never be possible is for one person to obtain or use another person's license, or to make someone else's installation limited — that is a vulnerability, and in scope of our security policy.
Two kinds of request happen on their own: the clock check and the daily billing check-in. Everything else below happens because the user clicked something. Manifest V3 forbids remote code, and nothing here carries accounts, secret keys or generated codes. In full:
| Host | When | Carries user data? |
|---|---|---|
| time.akamai.com timeapi.io cloudflare.com | Clock-drift check, cached; when none of the sources can be reached, Settings says so. TOTP breaks on a drifted clock. | No |
| authenticator.sh | Welcome page on install; feedback page on uninstall, with the install id in its address; help and support pages when opened from the popup. | The install id, on the uninstall page only; otherwise no (ordinary web server logs) |
| api.authenticator.sh | Billing check-in once a day; checkout, license and portal calls when the user buys Pro, enters a key, restores a purchase or manages a subscription. | The fields listed above — never account data. A license key or an email address only when the user types one in |
| checkout.stripe.com billing.stripe.com | Stripe Checkout and customer portal, opened in a tab when the user buys Pro or manages a subscription. | What the buyer enters there. Stripe's and Link's own terms apply |
| chromewebstore.google.com | Store listing, opened in a tab on click: the review form from the rating prompt, our password manager from the cross-promo banner. | No (Google sees a store page visit like any other) |
| authenticator.featurebase.app | Public feature board, opened in a tab by the feature-request link in the support footer. | Only what the user posts there. Third-party service, its own terms. |
No fonts, scripts, styles or images load from third-party hosts. Everything needed to render the interface ships inside the package.
You do not have to take any of the above on trust. The extension is open source, and each release is reproducible:
.crx from the Chrome Web Store and unzip itnpm ci && npm run build on Node 20 LTSdist/ with the unzipped packageA SHA-256 for every file in the produced dist/ is published with each GitHub release as SHA256SUMS-v<version>.txt, in sha256sum format, so step 4 can be a single sha256sum -c run. Differences should be limited to file ordering inside archives and whitespace in minified output across Node patch versions.
Runtime dependencies are deliberately few — OTPAuth, React, jsQR, Zustand, Framer Motion and Lucide — pinned by a committed lockfile. A CycloneDX SBOM is available on request.
Read the sourceWe are a small independent vendor. If any of the following is a hard requirement for your process, it is better that you know now than three weeks into an evaluation:
What we offer instead is full source transparency, a verifiable build, a minimal permission surface, and a server that is never sent your secrets, account names or codes in the first place.
Vulnerability reports and security questions: security@authenticator.sh. Our disclosure policy, scope, response times and safe-harbour terms are on the security policy page; what is stored and what leaves the device is set out in the privacy policy.
We are happy to answer a written vendor assessment questionnaire or walk a security team through the code.
For anything that is not a security matter — installation, account recovery, billing questions from a deployment — see support. Please do not send vulnerability reports there; that inbox is not treated as confidential.