Metadata-Version: 2.4
Name: avgvstousb
Version: 4.0.1
Summary: Hardware-bound AES-256-GCM encryption with USB key enforcement
Project-URL: Homepage, https://github.com/Rp-ics/avgvstousb
Project-URL: Source, https://github.com/Rp-ics/avgvstousb
Project-URL: Issues, https://github.com/Rp-ics/avgvstousb/issues
Author: Roy Merlo, RPX
License: GPL-3.0-or-later
License-File: LICENSE
Keywords: aes,encryption,privacy,security,usb
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.9
Requires-Dist: psutil>=5.9
Requires-Dist: pycryptodome>=3.20
Description-Content-Type: text/markdown

# AVGVSTO USB

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

[![PyPI version](https://img.shields.io/pypi/v/avgvstousb)](https://pypi.org/project/avgvstousb/)
[![Python versions](https://img.shields.io/pypi/pyversions/avgvstousb)](https://pypi.org/project/avgvstousb/)
[![License](https://img.shields.io/pypi/l/avgvstousb)](https://www.gnu.org/licenses/gpl-3.0.txt)

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

```bash
pip install avgvstousb
```

```python
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?](#why-avgvsto)
- [Installation](#installation)
- [Security model](#security-model)
- [Library API reference](#library-api-reference)
  - [Encryption & decryption](#encryption--decryption)
  - [USB management](#usb-management)
  - [Attempt tracking & brute-force protection](#attempt-tracking--brute-force-protection)
  - [Duress / plausible deniability](#duress--plausible-denity)
  - [Reset password system](#reset-password-system)
  - [Utilities](#utilities)
- [CLI reference](#cli-reference)
- [Full examples](#full-examples)
- [API stability](#api-stability)
- [License](#license)

---

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

```bash
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

```bash
avgvsto status
```

```text
[--] 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.

```python
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)`.

```python
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)`.

```python
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)`.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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`.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
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.

```python
# 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.

```python
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.

```python
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:**

```python
{
    "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.

```python
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`.

```python
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

```bash
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.

```bash
avgvsto encrypt report.pdf --usb E:\ --attempts 5
```

```text
Encryption password: ********
Confirm password:    ********
[OK] Encrypted -> report.pdf.avgvsto
```

With duress mode:

```bash
avgvsto encrypt secret.docx --usb E:\ --attempts 3 --duress
```

```text
Encryption password: ********
Confirm password:    ********
Duress password:     ********
Confirm duress:      ********
[OK] Encrypted -> secret.docx.avgvsto
```

---

#### `decrypt`

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

```bash
avgvsto decrypt report.pdf.avgvsto --usb E:\
```

```text
Decryption password: ********
[OK] Decrypted -> report.pdf
```

---

#### `verify`

Check file integrity without writing anything to disk.

```bash
avgvsto verify report.pdf.avgvsto
```

```text
Password: ********
[OK] [OK] Integrity OK  ·  Format v1  ·  AES-256-GCM  ·  Attempts limit: 5  ·  Payload: 1.2 MB
```

---

#### `status`

Display USB binding status and statistics.

```bash
avgvsto status
```

```text
[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.

```bash
avgvsto bind-usb E:\
```

```text
[OK] USB bound — ID: 3a8f2cd1e4b9...
```

---

## Full examples

### Example 1: Encrypt a document with attempt limit

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```bash
# 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](LICENSE).

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