How can I use industry standard cryptographic functions in workflows
The goal: make signing, encryption, hashing, and random bytes straightforward without leaving JavaScript processors. Version 3.1.0 of opscotch introduces a CryptoContext that brings libsodium-grade primitives into workflows. It works with ByteContext, so everything is buffers (not strings).
- Everything is buffers inside the agent core: you're just passing byte handles to and from functions.
- Keys must be registered (or generated) for a specific purpose before use
- The agent owns key material and buffer memory; you manipulate handles.
- Docs: /docs/next/apireference#CryptoContext, /docs/next/apireference#ByteContext, /docs/next/apireference#ByteReader.
- New to buffers? See Byte manipulation in workflows for a primer on
ByteContextandByteReader.
Supported capabilities:
- Public key signatures (Ed25519):
sign,verifySignature - Authenticated public key encryption (X25519 + XSalsa20 + Poly1305):
encryptPublicKey,decryptPublicKey - Anonymous public key encryption (X25519 + XSalsa20):
encryptAnonymous,decryptAnonymous - Symmetric encryption (XSalsa20):
encryptSymmetric,decryptSymmetric - Hashing:
hash - Random bytes:
randomBytes - Key lifecycle:
generateKeyPair,registerKey,validateKey
Which function should I use?
If you need to:
- Prove who sent a message and that it was not tampered with: use public key signatures (
sign+verifySignature). Think “wax seal on a letter—visible to all, proves origin, doesn’t hide contents." - Send a message so only the recipient can read it and the recipient knows it came from you: use authenticated public key encryption (
encryptPublicKey/decryptPublicKey). Think “locked box with your signature on it” between known parties. - Send a message so only the recipient can read it, but the sender stays anonymous: use anonymous public key encryption (
encryptAnonymous/decryptAnonymous). Think "sealed dropbox slot—recipient can open, sender stays unknown". - Encrypt when both parties already share the same secret key and want speed: use symmetric encryption (
encryptSymmetric/decryptSymmetric). Think "shared password" - Produce a fixed, repeatable fingerprint: use hash. Think "unique tag that changes if the data changes".
- _Get high-quality random bytes for nonces, salts, keys: use
randomBytes. Think “good dice rolls.”
If you are unsure:
- Start with symmetric for internal shared-secret use cases; add authenticated public key encryption when you need sender identity.
- Use signatures when you need authenticity without encryption (e.g., signing payloads that flow through untrusted intermediaries).
- Use anonymous encryption when you need confidentiality without revealing sender identity.
Key lifecycle (register, validate, generate)
Keys are registered per purpose and type:
- Purpose:
sign,authenticated,anonymous,symmetric - Type:
public,secret
Length is enforced per algorithm. Register keys in hex:
// Register an Ed25519 signing secret key
const signingSecretId = context.crypto().registerKey("sign", "secret", "ABCDEF...");
// Register the matching public key
const signingPublicId = context.crypto().registerKey("sign", "public", "123456...");
// Validate before use
if (!context.crypto().validateKey(signingPublicId, "sign", "public")) {
context.addSystemError("Missing or invalid signing public key");
}
Generate a new pair (returns [public, secret]):
const [pub, sec] = context.crypto().generateKeyPair("sign"); // Ed25519
const pubId = context.crypto().registerKey("sign", "public", byteContext.binaryToHex(pub));
const secId = context.crypto().registerKey("sign", "secret", byteContext.binaryToHex(sec));
Bootstrap keys (preloaded)
You can also preload keys in the bootstrap so they are available to the agent and workflows at startup. Each key has:
id(required): unique identifier you reference in workflows.purpose:sign(secret 64, public 32),authenticated(secret 32, public 32),symmetric(secret 32),anonymous(secret 32, public 32).type:publicorsecret.keyHex: hex-encoded key material.metadata(optional): for your own management; not loaded into the agent.
Bootstrap keys array example:
[
{
"id": "signing-secret",
"purpose": "sign",
"type": "secret",
"keyHex": "ABCDEF..."
},
{
"id": "signing-public",
"purpose": "sign",
"type": "public",
"keyHex": "123456..."
}
]
Bootstrap keys are only ever used by reference (never readable in workflows). Keys added during a workflow via registerKey(...) live only for that context.
Examples
Integrity only (no secrecy): Public key signatures (Ed25519)
Sign with a secret key, verify with a public key.
let payload = byteContext.createFromString("important message");
let sig = context.crypto().sign(payload, signingSecretId);
let ok = context.crypto().verifySignature(sig, payload, signingPublicId);
if (!ok) context.addSystemError("Signature verification failed");
Shared-secret, high-throughput: Symmetric encryption (XSalsa20)
Requires a 32-byte secret key (register as symmetric, secret) and a 24-byte nonce.
const sharedKeyId = context.crypto().registerKey("symmetric", "secret", "0011..."); // 32-byte hex
let nonce = context.crypto().randomBytes(24);
let plaintext = byteContext.createFromString("shared secret text");
let ciphertext = context.crypto().encryptSymmetric(plaintext, nonce, sharedKeyId);
let restored = context.crypto().decryptSymmetric(ciphertext, nonce, sharedKeyId);
let roundtrip = byteContext.binaryToHex(plaintext) === byteContext.binaryToHex(restored);
Confidential + authenticated: Authenticated public key encryption (X25519 + XSalsa20 + Poly1305)
Both parties are known. You need:
- Recipient public key (purpose
authenticated, typepublic) - Sender secret key (purpose
authenticated, typesecret) - 24-byte nonce
let nonce = context.crypto().randomBytes(24);
let msg = byteContext.createFromString("hello recipient");
let encrypted = context.crypto().encryptPublicKey(msg, nonce, recipientPublicId, senderSecretId);
let decrypted = context.crypto().decryptPublicKey(encrypted, nonce, senderPublicId, recipientSecretId);
Confidential + anonymous: Anonymous public key encryption (X25519 + XSalsa20)
Sender stays anonymous; only recipient can decrypt. Needs recipient public/secret keys (purpose anonymous).
let payload = byteContext.createFromString("one-way encrypted note");
let sealed = context.crypto().encryptAnonymous(payload, recipientPublicId);
let opened = context.crypto().decryptAnonymous(sealed, recipientPublicId, recipientSecretId);
Fingerprints and entropy: Hashing and random bytes
let data = byteContext.createFromString("hash me");
let digest = context.crypto().hash(data);
let salt = context.crypto().randomBytes(16); // cryptographically strong
Working with buffers (ByteContext + Crypto)
Remember everything is a buffer handle. Use ByteContext to move between strings, hex, and bytes:
let payload = byteContext.createFromString("payload");
let payloadHex = byteContext.binaryToHex(payload);
let fromHex = byteContext.hexToBinary(payloadHex);
Error handling
- Key length/purpose/type mismatches throw errors.
- Nonces must be the correct size (24 bytes for the provided primitives).
- Handle errors via
addUserError/addSystemErrorand checkisErrored()(see the error-handling post).
Picking patterns
- Integrity only (no secrecy): sign and verify. Example: sign workflow outputs before handing to another system.
- Confidential + authenticated: authenticated public key encryption (known sender, known recipient). Example: agent-to-agent messages.
- Confidential + anonymous: anonymous public key encryption (known recipient, anonymous sender). Example: ingestion inbox.
- Shared-secret, high-throughput: symmetric encryption. Example: internal queues with a pre-shared key.
- Hashes for fingerprints and IDs: store hashes, not secrets; use
hashto create deterministic tags or integrity checks. - Nonces: always random and unique per encryption; use
randomBytes(24)and store/transmit alongside ciphertext. - Key hygiene: keep purposes/types straight; validate before use; prefer generated keys where possible; keep keys out of logs.
With CryptoContext you can sign, encrypt, hash, and generate random bytes inside workflows using industry-standard algorithms, while keeping key material managed by opscotch and working in buffer handles alongside ByteContext.