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
121 lines
4.7 KiB
Python
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()
|
|
|
|
|