Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mfa

TOTP codes on your desktop, so you stop reaching for your phone.

Rust Platform: macOS License: MIT Tests

A native macOS app in Rust: eframe for the picker window, objc2 and AppKit for the Dock and activation behaviour, security-framework for the login keychain, and nokhwa for the webcam importer. RFC 6238 and RFC 4226 are implemented directly and checked against the published test vectors.

Two sections here are about things that failed silently and took real work to diagnose, and they are the honest part of the project: why this is not a menu bar app, and what enrolling a second device costs you in security.

Click the Dock icon, or press control-option-command-M from anywhere. Type a few letters, press Enter, and the code is on your clipboard. Secrets live in the macOS login keychain and are never written to disk.

Install

scripts/install.sh

Builds ~/Applications/mfa.app, symlinks the mfa command into ~/.local/bin, and registers a LaunchAgent so it starts at login. Because the app runs all the time, its Dock icon is always there.

Add --pin-dock to also pin it permanently, so the icon keeps its place when the app is not running. That one rewrites your Dock layout and restarts the Dock, which is why it is opt in.

scripts/install.sh --uninstall reverses all of it.

Enroll an account

Three ways in, depending on what the site gives you.

Paste the secret. Most sites show a "can't scan the code?" text secret next to the QR. Spaces, hyphens and lowercase are all fine.

mfa add gmail

Read a QR screenshot. Screenshot the QR with cmd-shift-4, then:

mfa add --qr ~/Desktop/qr.png

Paste an otpauth:// URI, if you already have one:

mfa add gmail --uri 'otpauth://totp/...'

Move everything over at once

From Google Authenticator, with the webcam

Google Authenticator will not export anything but a QR code, so the shortest path is to hold the phone up to the camera. Open the picker (Dock icon, or control-option-command-M) and click Scan with camera. From a terminal, mfa scan does the same thing.

Import file... in the same window opens a file picker for QR screenshots and export files, and you can drag images straight onto the window.

  1. In Google Authenticator: menu, then Transfer accounts, then Export accounts
  2. Pick the accounts and continue, which shows a QR code
  3. Hold the phone screen up to the webcam until the accounts appear in the list
  4. Save to keychain

One QR code carries up to ten accounts, so a normal setup is a single scan. If Google Authenticator splits the export across several codes, keep scanning: the window accumulates accounts across every code it reads and only writes to the keychain when you save. Codes already read are ignored, so a code lingering in frame does not add anything twice.

macOS asks for camera access the first time. If it was declined, the window offers an Open Camera settings button and a Try again button, so the switch can be flipped without restarting anything.

Two things about camera access are worth knowing, because both fail silently. The app needs the com.apple.security.device.camera entitlement, since the hardened runtime otherwise denies the camera with no prompt and no error. And macOS attributes the request to the "responsible process", which for a binary run by a shell is the terminal, not this app; mfa scan therefore relaunches itself through open so that it is responsible for its own request. Without either piece the camera opens successfully and then delivers no frames for ever, which looks exactly like broken hardware.

From a file or a screenshot

mfa import <file-or-screenshot>

Recognized automatically:

Source What to hand it
Google Authenticator Screenshot of the "Transfer accounts" QR
Aegis Unencrypted JSON export
2FAS JSON export
Anything else Text file of otpauth:// URIs, one per line

Images can be PNG, JPEG, GIF, BMP, TIFF, WebP or HEIC. HEIC matters because a photo taken on an iPhone is HEIC, and that is what you get from pointing one phone at another; it is transcoded with sips before decoding.

An encrypted Aegis export cannot be read. Re-export with encryption turned off, import it, then delete the plaintext file.

Counter-based (HOTP) and Steam accounts are skipped, and named in the output.

The picker

Click the Dock icon or press control-option-command-M. Then:

Key What it does
type filters accounts
up, down move the selection
Enter copy the selected code and close
Esc close
the hotkey again close

It also closes when it loses focus, so clicking away is safe. On close the app steps aside and hands focus back to whatever you were typing in, so the code is ready to paste and the next Dock click counts as a fresh activation.

To change the combination, set MFA_HOTKEY and restart:

MFA_HOTKEY='cmd+shift+backslash' mfa

Modifiers are ctrl, alt (or opt), cmd, shift, joined with +. At least one modifier is required, because a bare key would fire while you type. To make a change stick, edit the ProgramArguments in ~/Library/LaunchAgents/com.brahy.mfa.plist to set the variable, or re-run the installer after editing DEFAULT_HOTKEY in src/picker.rs.

Why not the menu bar

The first version of this put the codes in a menu bar dropdown. On a 14 inch MacBook Pro that does not work, and the reason is worth recording.

The status item was created successfully every time. It just never appeared. Reading the window server's list of menu bar windows explains it: Control Center alone owns 18 items running from x=701 to x=1514 on a screen 1512 points wide. The bar is already overflowing past the right edge, so a new status item is given zero width and no window at all. Nothing about the icon or the title changes that.

So the Dock icon and the hotkey are not conveniences layered on top of the menu bar. They are the interface. mfa tray still starts the menu bar version if you want it on a machine with room to spare, and mfa list --codes always works in a terminal.

Day to day

mfa                    # start the picker (what the LaunchAgent runs)
mfa scan               # read transfer QR codes with the webcam
mfa list               # what is enrolled
mfa list --codes       # with live codes and the countdown
mfa code gmail         # print one code and copy it to the clipboard
mfa remove gmail       # forget an account
mfa export --yes       # print every secret as a URI, for a backup
mfa tray               # menu bar version, if your menu bar has room

code and remove match loosely: enough of the name to be unambiguous is enough. mfa code git finds github.

The picker re-reads the vault every time it opens, so an account added from the terminal is there immediately.

What this costs you in security

Your phone was a second device. This is not. A compromised Mac now yields both your password manager and your TOTP seeds, where before it yielded one of them.

The keychain narrows the window rather than closing it: secrets are encrypted at rest, unlocked with your login, and never touch disk in plaintext. Any process running as you can still read them, which is the same footing as your browser's saved passwords.

That trade is worth making for routine logins and not worth making for the accounts that would hurt most. Keep root, banking, and domain registrars on the phone.

Backups

Enrolling here does not remove the account from your phone, and a TOTP secret works in as many apps as you enroll it in. Keeping both is the cheapest backup.

mfa export --yes prints every secret in plain text. It is a real backup and a real liability, so treat the output like a password file: no shell history, no unencrypted disk, no cloud sync.

Layout

Path What it holds
src/totp.rs RFC 6238 / RFC 4226, verified against the spec's test vectors
src/account.rs An account and its otpauth:// form
src/store.rs The keychain-backed vault
src/enroll.rs QR decoding and the import formats
src/picker.rs The hotkey and the picker window
src/scan.rs The webcam scanner
src/tray.rs The menu bar version
src/main.rs The CLI
assets/icon.svg The app icon, as editable source
src/bin/mkicon.rs Renders that SVG into assets/mfa.icns

To change the icon, edit assets/icon.svg and re-render it:

cargo run --features mkicon --bin mkicon

Tests

cargo test

Codes are checked against every published RFC 6238 vector across SHA1, SHA256 and SHA512, and the QR path is tested by encoding real PNGs and reading them back.

Set MFA_DEBUG=1 to trace the window lifecycle on stderr, which is how the menu bar problem above was found. MFA_DEBUG_SHOW=1 opens the picker window at startup, so the window can be checked without synthesising a system-wide key event.

License

MIT. See LICENSE.

About

TOTP codes on the macOS desktop: Dock app, global hotkey, secrets in the login keychain.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages