Files
IxMxAMAR 15e4b0da4e release(v0.1.2): fix critical loader collision + 6 Gemini-review findings
Critical:
- rename nodes/ -> mp_nodes/, shared/ -> mp_shared/ to avoid shadowing
  ComfyUI core's nodes.py via sys.path injection (root cause of v0.1.1
  'nodes is not a package' startup failure)
- http_post now streams response and enforces 50MB cap mid-stream (DoS)
- AES key derivation: HKDF -> PBKDF2-HMAC-SHA256 600k iters + random salt
  (work factor against weak-key brute force; ciphertext format changed)
- fs_mkdir/copy/move/delete confined to ComfyUI input/output/temp + ~ by
  default; UTILITY_MEGAPACK_ALLOW_ARBITRARY_PATHS=1 to override

UX / correctness:
- combo settings now declare options array (themes dropdown was empty)
- LATENT default 1x4x8x8 zeros -> {} (avoids Flux/SD3 shape crashes)
- @op decorator rejects reserved widget names mode/theme at register time
- _pad_outputs docstring updated to match relaxed opaque-type behavior
2026-05-13 21:36:54 +05:30

164 lines
4.8 KiB
Python

"""Cryptographic operations: hashes, HMAC, AES-GCM."""
import base64
import hashlib
import hmac as _hmac
import os
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from .. import op
# PBKDF2 work factor — 600k iterations is the OWASP 2023 recommendation for
# SHA-256-based password derivation. Increases the cost of a dictionary
# attack against user-entered passwords substantially.
# v0.1.2 (Gemini review #5) replaces the old HKDF-with-static-salt approach,
# which had no work factor and was vulnerable to brute-force on weak keys.
_PBKDF2_ITERATIONS = 600_000
_SALT_BYTES = 16
def _derive_aes_key(key_str: str, salt: bytes) -> bytes:
"""Derive a 32-byte AES key from an arbitrary-length string via PBKDF2-HMAC-SHA256."""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=_PBKDF2_ITERATIONS,
)
return kdf.derive(key_str.encode("utf-8"))
def _hash_hex(text: str, name: str) -> str:
h = hashlib.new(name)
h.update(text.encode("utf-8"))
return h.hexdigest()
@op(
op_id="hash_sha256",
display_name="Hash SHA-256",
category="Crypto",
input_schema={"required": {"text": ("STRING", {"default": ""})}},
output_indices=(0,),
)
def hash_sha256(self, text):
return (_hash_hex(text, "sha256"),)
@op(
op_id="hash_sha512",
display_name="Hash SHA-512",
category="Crypto",
input_schema={"required": {"text": ("STRING", {"default": ""})}},
output_indices=(0,),
)
def hash_sha512(self, text):
return (_hash_hex(text, "sha512"),)
@op(
op_id="hash_md5",
display_name="Hash MD5 (legacy)",
category="Crypto",
input_schema={"required": {"text": ("STRING", {"default": ""})}},
output_indices=(0,),
description="Legacy / non-cryptographic. Use SHA-256 for security-relevant work.",
)
def hash_md5(self, text):
return (_hash_hex(text, "md5"),)
@op(
op_id="hash_blake2b",
display_name="Hash BLAKE2b",
category="Crypto",
input_schema={"required": {"text": ("STRING", {"default": ""})}},
output_indices=(0,),
)
def hash_blake2b(self, text):
return (_hash_hex(text, "blake2b"),)
@op(
op_id="hmac_sign",
display_name="HMAC Sign (SHA-256)",
category="Crypto",
input_schema={"required": {
"text": ("STRING", {"default": ""}),
"key": ("STRING", {"default": ""}),
}},
output_indices=(0,),
)
def hmac_sign(self, text, key):
sig = _hmac.new(key.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest()
return (sig,)
@op(
op_id="hmac_verify",
display_name="HMAC Verify",
category="Crypto",
input_schema={"required": {
"text": ("STRING", {"default": ""}),
"key": ("STRING", {"default": ""}),
"expected_hex": ("STRING", {"default": ""}),
}},
output_indices=(3,),
description="Constant-time compare of HMAC-SHA256(text, key) vs expected_hex.",
)
def hmac_verify(self, text, key, expected_hex):
actual = _hmac.new(key.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest()
return (_hmac.compare_digest(actual, expected_hex),)
@op(
op_id="aes_encrypt",
display_name="AES-GCM Encrypt",
category="Crypto",
input_schema={"required": {
"plaintext": ("STRING", {"default": ""}),
"key": ("STRING", {"default": ""}),
}},
output_indices=(0,),
description=(
"AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation (600k iters). "
"Returns base64(salt[16] || nonce[12] || ciphertext_with_tag). "
"NOTE: ciphertexts from v0.1.0/0.1.1 (HKDF-based) cannot be decrypted by v0.1.2+."
),
)
def aes_encrypt(self, plaintext, key):
salt = os.urandom(_SALT_BYTES)
aes = AESGCM(_derive_aes_key(key, salt))
nonce = os.urandom(12)
ct = aes.encrypt(nonce, plaintext.encode("utf-8"), None)
return (base64.b64encode(salt + nonce + ct).decode("ascii"),)
@op(
op_id="aes_decrypt",
display_name="AES-GCM Decrypt",
category="Crypto",
input_schema={"required": {
"ciphertext_b64": ("STRING", {"default": ""}),
"key": ("STRING", {"default": ""}),
}},
output_indices=(0,),
description="Inverse of aes_encrypt. Raises on tag mismatch or malformed input.",
)
def aes_decrypt(self, ciphertext_b64, key):
blob = base64.b64decode(ciphertext_b64)
min_len = _SALT_BYTES + 12 + 16 # salt + nonce + GCM tag minimum
if len(blob) < min_len:
raise ValueError(
f"ciphertext too short for AES-GCM with PBKDF2 (need >= {min_len} bytes, got {len(blob)})"
)
salt = blob[:_SALT_BYTES]
nonce = blob[_SALT_BYTES:_SALT_BYTES + 12]
ct = blob[_SALT_BYTES + 12:]
aes = AESGCM(_derive_aes_key(key, salt))
return (aes.decrypt(nonce, ct, None).decode("utf-8"),)