Website: https://otpeer.com
OTPeer is an open source two-factor authenticator you fully own: your codes live in an encrypted local vault and sync peer-to-peer between your own devices — no cloud, no account, no telemetry. Import once from Google/Microsoft/Facebook Authenticator (or Aegis, 2FAS, andOTP) and read live codes from your terminal today; desktop and mobile apps are on the roadmap.
The CLI ships on npm as
authenticator-clui
(its original name, kept for its install base); the product family, and the
future desktop/mobile apps ("OTPeer Authenticator" in the stores), carry
the OTPeer brand — see the
branding decision.
👉 End users: download Desktop or install the CLI from otpeer.com. CLI details also live on the package README and npm. This page is about the project as a whole: architecture, building from source, and contributing.
- Vision
- Repository structure
- Architecture
- Building from source
- Desktop releases
- Running the CLI from a checkout
- Testing
- Publishing
- Roadmap
- Contributing
- Security
- License
Today this repo ships one product: the authenticator-clui npm package.
The goal is one authenticator, three surfaces, all sharing the same vault
logic:
| Surface | Status | Distribution |
|---|---|---|
| CLI (Node.js) | ✅ published | npm |
| Desktop "OTPeer Authenticator" (Electron, Mac/Ubuntu/Windows) | ✅ app ready | otpeer.com (GitHub Releases for artifacts; Homebrew/Flathub/winget later) |
| Mobile "OTPeer Authenticator" (React Native, iOS/Android) | 🔜 planned | App Store / Play Store / F-Droid |
Devices will sync peer-to-peer over the local network, QR-paired — no backend server, no account, and no permissions beyond the camera and iOS's local-network prompt. Each device keeps its own encrypted vault; sync is an explicit, local, device-to-device merge.
This is an npm-workspaces monorepo. The root is private and never published — only individual packages ship to users, each through its own channel.
otpeer-authenticator/
├── package.json workspace root (private — publishes nothing)
├── packages/
│ ├── core/ packages/core — shared engine (TypeScript)
│ │ └── src/
│ │ ├── accounts.ts vault load/save orchestration
│ │ ├── totp.ts TOTP code generation, window timing
│ │ ├── edbase32.ts RFC 3548 base32 encoding
│ │ ├── importers/ Google Authenticator export decoding
│ │ ├── adapters/ StorageAdapter / CryptoProvider interfaces
│ │ └── node/ Node.js implementations of the adapters
│ ├── cli/ authenticator-clui — the published npm package
│ │ ├── bin.js command-line entry (`authenticator`, `auth`)
│ │ ├── core.js wires the vendored core to CLI storage paths
│ │ ├── src/ terminal-only code (password prompt, table render)
│ │ └── vendor/core/ build-generated copy of core (gitignored)
│ └── desktop/ "OTPeer Authenticator" — Electron + React app
│ ├── electron/ main (fixed window + tray), preload, vault-service
│ ├── build/ OTPeer app / tray icons (icns, ico, png)
│ ├── src/renderer/ React UI (hamburger IA, accounts, sync)
│ └── vendor/core/ build-generated copy of core (gitignored)
├── website/ otpeer.com landing page (Vite → GitHub Pages)
├── docs/plan/ in-depth design docs, one per roadmap stage
└── readme_assets/ images used by the READMEs
The design rule that everything else follows from: packages/core
contains all logic that must behave identically on every platform, and it
never touches a platform API directly. It talks to the outside world only
through two injected interfaces:
interface StorageAdapter { // where the vault blob lives
read(): Promise<string | null>;
write(data: string): Promise<void>;
delete(): Promise<void>;
exists(): Promise<boolean>;
}
interface CryptoProvider { // how the vault is encrypted
encrypt(plaintext: string, password: string): string;
decrypt(ciphertext: string, password: string): string;
}Each client supplies its own implementations:
| Storage | Crypto | |
|---|---|---|
| CLI / Electron | fs → ~/.authenticator-clui/ |
Node built-in crypto |
| React Native (planned) | react-native-mmkv |
react-native-quick-crypto |
This is why React Native support is feasible without rewriting the engine:
RN can't run Node's fs/crypto, but it can implement these two
interfaces.
Why core is vendored, not a published dependency: packages/core is
private: true and exists only inside this repo (its npm workspace name is
still required by npm, but clients load a vendored copy, not that import path).
The CLI's build step copies core's compiled output into
packages/cli/vendor/core, so the published npm tarball is fully
self-contained. This keeps core's API free to change rapidly during early
development. Once it stabilizes, it may be published as its own package.
Requirements: Node.js ≥ 14, npm ≥ 7 (for workspaces support).
git clone https://github.com/sthnaqvi/otpeer-authenticator.git
cd otpeer-authenticator
npm install # installs all workspace deps (repo root only)
npm start # lists available surface commands — does not launch an appRun a surface from the repo root:
| Command | What it does |
|---|---|
npm run desktop |
Build and launch the Electron desktop app |
npm run cli -- … |
Run the CLI (e.g. npm run cli -- --help) |
npm run website |
Start the otpeer.com landing page dev server |
npm run mobile |
Mobile app (not implemented yet) |
npm run build |
Build shared core and vendor into CLI + desktop |
npm test |
Run core test suite |
npm run build at the root does three things, in order:
packages/core: TypeScript →packages/core/dist/packages/cli: copiescore/dist→packages/cli/vendor/corepackages/desktop: copiescore/dist→packages/desktop/vendor/core
Output directories are gitignored; they're regenerated on build. Desktop and CLI
start scripts also rebuild core automatically when dist is missing.
Do not run npm install inside packages/desktop or packages/cli — use
the monorepo root. Inside a package, npm start still works when you are already
there (e.g. cd packages/desktop && npm start launches desktop only).
End users should download OTPeer Authenticator from otpeer.com (OS-aware links to the latest build).
Installers are published on
GitHub Releases
(tags named desktop-v*, e.g. desktop-v0.1.0) — that is the artifact
source of truth the website points at.
| Platform | Artifact |
|---|---|
| macOS | .dmg (arm64 = Apple Silicon, x64 = Intel) |
| Windows | NSIS .exe |
| Linux | .AppImage and .deb |
| What you see | Why |
|---|---|
| “… is damaged and can’t be opened” | App has no Developer ID signature. On recent macOS, Chrome/Safari downloads of unsigned apps get this dialog (not the older “unidentified developer” one). |
| “Apple could not verify…” / unidentified developer | Requires a paid Apple Developer Program Developer ID Application certificate (~$99/year). |
| Opens with no warning | Requires Developer ID plus Apple notarization. |
Unsigned builds cannot produce the “unidentified developer” dialog for browser downloads — Apple closed that path. Until notarization is wired, clear quarantine once after install:
xattr -cr "/Applications/OTPeer Authenticator.app"
open "/Applications/OTPeer Authenticator.app"When you have an Apple Developer account, we can add CI signing + notarization so Mac users get a normal install.
Maintainers — cut a release:
- Merge desktop work to
master. - Confirm
packages/desktop/package.jsonversion(e.g.0.1.0). - Tag and push:
git tag desktop-v0.1.0 && git push origin desktop-v0.1.0 - Wait for the Desktop release workflow; assets appear on the Releases page.
Local Mac-only packaging: npm run desktop then cd packages/desktop && npm run dist,
or from root after a desktop build: cd packages/desktop && npm run dist.
From the repo root:
npm run cli -- --help
npm run cli -- --import "otpauth-migration://offline?data=..."
npm run cli -- --runOr from packages/cli: node bin.js --help (same entry point).
The vault is written to ~/.authenticator-clui/accounts.json — same
location as an installed copy, so be aware they share state.
Core has a Jest test suite covering encryption (GCM round-trip, tamper detection, legacy CBC decrypt), vault format migrations, storage-path migration, TOTP window alignment, base32 encoding, and import parsing.
npm test # runs core's test suite from the repo rootOnly packages/cli is published, and only from that directory:
cd packages/cli
npm publishprepublishOnly automatically runs the full root build first, so a publish
can never ship a stale or missing vendor/core. Publishing from the repo
root is blocked by design (private: true).
Release checklist:
- Bump the version:
cd packages/cli && npm version minor(orpatch/major). Note: npm does not auto-commit or tag in a monorepo subfolder — that's step 4, and why it's listed separately. npm installat the repo root to resyncpackage-lock.json, and update the package README if user-facing behavior changed; commit.cd packages/cli && npm publish- Tag the release:
git tag v<version> && git push --tags
Development is staged; each stage has an in-depth design doc in
docs/plan/:
| Stage | Scope | Doc |
|---|---|---|
| A1 ✅ | Extract shared core, monorepo restructure, publish hardening | doc |
| A2 ✅ | Vault format versioning, AES-GCM upgrade, migrations, test suite | doc |
| B ✅ | CLI: single-account CRUD, code/copy/qr/export, otplib removal | doc |
| B2 ✅ | Full OTP compatibility (8-digit/60s/SHA-256/HOTP/Steam), Aegis/2FAS/andOTP imports, paper backup | doc |
| C ✅ | P2P sync v1: QR-paired local sync, minimal permissions, LWW merge | doc |
| C2 ✅ | Rebranding: OTPeer product family, ASO/store naming, marketing plan | doc |
| D ✅ | Desktop app "OTPeer Authenticator" (Electron, Mac/Ubuntu/Windows) — deployment channels | doc |
| D2 | Website at otpeer.com — Desktop downloads, CLI install, marketing | doc |
| E | Mobile app "OTPeer Authenticator" (React Native, iOS/Android) | doc |
| F | CI, packaging, store submissions | doc |
| G | Browser extension (desktop-app native messaging) | doc |
Contributions are welcome — bug reports, fixes, and stage work alike.
Where things go:
- Platform-independent logic (vault, crypto orchestration, TOTP, import
parsing, future sync) →
packages/core, in TypeScript, behind the adapter interfaces. Core must neverrequirefs,crypto, or any platform module outsidesrc/node/. - Terminal-specific code (argument parsing, prompts, table rendering) →
packages/cli. - New platform (e.g. a new client) → new
packages/<name>workspace that consumes core; don't fork core logic into a client.
Workflow:
- Fork and branch from
master npm install && npm run build, make your change- Verify the CLI flows still work (
--import,--run,--delete,--encryptround-trip) — behavior changes to existing flags need a strong reason - Open a PR describing what changed and why; link the roadmap stage if your change is part of one
Ground rules:
- Existing users' vaults are sacred: any change to the storage location or file format must ship with an automatic migration (see stage-a2 doc for the pattern).
- No network calls anywhere in the codebase until Stage C, and then only the explicit local sync path. This project's core promise is that secrets stay on-device.
- Keep the published tarball auditable:
packages/cli/package.jsonuses afileswhitelist; if you add a runtime file, add it there deliberately.
Reporting issues: github.com/sthnaqvi/otpeer-authenticator/issues
This project handles 2FA secrets, so the bar is deliberately conservative:
- Vault encryption: AES-256-GCM authenticated encryption with random IV + salt per encryption and scrypt key derivation — wrong passwords and tampered/corrupted vault files fail loudly at the auth-tag check. Vaults written by older versions (AES-256-CBC) are transparently re-encrypted to GCM on first use.
- No network, no telemetry, no analytics
- Found a vulnerability? Please open an issue asking for a private contact channel rather than posting exploit details publicly.
MIT © Sayed Tauseef Naqvi
