This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr Cryptography JavaScript SDK

Encrypt and decrypt data using the Dapr JavaScript SDK

The Dapr JavaScript SDK lets Node.js applications encrypt and decrypt messages and files through a configured cryptography component. Use DaprClient.crypto with either HTTP or gRPC, passing data directly or working with Node.js streams.

These guides target @dapr/dapr version 3.19.0 or later. The underlying Cryptography API is alpha. Prefer gRPC for large payloads and production streaming: Dapr buffers HTTP cryptography output until the request is fully consumed.

To get started, follow the Cryptography how-to guide. See the client usage guidance for configuration, supported inputs, encryption options, stream handling, and limitations.

The client implements encryption and decryption using the Dapr Crypto Scheme v1. It does not expose general-purpose signing, signature verification, hashing, or key-management methods. Provision keys through your component’s backing store; the application passes key references, not key material, to client.crypto.

1 - How to: Encrypt and decrypt data with the Dapr JavaScript SDK

Configure a cryptography component and encrypt and decrypt messages and files using the Dapr JavaScript SDK

Use DaprClient.crypto to encrypt and decrypt data without passing cryptographic keys to your application. This guide uses a local key for development, then shows how to work with buffers and files.

Prerequisites

In your application directory, initialize an npm project if you do not already have one, then install the SDK:

npm init -y
npm install @dapr/dapr@3.19.0

The examples use JavaScript ES modules and run as .mjs files without a TypeScript compiler. The same APIs are available in TypeScript.

Configure a local cryptography component

Create a keys directory and generate a random 256-bit symmetric key:

node -e "const fs = require('node:fs'); fs.mkdirSync('keys', { recursive: true }); fs.writeFileSync(require('node:path').join('keys', 'symmetric256'), require('node:crypto').randomBytes(32));"

Run this command only for initial setup. Rerunning it replaces the key, preventing you from decrypting data encrypted with the previous key unless you retained a copy.

Create a separate components directory and save the following as components\crypto.yaml:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: crypto-local
spec:
  type: crypto.dapr.localstorage
  version: v1
  metadata:
    - name: path
      value: '.\keys'

The path is resolved from the sidecar’s working directory. Run the commands below from your application directory; for other deployments, set the path to a directory accessible to the sidecar.

The component name, crypto-local, is the componentName used by the client. The key name, symmetric256, is the file name in the key directory. This example pairs a 256-bit symmetric key with A256KW key wrapping.

Encrypt and decrypt a message

Save the following as app.mjs:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "3500",
  communicationProtocol: CommunicationProtocolEnum.HTTP,
});

try {
  const plaintext = Buffer.from("A message to encrypt", "utf8");
  const ciphertext = await client.crypto.encrypt(plaintext, {
    componentName: "crypto-local",
    keyName: "symmetric256",
    keyWrapAlgorithm: "A256KW",
  });

  const decrypted = await client.crypto.decrypt(ciphertext, {
    componentName: "crypto-local",
  });

  if (!decrypted.equals(plaintext)) {
    throw new Error("Decrypted bytes do not match the original message");
  }
  console.log("Message round-trip succeeded");
} finally {
  await client.stop();
}

Start the application with a sidecar that loads the component:

dapr run --app-id crypto-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node app.mjs

Passing data and options returns a Promise<Buffer> for both encryption and decryption. Encryption also accepts a string, encoded as UTF-8. Decryption uses the key reference embedded in the encrypted document by default, so this example does not need to supply keyName again.

To run the same example over gRPC, change communicationProtocol to CommunicationProtocolEnum.GRPC and daprPort to "50001".

Encrypt and decrypt files with streams

For files, pass only the options object to obtain a Node.js Duplex. Write plaintext into an encryption stream and read ciphertext from it; a decryption stream accepts ciphertext and emits plaintext.

Create a nonempty plaintext.txt in your application directory and save this example as files.mjs. It uses gRPC, the recommended protocol for large payloads and production streaming:

import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
});

try {
  await pipeline(
    createReadStream("plaintext.txt"),
    await client.crypto.encrypt({
      componentName: "crypto-local",
      keyName: "symmetric256",
      keyWrapAlgorithm: "A256KW",
    }),
    createWriteStream("ciphertext.out"),
  );

  await pipeline(
    createReadStream("ciphertext.out"),
    await client.crypto.decrypt({ componentName: "crypto-local" }),
    createWriteStream("plaintext.out"),
  );

  console.log("Encrypted ciphertext.out and decrypted plaintext.out");
} finally {
  await client.stop();
}

Run it with the same component and ports:

dapr run --app-id crypto-files-example --dapr-http-port 3500 --dapr-grpc-port 50001 --resources-path .\components -- node files.mjs

Await each pipeline so failures propagate and encryption finishes before decryption starts. Use new streams for each operation. Do not combine encryption and decryption in one pipeline on runtimes affected by the concurrent cryptography limitation.

The stream overloads also work over HTTP, but the runtime buffers HTTP output until the entire input has arrived. A stream-shaped API does not remove that runtime memory cost.

Next steps

Read the client usage guidance for choosing ciphers, controlling embedded key references, preserving binary ciphertext, and handling errors. See the cryptography quickstart for additional examples.

2 - Cryptography client usage

Configure and use JavaScript cryptography operations, encryption options, and streams

Cryptography is available through DaprClient.crypto. This page describes @dapr/dapr version 3.19.0 or later, which supports both HTTP and gRPC. Both transports use the alpha Cryptography API.

Client lifetime and configuration

Reuse a long-lived DaprClient rather than creating one for each operation. Await outstanding operations and stream pipelines before calling await client.stop() during application shutdown.

HTTP is the default protocol and uses sidecar port "3500" by default. For gRPC, configure the protocol and its port explicitly:

import { CommunicationProtocolEnum, DaprClient } from "@dapr/dapr";

const client = new DaprClient({
  daprHost: "127.0.0.1",
  daprPort: "50001",
  communicationProtocol: CommunicationProtocolEnum.GRPC,
  daprApiToken: process.env.DAPR_API_TOKEN,
});

Use CommunicationProtocolEnum.HTTP and the sidecar’s HTTP port for HTTP. Port options are strings. If you omit explicit host and port options, the SDK can resolve the selected protocol’s endpoint from DAPR_HTTP_ENDPOINT or DAPR_GRPC_ENDPOINT. The API token defaults to DAPR_API_TOKEN when it is not supplied explicitly. See the JavaScript client configuration for more information.

Choose an overload

The argument count selects whether an operation returns a stream or buffers its output:

MethodInputResult
encrypt(data, options)Buffer, ArrayBuffer, ArrayBufferView, or stringPromise<Buffer> containing ciphertext
decrypt(data, options)Buffer, ArrayBuffer, or ArrayBufferViewPromise<Buffer> containing plaintext
encrypt(options)Write plaintext to the returned streamPromise<Duplex> emitting ciphertext
decrypt(options)Write ciphertext to the returned streamPromise<Duplex> emitting plaintext

An ArrayBufferView includes typed arrays such as Uint8Array and DataView. The SDK uses the view’s byte offset and byte length, not the entire backing buffer.

Encryption converts strings to UTF-8. Decryption expects binary input, not a string. To decrypt ciphertext stored as base64, decode it first:

const decrypted = await client.crypto.decrypt(Buffer.from(savedBase64, "base64"), {
  componentName: "crypto-local",
});

This snippet assumes an existing client and a savedBase64 string previously produced with ciphertext.toString("base64"). Keep ciphertext as bytes unless you deliberately encode it; converting arbitrary ciphertext with .toString("utf8") corrupts it. Convert decrypted bytes to UTF-8 only if the original plaintext was UTF-8 text.

Encryption options

Pass the same encryption options with either overload:

OptionRequiredBehavior
componentNameYesName of the configured Dapr cryptography component, not its component type.
keyNameYesKey reference in the component’s backing store, optionally including a version where the provider supports it.
keyWrapAlgorithmYesAlgorithm used to wrap the data-encryption key. Must be compatible with the referenced key and component.
dataEncryptionCipherNo"aes-gcm" (default) or "chacha20-poly1305" for encrypting the payload.
omitDecryptionKeyNameNoDefaults to false. Set to true to omit the key reference from the encrypted document; decryption must then supply keyName.
decryptionKeyNameNoAlternate key reference to embed in the document. Defaults to the encryption key reference and is ignored when omitDecryptionKeyName is true.

The SDK’s keyWrapAlgorithm type accepts "A256KW", "A128CBC", "A192CBC", "A256CBC", "RSA-OAEP-256", "AES", and "RSA". This list describes the SDK type, not a guarantee that every component, key, or runtime accepts every value. Choose a value supported by your provider; for example, the setup guide uses A256KW with a 256-bit symmetric key. The wrapping algorithm and payload cipher are separate settings.

TypeScript applications can import the request types from the package’s type module:

import type { DecryptRequest, EncryptRequest } from "@dapr/dapr/types/crypto/Requests";

const encryptOptions: EncryptRequest = {
  componentName: "crypto-local",
  keyName: "symmetric256",
  keyWrapAlgorithm: "A256KW",
  dataEncryptionCipher: "chacha20-poly1305",
};

const decryptOptions: DecryptRequest = {
  componentName: "crypto-local",
};

These request types are not exported from the package root in 3.19.0.

Decryption and key references

Decryption takes a required componentName and an optional keyName. The runtime reads the cipher and wrapped key information from the encrypted document; do not pass encryption options such as keyWrapAlgorithm to decrypt.

By default, the document contains the encryption key reference. An explicit decryption keyName overrides that embedded reference. The component must have access to the corresponding decryption key; overriding the name does not make an unrelated key valid.

To keep the key reference out of the document:

const ciphertext = await client.crypto.encrypt("A message to encrypt", {
  componentName: "crypto-local",
  keyName: "symmetric256",
  keyWrapAlgorithm: "A256KW",
  omitDecryptionKeyName: true,
});

const plaintext = await client.crypto.decrypt(ciphertext, {
  componentName: "crypto-local",
  keyName: "symmetric256",
});

Run this snippet with an existing client inside an async function or an ES module. Without an explicit decryption keyName, this document cannot be decrypted.

Use decryptionKeyName instead when the recipient knows the same key under a different reference, such as an alias or a key in another configured component:

const ciphertext = await client.crypto.encrypt("A message for a recipient", {
  componentName: "sender-crypto",
  keyName: "sender-key",
  keyWrapAlgorithm: "RSA",
  decryptionKeyName: "recipient-key",
});

const plaintext = await client.crypto.decrypt(ciphertext, {
  componentName: "recipient-crypto",
});

This example requires separately configured components and a matching RSA key pair: the sender must be able to wrap with the public key, and the recipient must be able to unwrap with the corresponding private key. decryptionKeyName only changes the embedded reference; it does not create, copy, or rotate keys.

Streams and memory use

Calling encrypt(options) or decrypt(options) resolves to a Duplex, not the completed operation. Its writable side receives input; its readable side emits output. Consume the output while supplying input and end the writable side when input is complete. Prefer await pipeline(source, cryptoStream, destination) from node:stream/promises to propagate errors and close participating streams.

The file example demonstrates this pattern. Stream in binary mode when handling ciphertext. Do not set a text encoding on a ciphertext source or treat encrypted output as text.

The data overloads accumulate the complete result in memory, even when gRPC streams internally. Use them only when input and output fit comfortably in memory.

  • HTTP: The SDK supports both overloads and sends raw bytes, but Dapr buffers cryptography output until it has consumed the request. Do not wait for output before finishing input. Prefer gRPC for large payloads and production streaming.
  • gRPC: Uses bidirectional streaming and splits writes into 2 KiB request chunks. The crypto API has no configurable chunkSize. The current stream implementation can queue input and output internally; do not assume that using streams guarantees bounded memory at arbitrary producer or consumer speeds.
  • Empty input: HTTP supports empty plaintext with either overload. In the 3.19.0 gRPC implementation, options are sent with the first nonempty input chunk, so an empty input does not send an initial options frame. Use HTTP for empty plaintext rather than relying on gRPC empty-input behavior.

Ciphertext produced over HTTP can be decrypted over gRPC and vice versa, provided the component can access the correct key. The document uses Dapr’s encryption format, not a bare AES or RSA ciphertext.

Cancellation and errors

EncryptRequest and DecryptRequest have no AbortSignal, timeout, or cancellation-token option. Do not assume cancellation support from other building blocks applies to cryptography. Destroying an HTTP crypto Duplex aborts its underlying request; the gRPC crypto overloads do not expose an equivalent request-cancellation option. A timeout implemented by racing promises alone does not cancel the operation.

Await buffer operations and handle their rejected promises. For streams, errors can occur after the promise has returned the Duplex, so awaiting client.crypto.encrypt(options) alone is not sufficient. Await the pipeline and handle its rejection.

Missing components, missing or incompatible keys, unsupported algorithms, malformed ciphertext, and authentication failures during decryption cause operations to fail rather than returning an empty successful result. HTTP runtime errors are Error instances whose message contains JSON with status, error, and error_msg; network and stream errors can have different shapes. gRPC errors come from the Connect transport and should not be interpreted as HTTP status codes.

A failed or cancelled file pipeline may leave a partial output file. Remove or otherwise handle that file, and do not treat decrypted output as complete until the whole operation succeeds.

Concurrent operations

Runtimes affected by dapr/kit#170 can fail when decryption overlaps other cryptography operations. On affected sidecars, serialize decryption with all other cryptography work, including encryption, across callers sharing that sidecar. Serializing only within one function or one client does not protect against other callers.

In particular, finish encryption before starting decryption rather than chaining them in one pipeline or using Promise.all for round-trips. Check the issue and your runtime version before enabling overlapping operations.