Skip to content

Repository files navigation

PAN-OTP: One-Time Password Format Converter

Convert between different OTP (One-Time Password) data formats with ease. Like Pandoc, but for OTP credentials.

Motivation

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.

Features

  • 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

Installation

Requirements

  • Python 3.12+

From Source

git clone https://github.com/adityarup/pan-otp.git
cd pan-otp
pip install -e .

Quick Start

Command Line

Convert JSON to CSV:

pan-otp convert input.json --format csv --output output.csv

Convert with data cleaning:

pan-otp convert data.json --format otpauth --output otp.txt --clean

Convert and sort alphabetically by issuer:name:

pan-otp convert accounts.json --format csv --output sorted.csv --sort

Export to Google Authenticator format:

pan-otp convert accounts.csv --format otpauth-migration --output migration.txt

View all available formats:

pan-otp convert --help

Python API

from 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)

Data Formats

JSON

{
  "accounts": [
    {
      "secret": "JBSWY3DPEBLW64TMMQ======",
      "name": "alice@example.com",
      "issuer": "Google",
      "type": "totp",
      "algorithm": "sha1",
      "digits": 6,
      "period": 30,
      "counter": null
    }
  ]
}

CSV

secret,name,issuer,type,algorithm,digits,period,counter
JBSWY3DPEBLW64TMMQ======,alice@example.com,Google,totp,sha1,6,30,

otpauth:// URI

otpauth://totp/Google:alice@example.com?secret=JBSWY3DPEBLW64TMMQ%3D%3D%3D%3D%3D%3D&issuer=Google&algorithm=SHA1&digits=6

otpauth-migration://

Binary protobuf format used by Google Authenticator for exporting/importing multiple accounts.

Architecture

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)

Design Patterns

  • Adapter Pattern: Unified interface (Adapter) for format conversion
  • Functional Cleaning: clean_account(account) -> account returns new instances
  • Field Validation: Pydantic validators on OtpEntity for data integrity
  • Error Handling: Skip invalid entries with logging, no exceptions on partial failures

Data Cleaning

The clean_otp_labels() function performs the following operations:

  1. URL Decoding: %20 → space, etc.
  2. Unicode Normalization: NFC form
  3. Whitespace: Strip, collapse multiple spaces
  4. 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
  5. Duplicate Removal: Issuer in both fields
  6. Title Casing: "google" → "Google"
  7. 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"

Development

Code Quality

Uses Ruff for formatting and linting:

# Check for issues
ruff check src/

# Auto-fix and format
ruff format src/

Format on Save (VS Code)

Install the Ruff extension for automatic formatting on save. Configuration is already in .vscode/settings.json.

Shell Auto-Completion

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-completion

Then reload your shell:

source ~/.bashrc    # Bash
source ~/.zshrc     # Zsh

After setup, enjoy auto-completion:

pan-otp c<TAB>     # Completes to 'convert'
pan-otp convert -<TAB>  # Shows available options

Validation

# Type checking with Pydantic validators
python -c "from pan_otp import OtpEntity; OtpEntity(secret='invalid')"
# ValueError: invalid base32 secret

Security Considerations

  • 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

License

This project is licensed under the BSD 3-Clause License - see LICENSE file for details.

Copyright © 2026 Adityarup Laha

Contributing

Contributions welcome! Code must pass Ruff checks and validators must be respected.

Troubleshooting

Protobuf compilation error

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 hook

Invalid secret format

ValueError: invalid base32 secret

Solution: Ensure secrets are properly base32-encoded (A-Z, 2-7, and = padding)

Line too long in linting

ruff format src/  # Auto-format to 88-char lines

Completion not working

Problem: Tab-completion not available after installation

Solution:

  1. Verify pan-otp is in PATH: which pan-otp
  2. Re-source your shell config: source ~/.bashrc
  3. Try the interactive installer: pan-otp --install-completion
  4. 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   # Zsh

Acknowledgments

This project was developed with Claude Haiku 4.5 via GitHub Copilot.

References

About

Convert between different OTP (One-Time Password) data formats with ease. Like Pandoc, but for OTP credentials.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages