Nodes/ComfyUI-CustomNodePacks/C2C Vault — Locked (password to run)
ComfyUI Node

C2C Vault — Locked (password to run)

Hand someone a workflow they can run but never read

By Code2Collapse·Created 7 months ago·Updated 6 days ago· 56
C2C Vault — Locked (password to run)
  • input_0
  • input_1
  • input_2
  • input_3
  • input_4
  • input_5
  • input_6
  • input_7
  • input_8
  • input_9
  • param_0
  • param_1
  • param_2
  • param_3
  • param_4
  • param_5
  • param_6
  • param_7
  • output_0
  • output_1
  • output_2
  • output_3
  • output_4
  • output_5
  • output_6
  • output_7
◄vault_id►
◄vault_payload►
◄vault_interface{}►

ComfyUI's whole sharing culture runs on "workflow included" - every PNG you save carries the full node graph in its metadata, and that's usually the point. It stops being the point the moment you hand a paying client a finished pipeline and they can read your entire recipe, or you want to send something runnable without it being copy-pasteable into someone's own canvas. C2C Vault - Locked (from the big Code2Collapse/ComfyUI-CustomNodePacks suite) is the ecosystem answer to that second case: you fold a selection of nodes into one opaque node whose wiring travels as AES-GCM ciphertext, and it refuses to run until someone enters the password.

No API, no key, no cloud involved - this all happens on your machine. To get it, install the parent pack the usual way: ComfyUI Manager → search CustomNodePacks, or cd ComfyUI/custom_nodes && git clone https://github.com/Code2Collapse/ComfyUI-CustomNodePacks.git, then restart ComfyUI. It needs the cryptography package and a password you type once per session.

How it works

You start with the nodes you want to protect on the canvas. Select them, then right-click the canvas itself and pick C2C: Lock selection into a Vault (password to RUN). Set a password, and the server serializes those nodes into a little "subgraph" JSON - nodes, links, and the wires that cross the boundary - encrypts it with AES-256-GCM, and drops a fresh C2C_VaultLocked node into your graph where the selection sat. The originals stay on the canvas - delete them once you've verified the vault runs, or the plaintext graph sits right next to the vault you built to hide it.

Two design choices are worth understanding, because they're why this isn't theater. First, the password is never a widget value - a widget gets serialized into the very workflow JSON the vault exists to protect. It's POSTed to an HTTP route, stretched through PBKDF2-HMAC-SHA256 at 600,000 iterations, and only the derived key is kept in server memory for the session. Second, at run time the node decrypts the subgraph and executes it with its own tiny topological executor instead of ComfyUI's - ComfyUI's would emit per-node progress bars that name your internals. You see one output and no hint of what's inside.

That AES-GCM setup is tighter than it looks. The header - vault id, KDF parameters, and a hash of the vault's input/output sockets - is authenticated alongside the ciphertext, so a wrong password, a flipped byte, or a payload swapped into a different vault all fail identically.

The inputs that matter

  • vault_id - a plaintext label for which unlocked session this node may use. Not secret; the UI fills it for you.
  • vault_payload - the base64 ciphertext that rides along in the workflow. Never edit it by hand; the UI collapses it out of sight.
  • input_0 / input_1 / input_2 - up to three wires can cross into a vault. Try to lock a selection that needs more and the UI refuses; select upstream nodes too.
  • output - a single wildcard socket. If your selection produces several outputs, you get a warning and only the first is wired through.

Be honest about what this buys you

The author is refreshingly straight about the scope: it's a lock on a door, not a safe. The subgraph has to exist as plaintext in memory to run, so anyone who can execute Python in that process - attach a debugger, read memory, edit the pack - can recover it. What it genuinely stops: someone opening your saved PNG or .json and reading the wiring, pasting your nodes into their own workflow, and casual copying. That's most of what you'd actually fear when sharing a workflow. Nothing that must eventually execute what it protects can stop a determined attacker.

Common issues

  • "Vault locked…" at queue time - you need to click the Unlock… button on the node and type the password first. The session lasts a working day by default (480 min); if it dies mid-render, set C2C_VAULT_TTL_MINUTES higher (0 = until ComfyUI restarts).
  • Five wrong guesses earns a five-minute lockout - per vault, not per IP, because the attacker already has the file.
  • Lost password means lost vault. No recovery path, by design. Store it.
  • Recipient missing a node type inside the vault - after a successful unlock the error helpfully lists which packs to install. Before unlock it says nothing, on purpose.
  • cryptography not installed - the node still loads (so workflows don't break), but locking and unlocking error with the pip hint. Install it into ComfyUI's Python: <ComfyUI python> -m pip install cryptography.

If the recipient should run it freely without ever typing a password - you just don't want them reading it - meet the sibling C2C Vault - Sealed node instead.

CategoryC2C/Vault

Inputs (21)

NameTypeDefaultDescription
vault_idSTRINGPublic vault handle — authenticated in the ciphertext header, never secret. Lets the UI match an unlock session to the right blob without revealing what is inside.
vault_payloadSTRINGAES-GCM ciphertext of the internal graph. This is what travels in the .json — opaque to anyone without the password. Do not hand-edit.
vault_interfaceSTRING{}Clear-text manifest: socket names, types, node count, and any promoted parameters. This is the public API of the vault and it travels in the .json, so it carries a promoted parameter's LABEL and value only - which internal widget it drives is inside the ciphertext, because naming it here would describe the contents.
input_0opt*Positional boundary input 0 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[0] inside the ciphertext.
input_1opt*Positional boundary input 1 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[1] inside the ciphertext.
input_2opt*Positional boundary input 2 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[2] inside the ciphertext.
input_3opt*Positional boundary input 3 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[3] inside the ciphertext.
input_4opt*Positional boundary input 4 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[4] inside the ciphertext.
input_5opt*Positional boundary input 5 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[5] inside the ciphertext.
input_6opt*Positional boundary input 6 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[6] inside the ciphertext.
input_7opt*Positional boundary input 7 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[7] inside the ciphertext.
input_8opt*Positional boundary input 8 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[8] inside the ciphertext.
input_9opt*Positional boundary input 9 — the label you see is applied by the vault interface manifest, not by this socket name. Index must match boundary_in[9] inside the ciphertext.
param_0opt*Promoted parameter 0 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_1opt*Promoted parameter 1 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_2opt*Promoted parameter 2 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_3opt*Promoted parameter 3 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_4opt*Promoted parameter 4 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_5opt*Promoted parameter 5 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_6opt*Promoted parameter 6 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.
param_7opt*Promoted parameter 7 — connect this only when a promoted parameter has been converted to an input. The vault decides which internal widget it drives; that mapping lives inside the ciphertext and is never in the workflow JSON.

Outputs (8)

NameTypeDescription
output_0*—
output_1*—
output_2*—
output_3*—
output_4*—
output_5*—
output_6*—
output_7*—