Skip to main content

Hardware-bound AES-256-GCM encryption with USB key enforcement

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

avgvstousb-4.0.1.tar.gz (16.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

avgvstousb-4.0.1-py3-none-any.whl (19.9 kB view details)

Uploaded Python 3

File details

Details for the file avgvstousb-4.0.1.tar.gz.

File metadata

  • Download URL: avgvstousb-4.0.1.tar.gz
  • Upload date:
  • Size: 16.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for avgvstousb-4.0.1.tar.gz
Algorithm Hash digest
SHA256 1c167c1f283c7628b6bd2148e30cb2c31946f42e60e1fe0ab6557ed59c5c3f19
MD5 9cb119f44b9644a19fc5b7b46d5a3175
BLAKE2b-256 e6fd06df33bb1c2203cab0606c6ee321549ae4619994dc281cb633bed15a505e

See more details on using hashes here.

File details

Details for the file avgvstousb-4.0.1-py3-none-any.whl.

File metadata

  • Download URL: avgvstousb-4.0.1-py3-none-any.whl
  • Upload date:
  • Size: 19.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.1

File hashes

Hashes for avgvstousb-4.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cdb39c553dbb74ae60787438dc2831afaf95d260ef98639c32e26f6675924fd5
MD5 815ac01a9102712c8ead7a25ba512e7b
BLAKE2b-256 1a7a5d15c77542fecceb78f00cbfb744aa2a252ac26661f6d6e72a6186660030

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page