Convert between different OTP (One-Time Password) data formats with ease. Like Pandoc, but for OTP credentials.
After years of using Google Authenticator, my account list became incredibly cluttered—duplicates, inconsistent naming, services with polluted issuer/account fields. This tool exists to help you escape that mess: export your OTP accounts, clean them up, reorganize them, and import elsewhere. Think of it as a defragmenter for your 2FA setup.
-
Multiple Format Support
- JSON (structured key-value format)
- CSV (spreadsheet-compatible)
otpauth://URIs (standard OTP URI format)otpauth-migration://(Google Authenticator export format)
-
Data Cleaning & Normalization
- Automatic issuer extraction from various formats
- Whitespace and unicode normalization
- Duplicate detection and removal
- Title-casing for common services (Google, GitHub, etc.)
-
Type Safety & Validation
- Pydantic v2 models with field validation
- Base32 secret validation
- Configurable OTP parameters (digits, period, algorithm)
-
CLI & Library
- Command-line interface for format conversion
- Python API for programmatic use
- Comprehensive error handling with detailed logging
-
Organization & Sorting
- Sort accounts by issuer and name
- Deterministic output for reproducible exports
- Easily reorganize before importing to new authenticators
- Python 3.12+
git clone https://github.com/adityarup/pan-otp.git
cd pan-otp
pip install -e .Convert JSON to CSV:
pan-otp convert input.json --format csv --output output.csvConvert with data cleaning:
pan-otp convert data.json --format otpauth --output otp.txt --cleanConvert and sort alphabetically by issuer:name:
pan-otp convert accounts.json --format csv --output sorted.csv --sortExport to Google Authenticator format:
pan-otp convert accounts.csv --format otpauth-migration --output migration.txtView all available formats:
pan-otp convert --helpfrom pan_otp import (
OtpEntity, OtpList, OtpType, Algo,
JsonAdapter, CsvAdapter, OtpauthAdapter,
clean_otp_labels
)
# Create OTP entities
accounts = OtpList(accounts=[
OtpEntity(
secret="JBSWY3DPEBLW64TMMQ======",
name="alice@example.com",
issuer="Google",
type=OtpType.TOTP,
algorithm=Algo.SHA1,
digits=6,
period=30
)
])
# Load from different formats
json_adapter = JsonAdapter()
csv_adapter = CsvAdapter()
data = json_adapter.load(open("data.json").read())
# Clean the data
cleaned = clean_otp_labels(data)
# Export to different format
otpauth_text = OtpauthAdapter.dump(cleaned)
print(otpauth_text){
"accounts": [
{
"secret": "JBSWY3DPEBLW64TMMQ======",
"name": "alice@example.com",
"issuer": "Google",
"type": "totp",
"algorithm": "sha1",
"digits": 6,
"period": 30,
"counter": null
}
]
}secret,name,issuer,type,algorithm,digits,period,counter
JBSWY3DPEBLW64TMMQ======,alice@example.com,Google,totp,sha1,6,30,otpauth://totp/Google:alice@example.com?secret=JBSWY3DPEBLW64TMMQ%3D%3D%3D%3D%3D%3D&issuer=Google&algorithm=SHA1&digits=6
Binary protobuf format used by Google Authenticator for exporting/importing multiple accounts.
pan_otp/
├── models.py # Pydantic models (OtpEntity, OtpList)
├── main.py # CLI interface (Typer)
├── cleaner.py # Data cleaning utilities
├── adapters/
│ ├── base.py # Abstract Adapter interface
│ ├── json.py # JSON adapter
│ ├── csv.py # CSV adapter
│ ├── otpauth.py # otpauth:// URI adapter
│ └── otpauth_migration.py # Google Authenticator format
└── protos/
├── google_auth.proto # Protobuf definition
└── google_auth_pb2.py # Generated protobuf code (build-time)
- Adapter Pattern: Unified interface (
Adapter) for format conversion - Functional Cleaning:
clean_account(account) -> accountreturns new instances - Field Validation: Pydantic validators on OtpEntity for data integrity
- Error Handling: Skip invalid entries with logging, no exceptions on partial failures
The clean_otp_labels() function performs the following operations:
- URL Decoding:
%20→ space, etc. - Unicode Normalization: NFC form
- Whitespace: Strip, collapse multiple spaces
- Issuer Extraction from multiple formats:
- Colon:
"Google: account"→ issuer=Google, name=account - Dash:
"Google - account" - Parentheses:
"account (Google)" - Brackets:
"account [Google]" - Email:
"user@gmail.com"→ issuer=gmail.com, name=user
- Colon:
- Duplicate Removal: Issuer in both fields
- Title Casing:
"google"→"Google" - Empty String Handling: Convert to
None
Example:
from pan_otp import clean_account, OtpEntity
account = OtpEntity(
secret="ABC123",
name="google:%20account",
issuer="google"
)
cleaned = clean_account(account)
# Result: name="account", issuer="Google"Uses Ruff for formatting and linting:
# Check for issues
ruff check src/
# Auto-fix and format
ruff format src/Install the Ruff extension for automatic formatting on save. Configuration is already in .vscode/settings.json.
Enable tab-completion for the pan-otp command:
Bash (add to ~/.bashrc):
eval "$(_PAN_OTP_COMPLETE=bash_source pan-otp)"Zsh (add to ~/.zshrc):
eval "$(_PAN_OTP_COMPLETE=zsh_source pan-otp)"Fish (add to ~/.config/fish/config.fish):
eval (env _PAN_OTP_COMPLETE=fish_source pan-otp)Interactive Install:
pan-otp --install-completionThen reload your shell:
source ~/.bashrc # Bash
source ~/.zshrc # ZshAfter setup, enjoy auto-completion:
pan-otp c<TAB> # Completes to 'convert'
pan-otp convert -<TAB> # Shows available options# Type checking with Pydantic validators
python -c "from pan_otp import OtpEntity; OtpEntity(secret='invalid')"
# ValueError: invalid base32 secret- Secret Storage: OTP secrets remain in client memory only
- Base32 Validation: Strict validation of secret encoding
- No Persistence: CLI does not persist data between runs
- UTF-8 Encoding: All file I/O explicitly uses UTF-8
- Field Validation: Digit ranges (6-10), positive periods, non-negative counters
This project is licensed under the BSD 3-Clause License - see LICENSE file for details.
Copyright © 2026 Adityarup Laha
Contributions welcome! Code must pass Ruff checks and validators must be respected.
ImportError: Protobuf not compiled. Run build to compile protos.
Solution: The project needs to be built before using Google Authenticator export format:
pip install -e . # Triggers build hookValueError: invalid base32 secret
Solution: Ensure secrets are properly base32-encoded (A-Z, 2-7, and = padding)
ruff format src/ # Auto-format to 88-char lines
Problem: Tab-completion not available after installation
Solution:
- Verify
pan-otpis in PATH:which pan-otp - Re-source your shell config:
source ~/.bashrc - Try the interactive installer:
pan-otp --install-completion - Or use the helper script:
bash install-completion.sh
If completion still doesn't work, ensure the package is properly installed:
pip install -e .
hash -r # Bash
rehash # ZshThis project was developed with Claude Haiku 4.5 via GitHub Copilot.
- RFC 4226 - HOTP - HMAC-based One-Time Password
- RFC 6238 - TOTP - Time-based One-Time Password
- Key Uri Format - Standard OTP URI scheme