Refactor core components and enhance encryption with AES-GCM - #63
Refactor core components and enhance encryption with AES-GCM#63Cyber-Syntax wants to merge 16 commits into
Conversation
- BaseCryptoManager: instantiate PasswordContext once and assign its internal attributes; remove direct hashlib usage and the _calculate_sha256 unused method - config: parse dirs_to_backup as newline-delimited lines from INI, not comma-separated; remove stray comment - main: drop explicit type: Any hints when loading CLI app module
Introduce _safe_date_key to guard date extraction during pruning; unrecognised filenames are logged at DEBUG and treated as datetime.min to avoid affecting retention logic. Use this key for sorting so invalid names don't slip into the kept set. Document that keep_count=0 deletes all matching files.
- add _validate_input_file in DecryptManager to enforce a minimum encrypted size (44 bytes) required for PBKDF2 salt, nonce and tag - implement streaming encryption and decryption using AES-256-GCM with 64 KiB chunks and a single salt at the start - derive key via PBKDF2 and use per-chunk nonces - switch exception handling to Python 3 style (except OSError) - remove the calculate_sha256 test from tests BREAKING CHANGE: switch encryption format to streaming AES-GCM with per-chunk nonces; existing data from previous versions cannot be decrypted
Implement safe backup write by writing to a temporary file and atomically replacing the final archive to avoid TOCTOU issues and partial files. Open tarball on the temp path, then replace with the final path on success. Add prompt to overwrite existing backups and delete the old file when confirmed. Cleanup partial temp files on failure and remove legacy overwrite helpers.
Introduce a cross-process lock via a .lock file and atomic write of metadata.json to prevent corruption during concurrent updates. Add helpers _file_lock, _write_metadata, and _update_metadata and adapt update_backup_metadata, update_encrypted_hash, and update_decrypted_hash to use the new locking path.
Moved get_password, progress_bar, size_calculator from utils to core for better code structure maintability
There was a problem hiding this comment.
12 issues found across 34 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/autotarcompress/extract_manager.py">
<violation number="1" location="src/autotarcompress/extract_manager.py:111">
P1: The pv path is hardcoded to `/usr/bin/pv`, but `is_pv_available()` uses `shutil.which("pv")` (utils.py:120), which finds pv anywhere on PATH. When pv is installed outside `/usr/bin` (e.g. `/usr/local/bin` or `/bin`), `is_pv_available()` returns True yet `_extract_with_pv` raises FileNotFoundError. That is caught by `except OSError`, which returns False — so the whole extraction fails instead of falling back to `_extract_without_pv`. Same applies to the `/usr/bin/tar` invocation below it on systems where tar is not at that path. Resolve both binaries from PATH (consistent with `is_pv_available`) instead of hardcoding.</violation>
<violation number="2" location="src/autotarcompress/extract_manager.py:115">
P2: `tar_proc` is never read after this assignment, so Ruff's F841 check rejects the file. Call `subprocess.run(...)` directly instead of assigning its result.</violation>
<violation number="3" location="src/autotarcompress/extract_manager.py:160">
P1: When a traversal member appears after a valid member, this loop writes the valid member before detecting the unsafe path and returning `False`. Validate all members before calling `tar.extract` so path validation failure cannot leave partial extracted contents.</violation>
<violation number="4" location="src/autotarcompress/extract_manager.py:164">
P2: `startswith(extract_dir.resolve())` has no path-separator boundary, so a member such as `../extract_secret/file` resolves to a sibling path that still starts with the extract dir string and passes the check. Actual protection now comes only from `tar.extract(..., filter="data")`, which makes this flawed manual check redundant. Prefer relying on `filter="data"` alone (or compare with `is_relative_to`), and remove the misleading manual check.</violation>
</file>
<file name="src/autotarcompress/metadata.py">
<violation number="1" location="src/autotarcompress/metadata.py:128">
P3: New helpers are missing required type hints: `_file_lock` has no return annotation, and `_update_metadata`'s `updater` parameter is untyped. AGENTS.md requires full type annotations and return types on all functions, and mypy strict (`disallow_untyped_defs`) would flag these. Type both with a `Generator`/`Iterator[None, None, None]` return for the contextmanager and a `Callable[[BackupMetadata], None]` for `updater`.</violation>
<violation number="2" location="src/autotarcompress/metadata.py:152">
P1: Concurrent metadata writers can bypass this lock when one process unlinks the lock path after releasing it. Keep a stable `.lock` file instead of deleting it so every process flocks the same inode.</violation>
</file>
<file name="src/autotarcompress/utils.py">
<violation number="1" location="src/autotarcompress/utils.py:104">
P2: When `folder` already names a regular file, this check treats it as an ensured directory and returns it. Reject existing non-directories here so backup setup fails before archive creation.</violation>
</file>
<file name="src/autotarcompress/decrypt_manager.py">
<violation number="1" location="src/autotarcompress/decrypt_manager.py:183">
P1: Existing encrypted backups larger than 64 KiB no longer decrypt because this reader assumes the new chunked format. Add legacy-format detection and fallback, or version the on-disk format before switching formats.</violation>
<violation number="2" location="src/autotarcompress/decrypt_manager.py:184">
P1: When a file ends immediately after a nonce, this branch reports successful decryption of a truncated archive. Treat a missing ciphertext block as corruption, clean up the partial output, and return `False`.</violation>
<violation number="3" location="src/autotarcompress/decrypt_manager.py:188">
P1: Each chunk is authenticated independently without its position or stream completion, so deleting or reordering records can produce altered plaintext with valid tags. Authenticate sequence and end-of-stream metadata, or retain one authenticated record, and fail integrity mismatches.</violation>
</file>
<file name="src/autotarcompress/encrypt_manager.py">
<violation number="1" location="src/autotarcompress/encrypt_manager.py:126">
P1: When an encrypted file loses a complete chunk or has chunks reordered, each remaining GCM tag still verifies because the chunk position is not authenticated, and the decryptor accepts EOF as success. Authenticate the chunk sequence and end-of-file state, then reject missing or out-of-order records.</violation>
<violation number="2" location="src/autotarcompress/encrypt_manager.py:128">
P1: Existing `.enc` backups larger than 64 KiB become unreadable because this format change has no version marker or legacy decoding path. Add an explicit format version and retain support for the previous single-record layout, or provide a migration before switching formats.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| for member in members: | ||
| # Security: make sure the member won't extract outside | ||
| # the target directory (path traversal attack prevention). | ||
| target_path = extract_dir / member.name | ||
| if not str(target_path.absolute()).startswith( | ||
| str(extract_dir.absolute()) | ||
| if not str(target_path.resolve()).startswith( | ||
| str(extract_dir.resolve()) | ||
| ): | ||
| self.logger.error( | ||
| "Attempted path traversal: %s", member.name | ||
| "Attempted path traversal detected: %s — " | ||
| "aborting extraction.", | ||
| member.name, | ||
| ) | ||
| return False | ||
|
|
||
| # Extract each member with progress tracking | ||
| for member in tar.getmembers(): | ||
| tar.extract(member, path=extract_dir) | ||
| # Safe — extract this member immediately. | ||
| tar.extract(member, path=extract_dir, filter="data") | ||
| progress.update(member.size) |
There was a problem hiding this comment.
P1: When a traversal member appears after a valid member, this loop writes the valid member before detecting the unsafe path and returning False. Validate all members before calling tar.extract so path validation failure cannot leave partial extracted contents.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/autotarcompress/extract_manager.py, line 160:
<comment>When a traversal member appears after a valid member, this loop writes the valid member before detecting the unsafe path and returning `False`. Validate all members before calling `tar.extract` so path validation failure cannot leave partial extracted contents.</comment>
<file context>
@@ -125,39 +137,48 @@ def _extract_without_pv(
- for member in tar.getmembers():
+ # single loop does both in order.
+ # This is clearer and avoids iterating the member list twice.
+ for member in members:
+ # Security: make sure the member won't extract outside
+ # the target directory (path traversal attack prevention).
</file context>
| for member in members: | |
| # Security: make sure the member won't extract outside | |
| # the target directory (path traversal attack prevention). | |
| target_path = extract_dir / member.name | |
| if not str(target_path.absolute()).startswith( | |
| str(extract_dir.absolute()) | |
| if not str(target_path.resolve()).startswith( | |
| str(extract_dir.resolve()) | |
| ): | |
| self.logger.error( | |
| "Attempted path traversal: %s", member.name | |
| "Attempted path traversal detected: %s — " | |
| "aborting extraction.", | |
| member.name, | |
| ) | |
| return False | |
| # Extract each member with progress tracking | |
| for member in tar.getmembers(): | |
| tar.extract(member, path=extract_dir) | |
| # Safe — extract this member immediately. | |
| tar.extract(member, path=extract_dir, filter="data") | |
| progress.update(member.size) | |
| for member in members: | |
| target_path = extract_dir / member.name | |
| if not str(target_path.resolve()).startswith( | |
| str(extract_dir.resolve()) | |
| ): | |
| self.logger.error( | |
| "Attempted path traversal detected: %s — " | |
| "aborting extraction.", | |
| member.name, | |
| ) | |
| return False | |
| for member in members: | |
| tar.extract(member, path=extract_dir, filter="data") | |
| progress.update(member.size) |
| target_path = extract_dir / member.name | ||
| if not str(target_path.absolute()).startswith( | ||
| str(extract_dir.absolute()) | ||
| if not str(target_path.resolve()).startswith( |
There was a problem hiding this comment.
P2: startswith(extract_dir.resolve()) has no path-separator boundary, so a member such as ../extract_secret/file resolves to a sibling path that still starts with the extract dir string and passes the check. Actual protection now comes only from tar.extract(..., filter="data"), which makes this flawed manual check redundant. Prefer relying on filter="data" alone (or compare with is_relative_to), and remove the misleading manual check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/autotarcompress/extract_manager.py, line 164:
<comment>`startswith(extract_dir.resolve())` has no path-separator boundary, so a member such as `../extract_secret/file` resolves to a sibling path that still starts with the extract dir string and passes the check. Actual protection now comes only from `tar.extract(..., filter="data")`, which makes this flawed manual check redundant. Prefer relying on `filter="data"` alone (or compare with `is_relative_to`), and remove the misleading manual check.</comment>
<file context>
@@ -125,39 +137,48 @@ def _extract_without_pv(
target_path = extract_dir / member.name
- if not str(target_path.absolute()).startswith(
- str(extract_dir.absolute())
+ if not str(target_path.resolve()).startswith(
+ str(extract_dir.resolve())
):
</file context>
|
|
||
|
|
||
| @contextlib.contextmanager | ||
| def _file_lock(path: Path): |
There was a problem hiding this comment.
P3: New helpers are missing required type hints: _file_lock has no return annotation, and _update_metadata's updater parameter is untyped. AGENTS.md requires full type annotations and return types on all functions, and mypy strict (disallow_untyped_defs) would flag these. Type both with a Generator/Iterator[None, None, None] return for the contextmanager and a Callable[[BackupMetadata], None] for updater.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/autotarcompress/metadata.py, line 128:
<comment>New helpers are missing required type hints: `_file_lock` has no return annotation, and `_update_metadata`'s `updater` parameter is untyped. AGENTS.md requires full type annotations and return types on all functions, and mypy strict (`disallow_untyped_defs`) would flag these. Type both with a `Generator`/`Iterator[None, None, None]` return for the contextmanager and a `Callable[[BackupMetadata], None]` for `updater`.</comment>
<file context>
@@ -119,55 +124,141 @@ def load_metadata(config_dir: Path) -> BackupMetadata:
+@contextlib.contextmanager
+def _file_lock(path: Path):
+ """Acquire an exclusive cross-process file lock.
+
</file context>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This pull request introduces several improvements and refactorings to the autotarcompress codebase, focusing on safer backup handling, improved error management, and code organization. The most significant changes include atomic backup file creation to prevent corruption, enhanced user prompts for overwriting backups, improved configuration parsing, and better handling of edge cases during cleanup and decryption. Imports and utility usage have also been streamlined for clarity and maintainability.
Backup process improvements:
src/autotarcompress/backup_manager.py) [1] [2]src/autotarcompress/backup_manager.py)Configuration and parsing fixes:
dirs_to_backupas newline-delimited entries rather than comma-separated, fixing issues with misparsed backup directories. (src/autotarcompress/config.py)Cleanup and error handling:
src/autotarcompress/cleanup_manager.py)src/autotarcompress/decrypt_manager.py)Code organization and import refactoring:
progress_bar,size_calculator,get_password). TYPE_CHECKING blocks are used to avoid unnecessary runtime imports in CLI commands. (src/autotarcompress/backup_manager.py,src/autotarcompress/base_manager.py,src/autotarcompress/cli/commands/*,src/autotarcompress/decrypt_manager.py) [1] [2] [3] [4] [5] [6] [7] [8] [9]These changes collectively make the backup process safer, improve user experience, and enhance the maintainability and reliability of the codebase.