Skip to content

Latest commit

 

History

History
101 lines (68 loc) · 3.68 KB

File metadata and controls

101 lines (68 loc) · 3.68 KB

lib2fas Python

Unofficial implementation of 2fas for Python (as a library). This library serves as the backend for the robinvandernoord/2fas-python CLI, a command-line tool that provides an easy interface to interact with the 2fas TOTP.

Installation

To install this project, use pip:

pip install lib2fas
# or to also install the cli tool:
pip install 2fas

Usage

After installing the package, you can import it in your Python scripts as follows:

import lib2fas

services = lib2fas.load_services("/path/to/file.2fas", passphrase="optional")  # -> TwoFactorStorage

services.generate()  # generate all TOTP keys

gmail = services["gmail"]  # exact match (case-insensitive), returns a list of 'TwoFactorAuthDetails' instances.

github = services.find("githbu")  # fuzzy match should find GitHub, returns a new TwoFactorStorage.

for label, services in github.items():
    # one label can have multiple services!
    for service in services:  # 'service' is a TwoFactorAuthDetails instance
        # Print label, service name, and TOTP code
        print("Label:", label)
        print("Service Name:", service.name)
        print("TOTP Code:", service.generate())  # or .generate_int() to get the code as a number.

The passphrase option of load_services is optional. If you don't provide a passphrase and your file is encrypted, you will be prompted for one. When available, the OS keyring stores it under a per-session name. After a reboot it is no longer retrieved, although its stale keyring item may remain until it is cleaned up.

Storing the passphrase in the login keyring is a cache-expiry mechanism, not access control. Do not assume it protects the passphrase from other processes in your unlocked login session; the exact protection depends on the keyring backend.

Note: only the "Secret Storage" keychain backend on Debian-based Linux has been tested.

Unlocking with a key instead of a passphrase

A .2fas file is encrypted with AES-GCM under a key that PBKDF2 derives from your passphrase and a salt stored inside the file itself. You can work with that key directly instead of the passphrase:

import json

import lib2fas

with open("/path/to/file.2fas") as f:
    encrypted_block = json.load(f)["servicesEncrypted"]

salt = lib2fas.extract_salt(encrypted_block)  # the PBKDF2 salt embedded in the file
key = lib2fas.derive_key("my passphrase", salt)  # 32 bytes

services = lib2fas.load_services("/path/to/file.2fas", key=key)

For interactive unlocking you can inject your own UnlockerProtocol instead, which lets the caller decide where a key comes from and how long it is cached. For example, unlocking with a key stored on a hardware token instead of typing a passphrase:

import lib2fas


class HardwareKeyUnlocker(lib2fas.UnlockerProtocol):
    def unlock(self, filename: str, salt: bytes) -> bytes | None:
        return my_hardware_token.derive_key(salt)

    def invalidate(self, filename: str, salt: bytes) -> None: ...

    def cleanup(self) -> int:
        return -1  # number of stale items removed, or -1 if unknown


services = lib2fas.load_services("/path/to/file.2fas", unlocker=HardwareKeyUnlocker())

invalidate is called automatically when a key fails to decrypt, so the unlocker can evict a bad cached entry before the next retry. load_services retries until unlock() returns None (in which case it returns None too); pass max_retries=N to tolerate N failures and raise the PermissionError on the next one. The default unlocker is lib2fas.PassphraseUnlocker, which reproduces the keyring behaviour described above.

License

This project is licensed under the MIT License.