Files
MachinePainting 4f85fcadfa v1.1.0: Security hardening parity with DropSend (untested)
Mirrors the security architecture of ComfyUI_DropSendNode v1.1.x to close
the same vulnerability classes. The actual Google OAuth grant path is
unchanged from v1.0.1 (same Flow.from_client_config, same OOB redirect,
same fetch_token call), so existing users' OAuth setup continues to work.
What changed is the wrapper around it. Note: this release has not been
runtime-tested end to end; the security logic is a direct port of the
tested DropSend implementation.

Setup Node refactor:
- Removed google_client_id, google_client_secret, auth_code, and the
  service-account JSON from INPUT_TYPES. They no longer enter
  PromptServer.history, workflow JSON, PNG metadata, localStorage, or
  copy-pasted nodes.
- Added a browser-only modal (web/drivesend_oauth.js) and a same-origin
  POST route /drivesend/setup/stash for entry. Hardening: origin check,
  live-WebSocket gate, per-IP rate limit, 32 KB body cap, JSON content
  type lockdown, 60-second TTL, 32-entry capacity, lock-protected access.
- Added COMFYUI_DRIVESEND_ALLOW_SETUP=1 opt-in gate. Without it the node
  refuses to write or clear credentials, blocking remote workflow
  submitters from hijacking or wiping the operator's Drive connection.
- Removed plaintext stdout prints of GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET
  / GOOGLE_REFRESH_TOKEN / COMFYUI_ENCRYPTION_KEY. Replaced with a
  redacted (last-4-chars) banner.
- WebSocket credential delivery is sid-targeted, never broadcast. Refuses
  to send when client_id is None and surfaces a clear error. JS handler
  verifies client_id matches api.clientId before rendering.
- Service-account JSON is entered via a textarea in the same modal,
  base64-encoded into GOOGLE_SERVICE_ACCOUNT_JSON for delivery.

File-system hardening:
- .env and token.json now created via os.open(O_WRONLY|O_CREAT|O_TRUNC,
  0o600), race-free, instead of open('w') with default umask (0o644).
- Added safe_paths.py: clamps watch_folder to ComfyUI's output tree
  (with COMFYUI_DRIVESEND_ALLOWED_WATCH_PATHS opt-in for additional
  roots), rejects symlinks in event paths, validates Drive folder IDs.
- AutoUploader now runs every event path through safe_paths.is_safe_event_path.

Logging:
- Centralized logging in __init__.py with race-free 0o600 creation of
  drivesend.log on the root logger. Removed module-level
  logging.basicConfig duplicates from monitor_output.py, encrypt_file.py,
  drivesend_uploader_node.py.
- Verified: no logger or print call in the codebase emits an access
  token, refresh token, client secret, service-account JSON body, or
  encryption key value.

.gitignore additions:
- token.json, client_secret.json, service_account.json (Google credential
  caches, previously not ignored).
- Editor backups: *.bak, *.backup, *~, *.swp, *.swo, .idea/.
- Encrypted artifacts: *.enc.
- Stray credential material: *.key, *.pem, *.p12, *.pfx.
- .claude/, .pytest_cache/, .coverage, htmlcov/, build artifacts,
  Thumbs.db, Desktop.ini.

README rewrite:
- Linear three-step structure (Google Cloud setup, Setup Node, AutoUploader).
- Cloud / Local / Service-Account sections for the Setup Node.
- Threat model section documenting all enforced protections.
- "Setup is disabled" and "Browser delivery refused" troubleshooting.
- No em dashes per project style.

Tests:
- tests/test_security.py with five regression tests covering: send_sync
  is never called with sid=None for credential events; payload echoes
  client_id for JS-side verification; .env file mode is exactly 0o600;
  client_id pattern rejects malformed input; stash entries expire after
  TTL. Tests run without google-auth, googleapiclient, or watchdog
  installed via ghost modules.

Version: 1.0.1 -> 1.1.0
2026-05-09 07:52:42 -07:00

121 lines
4.7 KiB
Python

# encrypt_file.py
# DriveSend encryption module - creates .enc copies of files (preserves originals for ComfyUI)
import os
import time
import logging
from queue import Queue
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from cryptography.fernet import Fernet
import threading
# logging is configured centrally in __init__.py via a FileHandler on
# the root logger.
logger = logging.getLogger(__name__)
ENCRYPT_EXTENSIONS = ('.png', '.jpg', '.jpeg', '.webp', '.gif', '.mp4', '.avi', '.mov')
_stop_queue_processor = False # Stop signal for queue processor
def get_encryption_key():
"""
Get the encryption key from environment variable.
Checks multiple possible key names for compatibility.
Key retrieval order:
- COMFYUI_ENCRYPTION_KEY (uppercase - preferred)
- comfyui_encryption_key (lowercase - legacy)
- DROPSEND_ENCRYPTION_KEY
- DRIVESEND_ENCRYPTION_KEY
- RUNPOD_SECRET_COMFYUI_ENCRYPTION_KEY (RunPod fallback)
Returns:
str: The encryption key, or None if not set
"""
return (
os.environ.get('COMFYUI_ENCRYPTION_KEY') or
os.environ.get('comfyui_encryption_key') or
os.environ.get('DROPSEND_ENCRYPTION_KEY') or
os.environ.get('DRIVESEND_ENCRYPTION_KEY') or
os.environ.get('RUNPOD_SECRET_COMFYUI_ENCRYPTION_KEY')
)
def stop_queue_processor():
"""Signal the queue processor to stop."""
global _stop_queue_processor
_stop_queue_processor = True
logger.info("[DriveSend] Signaled encryption queue processor to stop")
def reset_queue_processor():
"""Reset the stop signal for queue processor."""
global _stop_queue_processor
_stop_queue_processor = False
class FileEncryptHandler(FileSystemEventHandler):
"""
Watches for new image/video files and creates encrypted .enc copies.
Original files are preserved so ComfyUI maintains its sequential naming.
"""
def __init__(self, watch_dir="/workspace/ComfyUI/output", delete_original=False, subfolder_monitor=True):
self.watch_dir = watch_dir
self.delete_original = delete_original
self.subfolder_monitor = subfolder_monitor
self.file_queue = Queue()
self.start_queue_processor()
def on_created(self, event):
if event.is_directory or not event.src_path.lower().endswith(ENCRYPT_EXTENSIONS):
return
from .safe_paths import is_safe_event_path
if not is_safe_event_path(event.src_path):
logger.warning(f"[DriveSend] Skipping unsafe path (symlink or outside output root): {event.src_path}")
return
logger.info(f"[DriveSend] Detected new file to encrypt: {event.src_path}")
self.file_queue.put(event.src_path)
def start_queue_processor(self):
def process_queue():
while not _stop_queue_processor:
if not self.file_queue.empty():
file_path = self.file_queue.get()
try:
time.sleep(1) # Ensure file is fully written
size1 = os.path.getsize(file_path)
time.sleep(0.1)
size2 = os.path.getsize(file_path)
if size1 != size2:
logger.warning(f"[DriveSend] File {file_path} still writing, requeueing...")
self.file_queue.put(file_path)
continue
ENCRYPT_KEY = get_encryption_key()
if not ENCRYPT_KEY:
logger.error("[DriveSend] Encryption key not found. Set COMFYUI_ENCRYPTION_KEY environment variable.")
continue
fernet = Fernet(ENCRYPT_KEY.encode())
with open(file_path, 'rb') as f:
file_data = f.read()
encrypted_data = fernet.encrypt(file_data)
enc_path = file_path + '.enc'
with open(enc_path, 'wb') as f:
f.write(encrypted_data)
logger.info(f"[DriveSend] Created encrypted file: {enc_path}")
# Original file is kept so ComfyUI maintains sequential naming
# User must manually delete originals from output folder
except Exception as e:
logger.error(f"[DriveSend] Error encrypting {file_path}: {e}")
time.sleep(0.1) # Small delay to avoid CPU overload
logger.info("[DriveSend] Encryption queue processor stopped")
threading.Thread(target=process_queue, daemon=True).start()