Utilities

Pure-function helper classes — public-key encoding, hashing, payload envelope wrapping, and library initialization.

Utilities

@zafeguard/mpc-sdk ships four helper classes for cryptographic plumbing that doesn't fit cleanly into the signing or encryption surfaces. Most production integrations touch at least one of them — typically KeyEncoding for normalizing public-key shape across chains, or Crypto for sealing payloads against a recipient's key.

ClassPurposeLicense required
MpcInitialize the SDK with your licence cert. Required once per process for the WASM backend.The init step IS the licence check.
KeyEncodingSEC1 compress / decompress, format detection, Ethereum address derivation.No
HashKeccak-256 (Ethereum hash).No
CryptoWrap / unwrap payloads under an RSA or P-256 ECIES envelope.No

Mpc — initialization

In Node.js, the SDK loads its native binary at import time and does not need an explicit init call — every other class works immediately. In browsers, React Native, and other WASM environments, you must call Mpc.init(certificate) once before using any signing or encryption primitive.

import { Mpc } from '@zafeguard/mpc-sdk';

await Mpc.init(certificate);

certificate is the base64url-encoded licence cert your account was issued. The call returns an object with the licensee, expiry, and feature flags so the application can branch on what the licence permits.

Mpc.init is idempotent — calling it a second time on the same process returns the cached licence without re-verifying. It's safe to call defensively at the top of any code path that uses the SDK.

Mpc.getLicense() → LicenseInfo | null

Returns the cached licence info (or null if init hasn't been called yet). Useful for branching on licence feature flags without re-running the init.

KeyEncoding — public-key encoding utilities

Every chain has its own conventions for public-key encoding — Ethereum uses uncompressed 65-byte form, Bitcoin uses compressed 33-byte form, Solana uses raw 32-byte Edwards encoding. KeyEncoding provides curve-aware compression, decompression, format detection, and derivation utilities so you don't write per-chain encoding code.

KeyEncoding.compress(curve, publicKey) → Buffer

Converts a public key to its compressed form. Accepts both compressed and uncompressed inputs — calling compress on an already-compressed key is a no-op that returns the same bytes.

import { KeyEncoding, Curve } from '@zafeguard/mpc-sdk';

const uncompressed = Buffer.from('04a1b2...', 'hex'); // 65 bytes
const compressed = KeyEncoding.compress(Curve.Secp256k1, uncompressed);
// compressed.length === 33; compressed[0] in [0x02, 0x03]

KeyEncoding.decompress(curve, publicKey) → Buffer

The inverse — expands to the uncompressed SEC1 form. Required when you need the full (x, y) coordinates for Ethereum address derivation or similar.

KeyEncoding.detectFormat(publicKey) → PublicKeyFormat

Returns one of PublicKeyFormat.Compressed, Uncompressed, or Edwards. Throws if the length / prefix combination doesn't match any known SEC1 / Edwards form.

Useful when accepting public keys from external sources and you don't know which encoding they used.

const fmt = KeyEncoding.detectFormat(publicKey);
if (fmt === PublicKeyFormat.Uncompressed) {
  // 65-byte SEC1 uncompressed
}

KeyEncoding.toEthAddress(uncompressedPubKey) → string

Derives a checksummed (EIP-55) Ethereum address from a 65-byte uncompressed secp256k1 public key. Throws if the input isn't exactly 65 bytes or doesn't start with 0x04.

const ethAddress = KeyEncoding.toEthAddress(uncompressedPubKey);
// "0xa1B2c3D4..."

If you have a compressed public key, decompress it first:

const uncompressed = KeyEncoding.decompress(Curve.Secp256k1, compressedPubKey);
const address = KeyEncoding.toEthAddress(uncompressed);

Hash and Utils.sha256 / Utils.keccak256 — hashing primitives

Utils.sha256 and Utils.keccak256 are the cross-runtime hash helpers — Node, browser, and React Native all reach them through the same call, so you don't have to switch between crypto.createHash (Node) and crypto.subtle.digest (browser) when your wallet code has to run on both. Hash.keccak256 is the older namespace; new code should use the Utils form.

Utils.sha256(data) → Uint8Array

SHA-256 digest. Input is Uint8Array (or Buffer, since Buffer extends Uint8Array). Returns a 32-byte Uint8Array. The compiled binary does the hashing — no license required.

import { Utils } from '@zafeguard/mpc-sdk';

// String input — wrap with TextEncoder first.
const messageHash = Utils.sha256(new TextEncoder().encode('hello world'));
// messageHash is a 32-byte Uint8Array; pass it straight to loaded.signMessage().

// Buffer input — works the same.
const txHash = Utils.sha256(Buffer.from(rawTransactionBytes));

Utils.keccak256(data) → Uint8Array (and Hash.keccak256 legacy alias)

Keccak-256 — Ethereum's native hash function. Used for address derivation, ABI-encoded message hashing, and EIP-712 typed-data hashing. Same call shape and return type as Utils.sha256.

import { Utils } from '@zafeguard/mpc-sdk';

const digest = Utils.keccak256(new TextEncoder().encode('hello'));

Hash.keccak256(data: Buffer) still works for back-compat; both call into the same Rust primitive.

Crypto — payload envelopes

Crypto wraps and unwraps small payloads under either an RSA-OAEP envelope or a P-256 ECIES envelope. The wrapped envelope is portable — the wrapping side and the unwrapping side need only a shared understanding of the recipient's public key.

Use cases:

  • Encrypting a key-share export for transit between two processes that have asymmetric trust (e.g. moving a share off a phone to a desktop backup tool).
  • Sealing a recovery bundle so only the licence holder's private key can unseal it.
  • Custom flows where you need to ship a sensitive payload from your backend to a user's device with end-to-end confidentiality.

RSA envelopes

import { Crypto } from '@zafeguard/mpc-sdk';

// Wrap with the recipient's RSA public key.
const wrapped = Crypto.wrap(plaintextBytes, rsaPublicKeyPem);

// Unwrap with the matching RSA private key.
const recovered = Crypto.unwrap(wrapped, rsaPrivateKeyPem);
// recovered.equals(plaintextBytes) === true

rsaPublicKeyPem and rsaPrivateKeyPem are PEM-encoded PKCS#8 strings (with -----BEGIN PUBLIC KEY----- / -----BEGIN PRIVATE KEY----- armour).

The envelope is RSA-OAEP-SHA256 wrapping an AES-256-GCM key, with AES-GCM encrypting the actual payload. Bit-flips in the wire format are rejected at the AES-GCM tag check.

P-256 envelopes

For iOS Secure Enclave compatibility, the SDK also exposes a P-256 ECIES variant:

const wrapped = Crypto.p256Wrap(
  plaintextBytes,
  userP256PublicKeyBytes,    // 33-byte compressed SEC1
  licenseFingerprint,        // 32 bytes; binds the envelope to your licence
);

const recovered = Crypto.p256Unwrap(
  wrapped,
  userP256PrivateKeyBytes,
  licenseFingerprint,
);

The P-256 form is useful when the recipient holds their unwrap key inside a hardware secure element (iOS Secure Enclave, Android Keystore StrongBox) that doesn't support RSA but does support P-256 ECDH.

The licenseFingerprint parameter binds the envelope to a specific licence — only an unwrap call presenting the same fingerprint will succeed. This prevents an envelope sealed for licence-A from being unsealed under licence-B even if both parties hold the same P-256 keypair.

Two-step unwrap for hardware-isolated keys

When the recipient's private key lives in a secure enclave that can perform ECDH but won't release the raw private bytes, the unwrap can be split into two steps:

// Step 1: extract the wrapper's ephemeral P-256 public key.
const ephemeralPub = Crypto.p256ExtractEphemeralKey(wrapped);

// Step 2: the secure enclave performs ECDH between its private key
// and the ephemeral public key — returns the 32-byte raw shared secret.
const sharedSecret = await secureEnclaveECDH(ephemeralPub);

// Step 3: finish the unwrap with the shared secret.
const recovered = Crypto.p256UnwrapWithSharedSecret(
  wrapped,
  sharedSecret,
  licenseFingerprint,
);

This pattern keeps the user's private key inside the enclave — it never enters JavaScript memory. The enclave's ECDH primitive returns only the shared secret, which is enough to complete the AES-GCM decryption.

When to reach for these utilities

  • You're displaying an Ethereum address derived from an secp256k1 public key your application minted via threshold-MPC. → KeyEncoding.toEthAddress.
  • You're accepting a public key from a user-supplied form and don't know if it's compressed or uncompressed. → KeyEncoding.detectFormat.
  • You're hashing an EIP-191 / EIP-712 message before signing. → Hash.keccak256.
  • You're shipping a key-share export across a network where you can't fully trust the transport. → Crypto.wrap (RSA) or Crypto.p256Wrap (P-256).
  • You're integrating with iOS Secure Enclave or Android StrongBox and need a P-256-based envelope. → Crypto.p256Wrap / p256ExtractEphemeralKey / p256UnwrapWithSharedSecret.

For higher-level patterns (recovery bundles, sign-pack export for cold-wallet flows), use the dedicated RecoveryBundle and the cold-wallet flow surfaces — they wrap these primitives with the right metadata and binding.

Platform availability

All four classes work in both Node.js and WASM environments. The Crypto wrap / unwrap calls additionally have a WASM-fallback path: if the native binary isn't loaded, the calls throw a clear error message pointing at ClusterAgentFetch (which uses the runtime's fetch instead of the native HTTP client).

Hash.keccak256 is the most-called helper; it has WASM fallbacks tuned for browser performance.

On this page