Skip to main content

How can I use industry standard cryptographic functions in workflows

· 6 min read
Jeremy Scott
Co-founder

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).

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: public or secret.
  • 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, type public)
  • Sender secret key (purpose authenticated, type secret)
  • 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/addSystemError and check isErrored() (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 hash to 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.