Skip to main content

AVGVSTO USB

Hardware-bound AES-256-GCM encryption — your USB drive is the key.

PyPI version Python versions License

AVGVSTO fuses your password with the physical fingerprint of any USB drive. Without the exact hardware, decryption is impossible — even with the correct password. Features anti-bruteforce progressive cooldown, duress/plausible deniability mode, and tamper-evident encryption.


Quick start

pip install avgvstousb
import avgvsto

# 1. Plug in a USB drive and bind it
avgvsto.save_usb_config("E:\\")         # Windows
# avgvsto.save_usb_config("/media/usb")  # Linux
# avgvsto.save_usb_config("/Volumes/USB")# macOS

usb_id = avgvsto.load_usb_id()

# 2. Encrypt a file
avgvsto.encrypt_file("classified.docx", "my-password", usb_id)

# 3. Decrypt it
avgvsto.decrypt_file("classified.docx.avgvsto", "my-password", usb_id)

Table of contents


Why AVGVSTO?

Feature What it means
USB hardware binding The physical USB drive is mixed into the encryption key. A cloned drive won't work.
AES-256-GCM Authenticated encryption — tamper-evident. Military-grade standard.
ChaCha20-Poly1305 Modern stream cipher. Faster on devices without AES-NI.
Anti-bruteforce Progressive cooldown: 5s → 20s → 40s → 60s+ per wrong guess. Persists across restarts.
Attempt limits Per-file max-attempt counter. File self-destructs after N wrong guesses.
Duress mode Two passwords in one file. One reveals a decoy, the other reveals the real data. Forensically indistinguishable.
Zero dependencies on cloud 100% offline. No telemetry. No accounts.
Secure deletion 3-pass random overwrite + zero pass before unlinking the original file.

Installation

pip install avgvstousb

Requires Python 3.9+. Works on Windows, macOS, and Linux.

System dependencies:

  • pycryptodome — AES-256-GCM / ChaCha20-Poly1305 / PBKDF2
  • psutil — USB drive detection

These are installed automatically.

Verify installation

avgvsto status
[--] No USB binding configured.
    Files encrypted:  0
    Files decrypted:  0
    Bytes encrypted:  0.0 B

Security model

User password  ─┐
                ├──→ PBKDF2-HMAC-SHA256 (1M iterations) ──→  256-bit key
USB hardware ID ─┘
  • No key is stored on disk. The USB ID is not a key — it's mixed into the key derivation so that decryption requires both the password AND the exact physical USB drive.
  • No cloud. No telemetry. No backdoors. Every cryptographic primitive is from pycryptodome, a well-audited open-source library.
  • Attempt limits are stored locally in ~/.avgvsto/. They survive restarts and can only be reset with the reset password (stored on the USB itself).

What an attacker needs

Scenario Outcome
Has the encrypted file, NO USB drive Cannot decrypt — hardware fingerprint is missing from key derivation
Has the file + USB drive, WRONG password Cannot decrypt — PBKDF2 produces wrong key, GCM auth tag check fails
Has everything, but attempt limit hit File is permanently locked — must use reset password
Both correct, no limit Only way to decrypt

Library API reference

Encryption & decryption

avgvsto.encrypt_file(src_path, password, usb_id, max_attempts=0, duress_password=None, duress_data=b"", cipher_id=0) → str

Encrypts a single file. The original file is securely overwritten and deleted. Returns the path to the encrypted .avgvsto file.

avgvsto.encrypt_file("photo.jpg", "s3cret", usb_id)
# → "photo.jpg.avgvsto"

avgvsto.encrypt_file("photo.jpg", "s3cret", usb_id, max_attempts=5)
# → "photo.jpg.avgvsto"  (locked after 5 wrong passwords)

avgvsto.encrypt_file("photo.jpg", "s3cret", usb_id, cipher_id=avgvsto.CIPHER_CHACHA20)
# → "photo.jpg.avgvsto"  (uses ChaCha20-Poly1305 instead of AES-256-GCM)

Parameters:

Arg Type Default Description
src_path str — Path to the file to encrypt
password str — Encryption password
usb_id str — USB hardware identifier (from save_usb_config() or load_usb_id())
max_attempts int 0 Max decryption attempts (0 = unlimited)
duress_password str|None None Second password that decrypts to decoy data
duress_data bytes b"" Decoy content revealed by duress password
cipher_id int CIPHER_AES CIPHER_AES (0) or CIPHER_CHACHA20 (1)

Raises: ValueError if the file is already encrypted (has .avgvsto extension).


avgvsto.decrypt_file(src_path, password, usb_id) → tuple[str, int]

Decrypts an .avgvsto file. The encrypted file is securely deleted after successful decryption. Returns (result_path, max_attempts).

result, max_att = avgvsto.decrypt_file("photo.jpg.avgvsto", "s3cret", usb_id)
# result → "photo.jpg"
# max_att → 5

Parameters:

Arg Type Description
src_path str Path to the .avgvsto file
password str Decryption password
usb_id str USB hardware identifier

Returns: (path_to_decrypted_file, max_attempts_from_header)

Raises: ValueError if authentication fails (wrong password, wrong USB, or corrupted file).


avgvsto.verify_file(src_path, password, usb_id) → tuple[bool, str]

In-memory integrity check. No files are written or modified. Returns (valid, detail_message).

ok, msg = avgvsto.verify_file("photo.jpg.avgvsto", "s3cret", usb_id)
print(ok)   # True
print(msg)  # "[OK] Integrity OK  ·  Format v1  ·  AES-256-GCM  ·  Attempts limit: 5  ·  Payload: 1.2 MB"

Parameters:

Arg Type Description
src_path str Path to the .avgvsto file
password str Password to verify against
usb_id str USB hardware identifier

avgvsto.encrypt_folder(folder_path, password, usb_id, max_attempts=0, duress_password=None, duress_data=b"", on_progress=None, cipher_id=0) → tuple[int, list[str]]

Recursively encrypts all files in a folder. Non-.avgvsto files are processed. Returns (success_count, error_messages).

ok, errors = avgvsto.encrypt_folder("./docs", "s3cret", usb_id)
print(f"{ok} files encrypted")   # "47 files encrypted"

# With progress callback
def progress(done, total, name):
    print(f"[{done+1}/{total}] {name}")

ok, errors = avgvsto.encrypt_folder("./docs", "s3cret", usb_id, on_progress=progress)

avgvsto.decrypt_folder(folder_path, password, usb_id, on_progress=None) → tuple[int, list[str]]

Recursively decrypts all .avgvsto files in a folder.

ok, errors = avgvsto.decrypt_folder("./docs", "s3cret", usb_id)

USB management

avgvsto.list_usb_drives() → list[str]

Detects connected removable drives. Returns a list of mount points / drive letters.

drives = avgvsto.list_usb_drives()
print(drives)  # ['E:\\']  on Windows
               # ['/media/user/USB']  on Linux
               # ['/Volumes/Untitled']  on macOS

avgvsto.save_usb_config(usb_path) → str | None

Reads the hardware fingerprint of a USB drive and stores it in ~/.avgvsto/usb_secure.key. Returns the USB ID string, or None on failure.

usb_id = avgvsto.save_usb_config("E:\\")
if usb_id:
    print(f"USB bound: {usb_id[:16]}...")

avgvsto.load_usb_id() → str | None

Loads the previously saved USB identifier. Returns None if no binding exists.

usb_id = avgvsto.load_usb_id()
if not usb_id:
    print("No USB binding configured.")

avgvsto.find_authorized_usb(saved_id) → str | None

Searches currently connected drives for one matching the saved ID. Returns the mount path or None.

path = avgvsto.find_authorized_usb(usb_id)
if path:
    print(f"Authorised USB connected at {path}")
else:
    print("Plug in the correct USB drive.")

avgvsto.get_usb_identifier(mount_path) → str | None

Reads the raw hardware fingerprint of a drive without saving it. Useful for one-shot checks.

uid = avgvsto.get_usb_identifier("E:\\")

Attempt tracking & brute-force protection

avgvsto.get_attempt_count(file_path) → int

Returns the number of recorded wrong-password attempts for a specific file.

count = avgvsto.get_attempt_count("photo.jpg.avgvsto")
if count >= 3:
    print("Too many wrong attempts!")

avgvsto.increment_attempt_count(file_path) → int

Records one more failed attempt for a file. Returns the new count.

avgvsto.increment_attempt_count("photo.jpg.avgvsto")

avgvsto.reset_attempt_count(file_path) → None

Clears the attempt counter for a file. Useful after successful decryption.

avgvsto.reset_attempt_count("photo.jpg.avgvsto")

avgvsto.brute_remaining() → float

Returns the number of seconds remaining in the progressive cooldown. 0.0 means free to proceed.

import time

rem = avgvsto.brute_remaining()
if rem > 0:
    print(f"Wait {rem:.0f}s before trying again.")
    time.sleep(rem)

Cooldown progression:

Wrong attempt # Cooldown
1st 5 seconds
2nd 20 seconds
3rd 40 seconds
4th 60 seconds
5th+ (n-3) × 60 seconds (capped at 1 month)

The cooldown is persistent across restarts (saved to ~/.avgvsto/brute_state.json).


avgvsto.brute_mark_fail() → None

Records a wrong-password event and starts the cooldown timer. Call this after a failed authentication.

try:
    avgvsto.decrypt_file("file.avgvsto", "wrong", usb_id)
except ValueError:
    avgvsto.brute_mark_fail()

avgvsto.brute_mark_success() → None

Resets the cooldown counter to zero after a successful decryption.

avgvsto.decrypt_file("file.avgvsto", "correct", usb_id)
avgvsto.brute_mark_success()  # reset progressive counter

Duress / plausible deniability

Encrypt a file that has two independent passwords. One reveals innocent decoy data, the other reveals the real data. An adversary cannot tell which password is real.

# Encrypt with duress password
avgvsto.encrypt_file(
    "real_budget.xlsx",
    "real-password",
    usb_id,
    max_attempts=0,
    duress_password="decoy-password",
    duress_data=b"This is my homework assignment, nothing interesting here."
)

# Decrypt with real password → real data
data, _ = avgvsto.decrypt_file("real_budget.xlsx.avgvsto", "real-password", usb_id)

# Decrypt with duress password → decoy data
data, _ = avgvsto.decrypt_file("real_budget.xlsx.avgvsto", "decoy-password", usb_id)

Both passwords produce a valid decryption. The file format (v3 dual-slot) ensures both ciphertexts are mathematically independent and forensically identical.


Reset password system

When you set max_attempts > 0, files can become permanently locked after too many wrong guesses. The reset password (stored on the USB drive itself) lets you clear all attempt counters.

usb_mount = "E:\\"

# Create a reset password on the USB
avgvsto.create_reset_password(usb_mount, "my-reset-pw")

# Check if a reset password exists
avgvsto.has_reset_password(usb_mount)  # True

# Check if reset is available
ok, reason = avgvsto.can_reset(usb_mount)
print(reason)  # "Resets used: 0 / 3"

# Perform the reset — clears ALL attempt counters on this machine
success, msg = avgvsto.do_reset_counters(usb_mount, "my-reset-pw")
print(msg)  # "14 locked counter(s) cleared.  (Reset 1/3 used)"

# Wipe the reset config from USB entirely
avgvsto.full_clear_usb_reset(usb_mount)

Rules:

  • Max 3 wrong reset-password attempts → reset permanently locked
  • Max 3 successful resets total → use full_clear_usb_reset() to start over
  • One reset password covers ALL files encrypted with this USB drive
  • The reset password is stored on the USB itself (avgvsto_reset.json), not on your computer

Utilities

avgvsto.scan_usb_for_avgvsto(usb_path) → dict

Scans a directory (typically a USB drive) for .avgvsto files. Returns stats and a per-file list.

result = avgvsto.scan_usb_for_avgvsto("E:\\")
print(result["total"])       # 47
print(result["valid"])       # 45
print(result["corrupted"])   # 2
print(result["total_bytes"]) # 104857600

for f in result["files"]:
    print(f["path"], f["status"], f["max_attempts"])

Result structure:

{
    "total": 47,
    "valid": 45,
    "corrupted": 2,
    "total_bytes": 104857600,
    "files": [
        {
            "path": "E:\\docs\\report.pdf.avgvsto",
            "size": 2048000,
            "status": "valid",
            "max_attempts": 5,
            "mtime": "2026-07-15 14:30"
        },
        # ...
    ]
}

avgvsto.get_locked_attempt_files() → list[dict]

Returns all attempt counter files stored on this machine.

locked = avgvsto.get_locked_attempt_files()
for item in locked:
    print(item["slot"], item["count"])

avgvsto.secure_delete(path) → None

Overwrites a file with random data (3 passes) then zeros, then unlinks it. Used internally by encrypt_file and decrypt_file.

avgvsto.secure_delete("temp_secret.txt")

Note: On SSD / flash storage, wear-levelling means the OS may write to a different physical sector. This provides strong protection on HDDs and is significantly better than os.remove() on any storage.


Constants

Constant Value Description
avgvsto.CIPHER_AES 0x00 AES-256-GCM (default)
avgvsto.CIPHER_CHACHA20 0x01 ChaCha20-Poly1305
avgvsto.APP_NAME "AVGVSTO" Application name
avgvsto.APP_VERSION "4.0" Current version
avgvsto.ENC_EXT ".avgvsto" Encrypted file extension
avgvsto.MAGIC b"AVGVSTO2" File header magic bytes
avgvsto.FORMAT_VER 1 v1 header (legacy, AES-256 only)
avgvsto.FORMAT_VER_3 3 v3 header (any cipher + optional duress slot)
avgvsto.PBKDF2_ITERS 1_000_000 PBKDF2 iterations

CLI reference

Usage

avgvsto encrypt <path> [--usb PATH] [--attempts N] [--duress]
avgvsto decrypt <path> [--usb PATH]
avgvsto verify  <path> [--usb PATH]
avgvsto status
avgvsto bind-usb <path>

Commands

encrypt

Encrypt a file or folder.

avgvsto encrypt report.pdf --usb E:\ --attempts 5
Encryption password: ********
Confirm password:    ********
[OK] Encrypted -> report.pdf.avgvsto

With duress mode:

avgvsto encrypt secret.docx --usb E:\ --attempts 3 --duress
Encryption password: ********
Confirm password:    ********
Duress password:     ********
Confirm duress:      ********
[OK] Encrypted -> secret.docx.avgvsto

decrypt

Decrypt an .avgvsto file or a folder of encrypted files.

avgvsto decrypt report.pdf.avgvsto --usb E:\
Decryption password: ********
[OK] Decrypted -> report.pdf

verify

Check file integrity without writing anything to disk.

avgvsto verify report.pdf.avgvsto
Password: ********
[OK] [OK] Integrity OK  ·  Format v1  ·  AES-256-GCM  ·  Attempts limit: 5  ·  Payload: 1.2 MB

status

Display USB binding status and statistics.

avgvsto status
[OK] USB binding: 3a8f2c...  (connected at E:\)
    Files encrypted:  42
    Files decrypted:  17
    Bytes encrypted:  1.2 GB

bind-usb

Save a USB drive's hardware fingerprint for future encryption/decryption.

avgvsto bind-usb E:\
[OK] USB bound — ID: 3a8f2cd1e4b9...

Full examples

Example 1: Encrypt a document with attempt limit

import avgvsto

# Bind your USB drive (do this once)
usb_id = avgvsto.save_usb_config("E:\\")
if not usb_id:
    raise SystemExit("USB binding failed")

# Encrypt with a 5-attempt limit
avgvsto.encrypt_file("will.docx", "secure-password-123", usb_id, max_attempts=5)
print("File encrypted and original securely deleted.")

Example 2: Decrypt with brute-force protection

import avgvsto

usb_id = avgvsto.load_usb_id()
if not usb_id:
    raise SystemExit("No USB binding found. Run bind-usb first.")

# Check cooldown
if avgvsto.brute_remaining() > 0:
    print(f"Cooldown active. Wait {avgvsto.brute_remaining():.0f}s.")
    exit(1)

password = input("Password: ")
try:
    result, _ = avgvsto.decrypt_file("will.docx.avgvsto", password, usb_id)
    print(f"Decrypted to {result}")
except ValueError as e:
    avgvsto.brute_mark_fail()  # this starts a cooldown
    print(f"Failed: {e}")

Example 3: Batch encrypt a folder

import avgvsto

usb_id = avgvsto.load_usb_id()
ok, errors = avgvsto.encrypt_folder(
    "./project",
    "s3cret",
    usb_id,
    on_progress=lambda done, total, name: print(f"{done+1}/{total} {name}"),
)
print(f"{ok} files encrypted, {len(errors)} errors")

Example 4: Scan USB for encrypted files

import avgvsto

scan = avgvsto.scan_usb_for_avgvsto("E:\\")
print(f"Found {scan['total']} encrypted files ({scan['corrupted']} corrupted)")
for f in scan["files"]:
    print(f"  {f['path']} — {f['status']}")

Example 5: Use ChaCha20-Poly1305 instead of AES

import avgvsto

usb_id = avgvsto.load_usb_id()
avgvsto.encrypt_file(
    "data.bin", "strong-password", usb_id,
    cipher_id=avgvsto.CIPHER_CHACHA20,
)

Example 6: Create and use a reset password

import avgvsto

usb_id = avgvsto.save_usb_config("E:\\")

# Encrypt with attempt limit
avgvsto.encrypt_file("important.pdf", "mypassword", usb_id, max_attempts=3)

# Create reset password on the USB
avgvsto.create_reset_password("E:\\", "reset-123")

# ... later, after too many wrong passwords ...
success, msg = avgvsto.do_reset_counters("E:\\", "reset-123")
print(msg)  # "3 locked counter(s) cleared.  (Reset 1/3 used)"

Example 7: Full CLI workflow

# 1. Insert USB drive and bind it
avgvsto bind-usb E:\

# 2. Check status
avgvsto status

# 3. Encrypt a folder
avgvsto encrypt ./documents --attempts 5

# 4. Decrypt a single file
avgvsto decrypt documents.avgvsto

# 5. Verify an encrypted file
avgvsto verify documents.avgvsto

API stability

The public API exposed at avgvsto.* level follows semantic versioning. Private functions (prefixed with _) may change between minor versions without notice.

Stable public functions:

Module Functions
avgvsto encrypt_file, decrypt_file, verify_file, encrypt_folder, decrypt_folder, verify_password_against_file
avgvsto.usb list_usb_drives, get_usb_identifier, save_usb_config, load_usb_id, find_authorized_usb
avgvsto.attempts get_attempt_count, increment_attempt_count, reset_attempt_count, brute_remaining, brute_mark_fail, brute_mark_success, create_reset_password, do_reset_counters, can_reset, has_reset_password, full_clear_usb_reset
avgvsto.stats stats_inc_encrypt, stats_inc_decrypt
avgvsto.utils scan_usb_for_avgvsto, get_locked_attempt_files, secure_delete

License

GNU General Public License v3.0 or later — see LICENSE.

Copyright (c) 2026 Roy Merlo & RPX. Open source, auditable, sovereign.

Metadata

Release files for avgvstousb 4.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for avgvstousb 4.0.1
File Size Uploaded Provenance
avgvstousb-4.0.1.tar.gz 16.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for avgvstousb 4.0.1
File Interpreter ABI Platform
avgvstousb-4.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 36.2 kB

Release files / avgvstousb-4.0.1.tar.gz

Download URL avgvstousb-4.0.1.tar.gz
Size 16.3 kB
Tags Source
SHA-256 checksum
How to use checksums
1c167c1f283c7628b6bd2148e30cb2c31946f42e60e1fe0ab6557ed59c5c3f19
BLAKE2b-256 checksum
How to use checksums
e6fd06df33bb1c2203cab0606c6ee321549ae4619994dc281cb633bed15a505e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Release files / avgvstousb-4.0.1-py3-none-any.whl

Download URL avgvstousb-4.0.1-py3-none-any.whl
Size 19.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cdb39c553dbb74ae60787438dc2831afaf95d260ef98639c32e26f6675924fd5
BLAKE2b-256 checksum
How to use checksums
1a7a5d15c77542fecceb78f00cbfb744aa2a252ac26661f6d6e72a6186660030
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.1

Security for avgvstousb 4.0.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

4.0.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page