CIFER SDK - Quantum-Resistant Blockchain Encryption
Skill for AI Agents | Enable quantum-resistant encryption in blockchain applications using the CIFER SDK (v0.5.3).
Overview
CIFER (Cryptographic Infrastructure for Encrypted Records) SDK provides quantum-resistant encryption for blockchain applications. This skill enables AI agents to implement secure data encryption, secret management, and on-chain commitments using post-quantum cryptography.
Key Capabilities
- Quantum-Resistant Encryption: ML-KEM-768 (NIST standardized) key encapsulation
- Multi-Chain Support: Automatic chain discovery and configuration
- Wallet Agnostic: Works with any EIP-1193 provider — zero wallet dependencies, bring your own wallet
- Web2 Mode: Email + password registration with session-based auth (no wallet needed)
- File Encryption: Async job system for large file encryption/decryption
- On-Chain Commitments: Store encrypted data references on-chain with log-based retrieval
- Transaction Intents: Non-custodial pattern - you control transaction execution
- High-Level Flows: Orchestrated operations for common patterns
Architecture
- SecretsController (on-chain): Manages secret ownership and delegation
- Blackbox API (off-chain): Handles encryption/decryption operations
- Enclave Cluster: Stores private key shards using threshold cryptography
- IPFS: Stores public keys for encryption
When to Use This Skill
Use the CIFER SDK when you need to:
- Encrypt sensitive data with quantum-resistant algorithms
- Store encrypted records on blockchain
- Manage encryption keys with owner/delegate authorization
- Encrypt files larger than 16KB using the job system
- Build applications requiring post-quantum security
- Build walletless apps with email-based authentication (Web2 mode)
Installation
npm install cifer-sdk
# or
yarn add cifer-sdk
# or
pnpm add cifer-sdk
Requirements: Node.js 18.0+, TypeScript 5.0+ (recommended)
Runtime Dependencies:
@noble/secp256k1(for PrivateKeySignerAdapter / Web2 sessions)@noble/hashes(for keccak256 address derivation)
ESM Import:
import { createCiferSdk, keyManagement, blackbox, commitments, flows, web2 } from 'cifer-sdk';
CommonJS Import:
const { createCiferSdk, keyManagement, blackbox, web2 } = require('cifer-sdk');
Sub-path Imports:
import * as web2 from 'cifer-sdk/web2';
import { encryptPayload } from 'cifer-sdk/blackbox';
import { Eip1193SignerAdapter } from 'cifer-sdk/adapters';
Quick Start
import { createCiferSdk, Eip1193SignerAdapter, blackbox } from 'cifer-sdk';
// 1. Initialize SDK with auto-discovery
const sdk = await createCiferSdk({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// 2. Connect wallet (browser)
const signer = new Eip1193SignerAdapter(window.ethereum);
// 3. Encrypt data
const encrypted = await blackbox.payload.encryptPayload({
chainId: 8453,
secretId: 123n,
plaintext: 'My secret message',
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
// 4. Decrypt data
const decrypted = await blackbox.payload.decryptPayload({
chainId: 8453,
secretId: 123n,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
console.log(decrypted.decryptedMessage); // 'My secret message'
Quick Start (Web2 - No Wallet)
import { createCiferSdk, web2 } from 'cifer-sdk';
import * as ed from '@noble/ed25519';
// 1. Initialize SDK
const sdk = await createCiferSdk({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// 2. Ed25519 key setup
const privateKey = ed.utils.randomPrivateKey();
const publicKey = await ed.getPublicKeyAsync(privateKey);
const ed25519Signer = {
async sign(message: Uint8Array) { return ed.signAsync(message, privateKey); },
getPublicKey() { return publicKey; },
};
// 3. Register (one-time)
const reg = await web2.auth.register({
email: 'user@example.com',
password: 'securePassword123',
blackboxUrl: sdk.blackboxUrl,
});
await web2.auth.verifyEmail({ email: 'user@example.com', otp: '123456', blackboxUrl: sdk.blackboxUrl });
await web2.auth.registerKey({ principalId: reg.principalId, password: 'securePassword123', ed25519Signer, blackboxUrl: sdk.blackboxUrl });
// 4. Create session
const session = await web2.session.createManagedSession({
principalId: reg.principalId,
ed25519Signer,
blackboxUrl: sdk.blackboxUrl,
});
// 5. Create secret & encrypt
const secret = await web2.secret.createSecret({ session, blackboxUrl: sdk.blackboxUrl });
const encrypted = await web2.blackbox.payload.encryptPayload({
session,
secretId: secret.secretId,
plaintext: 'Hello from Web2!',
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
// 6. Decrypt
const decrypted = await web2.blackbox.payload.decryptPayload({
session,
secretId: secret.secretId,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
console.log(decrypted.decryptedMessage); // 'Hello from Web2!'
Core Concepts
Secrets
A secret is the core primitive in CIFER. Each secret represents an ML-KEM-768 key pair where:
- Public key: Stored on IPFS, used for encryption
- Private key: Split across enclave cluster using threshold cryptography
| Property | Description |
|---|---|
owner |
Address that can transfer, set delegate, and decrypt |
delegate |
Address that can decrypt only (zero address if none) |
isSyncing |
true while key generation is in progress |
clusterId |
Which enclave cluster holds the private key shards |
secretType |
1 = ML-KEM-768 (standard) |
publicKeyCid |
IPFS CID of public key (empty if syncing) |
Lifecycle:
- Creation: User calls
createSecret()on SecretsController (pays fee) - Syncing: Enclave cluster generates keys and stores shards (~30-60 seconds)
- Ready:
isSyncingbecomes false,publicKeyCidis set - Usage: Owner/delegate can encrypt and decrypt
Authorization Model
Encryption requires only a valid signature — any wallet can encrypt for any secret. Decryption and management operations are restricted by role:
| Role | Encrypt | Decrypt | Transfer | Set Delegate |
|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes |
| Delegate | Yes | Yes | No | No |
| Any wallet | Yes | No | No | No |
Setting a delegate:
const txIntent = keyManagement.buildSetDelegateTx({
chainId,
controllerAddress,
secretId: 123n,
newDelegate: '0xDelegateAddress...',
});
Removing a delegate:
const txIntent = keyManagement.buildRemoveDelegationTx({
chainId,
controllerAddress,
secretId: 123n,
});
Encryption Model
CIFER uses hybrid encryption:
- ML-KEM-768: Post-quantum key encapsulation (1088-byte ciphertext)
- AES-256-GCM: Symmetric encryption for actual data
Output format:
| Field | Size | Description |
|---|---|---|
cifer |
1104 bytes | ML-KEM ciphertext (1088) + tag (16) |
encryptedMessage |
Variable | AES-GCM encrypted data (max 16KB) |
Block Freshness
All blackbox API calls require a recent block number in the signed payload to prevent replay attacks.
- Freshness window:
100 blocks (10 minutes) - SDK automatically retries with fresh block (up to 3 times)
- If you see "block too old" errors, check RPC reliability
Transaction Intents
The SDK returns transaction intents instead of executing transactions:
interface TxIntent {
chainId: number;
to: Address;
data: Hex;
value?: bigint;
}
Execute with any wallet library:
// ethers v6
await signer.sendTransaction({ to: txIntent.to, data: txIntent.data, value: txIntent.value });
// wagmi
await sendTransaction({ to: txIntent.to, data: txIntent.data, value: txIntent.value });
// viem
await walletClient.sendTransaction({ to: txIntent.to, data: txIntent.data, value: txIntent.value });
Web2 Mode
CIFER supports Web2 mode for apps that don't use blockchain wallets. Users register with email + password and authenticate via Ed25519-signed sessions.
WEB2_CHAIN_ID = -1: Sentinel value used for all Web2 operations. When chainId is -1, the SDK uses Date.now() instead of an RPC block number for freshness.
| Feature | Web3 | Web2 |
|---|---|---|
| Auth | EIP-1193 wallet | Email + password + Ed25519 key |
| Chain ID | Real chain ID (e.g. 8453) | WEB2_CHAIN_ID = -1 (sentinel) |
| Block freshness | RPC eth_blockNumber |
Date.now() (no RPC needed) |
| Signer | Wallet personal_sign |
Session EOA personal_sign |
| Secret creation | On-chain transaction | POST /web2/secret API call |
How sessions work:
- An ephemeral secp256k1 keypair is generated (session key)
- The session is authenticated with an Ed25519 signature
- The session key signs blackbox requests (same EIP-191 format as wallets)
- Sessions expire and can be auto-renewed (managed sessions)
Ed25519 Signer Interface (bring-your-own-library):
interface Ed25519Signer {
sign(message: Uint8Array): Promise<Uint8Array>;
getPublicKey(): Uint8Array;
}
Use @noble/ed25519 or any Ed25519 library:
import * as ed from '@noble/ed25519';
const privateKey = ed.utils.randomPrivateKey();
const publicKey = await ed.getPublicKeyAsync(privateKey);
const ed25519Signer: Ed25519Signer = {
async sign(message) { return ed.signAsync(message, privateKey); },
getPublicKey() { return publicKey; },
};
API Reference
SDK Initialization
With Discovery (Recommended)
const sdk = await createCiferSdk({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
sdk.getSupportedChainIds(); // [8453, 11155111, ...]
sdk.getControllerAddress(8453); // '0x...'
sdk.getRpcUrl(8453); // 'https://...'
Device Clock Integrity Check
The discovery result exposes an optional serverTime (Unix epoch ms) — the blackbox server's own clock at the moment it handled the /healthz request. Compare it against Date.now() to detect a misconfigured or manipulated device clock before trusting any device-side time-based logic (e.g. a time-lock countdown). It is optional and absent on older blackbox deployments.
import { discover } from 'cifer-sdk';
const discovery = await discover({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
if (discovery.serverTime !== undefined) {
const skewMs = Math.abs(Date.now() - discovery.serverTime);
if (skewMs > 10 * 60 * 1000) {
// device clock is more than 10 minutes off the blackbox — block time-sensitive UI
}
}
With Overrides
const sdk = await createCiferSdk({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
chainOverrides: {
8453: {
rpcUrl: 'https://my-private-rpc.example.com',
secretsControllerAddress: '0x...',
},
},
});
Synchronous (No Discovery)
import { createCiferSdkSync, RpcReadClient } from 'cifer-sdk';
const readClient = new RpcReadClient({
rpcUrlByChainId: {
8453: 'https://mainnet.base.org',
},
});
const sdk = createCiferSdkSync({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
readClient,
chainOverrides: {
8453: {
rpcUrl: 'https://mainnet.base.org',
secretsControllerAddress: '0x...',
},
},
});
Wallet Integration
All wallets must implement the SignerAdapter interface:
interface SignerAdapter {
getAddress(): Promise<string>;
signMessage(message: string): Promise<string>; // EIP-191 personal_sign
sendTransaction?(txRequest: TxIntent): Promise<TxExecutionResult>; // optional
}
MetaMask
import { Eip1193SignerAdapter } from 'cifer-sdk';
if (typeof window.ethereum === 'undefined') {
throw new Error('MetaMask is not installed');
}
await window.ethereum.request({ method: 'eth_requestAccounts' });
const signer = new Eip1193SignerAdapter(window.ethereum);
const address = await signer.getAddress();
// Handle account changes
window.ethereum.on('accountsChanged', (accounts) => {
signer.clearCache();
console.log('Switched to:', accounts[0]);
});
WalletConnect v2
import { EthereumProvider } from '@walletconnect/ethereum-provider';
const provider = await EthereumProvider.init({
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
chains: [8453],
showQrModal: true,
metadata: {
name: 'My CIFER App',
description: 'Quantum-resistant encryption',
url: 'https://myapp.com',
icons: ['https://myapp.com/icon.png'],
},
});
await provider.connect();
const signer = new Eip1193SignerAdapter(provider);
Thirdweb
import { createThirdwebClient, defineChain } from 'thirdweb';
import { createWallet, injectedProvider } from 'thirdweb/wallets';
const thirdwebClient = createThirdwebClient({
clientId: 'YOUR_THIRDWEB_CLIENT_ID',
});
const base = defineChain({
id: 8453,
name: 'Base',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['https://mainnet.base.org'] } },
});
const wallet = createWallet('io.metamask');
await wallet.connect({ client: thirdwebClient, chain: base });
const provider = injectedProvider('io.metamask');
const signer = new Eip1193SignerAdapter(provider);
// For in-app wallets (email/social login):
import { inAppWallet } from 'thirdweb/wallets';
const wallet = inAppWallet();
const account = await wallet.connect({
client: thirdwebClient,
chain: base,
strategy: 'email',
email: 'user@example.com',
});
const signer = {
async getAddress() { return account.address; },
async signMessage(message) { return account.signMessage({ message }); },
};
Private Key (Server-Side)
WARNING: Never expose private keys in frontend code!
Using ethers.js:
import { Wallet } from 'ethers';
import type { SignerAdapter } from 'cifer-sdk';
const wallet = new Wallet(process.env.PRIVATE_KEY);
const signer: SignerAdapter = {
async getAddress() { return wallet.address; },
async signMessage(message) { return wallet.signMessage(message); },
};
Using viem:
import { privateKeyToAccount } from 'viem/accounts';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const signer = {
async getAddress() { return account.address; },
async signMessage(message) { return account.signMessage({ message }); },
};
wagmi (React)
import { useAccount, useConnectorClient } from 'wagmi';
function useCiferSigner() {
const { address, isConnected } = useAccount();
const { data: connectorClient } = useConnectorClient();
const getSigner = async () => {
if (!isConnected || !connectorClient) {
throw new Error('Wallet not connected');
}
const provider = await connectorClient.transport;
return new Eip1193SignerAdapter(provider);
};
return { getSigner, address, isConnected };
}
Coinbase Wallet
import { CoinbaseWalletSDK } from '@coinbase/wallet-sdk';
const coinbaseWallet = new CoinbaseWalletSDK({
appName: 'My CIFER App',
appLogoUrl: 'https://myapp.com/logo.png',
});
const provider = coinbaseWallet.makeWeb3Provider({ options: 'all' });
await provider.request({ method: 'eth_requestAccounts' });
const signer = new Eip1193SignerAdapter(provider);
Supporting Multiple Wallets
type WalletType = 'metamask' | 'walletconnect' | 'coinbase';
async function createSigner(type: WalletType): Promise<SignerAdapter> {
switch (type) {
case 'metamask':
return new Eip1193SignerAdapter(window.ethereum);
case 'walletconnect':
const wcProvider = await EthereumProvider.init({ /* config */ });
await wcProvider.connect();
return new Eip1193SignerAdapter(wcProvider);
case 'coinbase':
const cbProvider = coinbaseWallet.makeWeb3Provider();
await cbProvider.request({ method: 'eth_requestAccounts' });
return new Eip1193SignerAdapter(cbProvider);
}
}
keyManagement Namespace
Interact with the SecretsController contract for secret management.
Read Operations
// Get secret creation fee
const fee = await keyManagement.getSecretCreationFee({
chainId: 8453,
controllerAddress: sdk.getControllerAddress(8453),
readClient: sdk.readClient,
});
// Get secret state
const state = await keyManagement.getSecret(params, 123n);
// Returns: { owner, delegate, isSyncing, clusterId, secretType, publicKeyCid }
// Check if secret is ready
const ready = await keyManagement.isSecretReady(params, 123n);
// Check authorization
const canDecrypt = await keyManagement.isAuthorized(params, 123n, '0x...');
// Get secrets by wallet
const secrets = await keyManagement.getSecretsByWallet(params, '0xUser...');
// Returns: { owned: bigint[], delegated: bigint[] }
// Get counts of secrets (more gas-efficient than getSecretsByWallet)
const counts = await keyManagement.getSecretsCountByWallet(params, '0xUser...');
// Returns: { ownedCount: bigint, delegatedCount: bigint }
Transaction Builders
// Create a new secret
const fee = await keyManagement.getSecretCreationFee(params);
const txIntent = keyManagement.buildCreateSecretTx({
chainId: 8453,
controllerAddress: sdk.getControllerAddress(8453),
fee,
});
// Set delegate
const txIntent = keyManagement.buildSetDelegateTx({
chainId: 8453,
controllerAddress: sdk.getControllerAddress(8453),
secretId: 123n,
newDelegate: '0xDelegate...',
});
// Remove delegate
const txIntent = keyManagement.buildRemoveDelegationTx({ ... });
// Transfer ownership (irreversible!)
const txIntent = keyManagement.buildTransferSecretTx({
chainId: 8453,
controllerAddress: sdk.getControllerAddress(8453),
secretId: 123n,
newOwner: '0xNewOwner...',
});
Event Parsing
const receipt = await provider.waitForTransaction(hash);
const secretId = keyManagement.extractSecretIdFromReceipt(receipt.logs);
// Parse individual event logs
const created = keyManagement.parseSecretCreatedLog(log);
const synced = keyManagement.parseSecretSyncedLog(log);
const delegateUpdated = keyManagement.parseDelegateUpdatedLog(log);
blackbox.payload Namespace
Encrypt and decrypt short messages (< 16KB).
Encrypt
const encrypted = await blackbox.payload.encryptPayload({
chainId: 8453,
secretId: 123n,
plaintext: 'My secret message',
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
outputFormat: 'hex', // or 'base64'
});
// Returns: { cifer: string, encryptedMessage: string }
Decrypt
const decrypted = await blackbox.payload.decryptPayload({
chainId: 8453,
secretId: 123n,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
signer, // Must be owner or delegate
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
inputFormat: 'hex',
});
// Returns: { decryptedMessage: string }
blackbox.files Namespace
Encrypt and decrypt large files using async jobs.
// Start encryption job
const job = await blackbox.files.encryptFile({
chainId: 8453,
secretId: 123n,
file: myFile,
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
// Returns: { jobId: string, message: string }
// Start decryption job
const job = await blackbox.files.decryptFile({ ... });
// Decrypt from existing encrypt job
const job = await blackbox.files.decryptExistingFile({
chainId: 8453,
secretId: 123n,
encryptJobId: previousJobId,
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
blackbox.jobs Namespace
Manage async file jobs.
// Get job status
const status = await blackbox.jobs.getStatus(jobId, sdk.blackboxUrl);
// Returns: { id, type, status, progress, secretId, chainId, ... }
// Poll until complete
const finalStatus = await blackbox.jobs.pollUntilComplete(
jobId,
sdk.blackboxUrl,
{
intervalMs: 2000,
maxAttempts: 120,
onProgress: (job) => console.log(`Progress: ${job.progress}%`),
}
);
// Download result (encrypt jobs: no auth, decrypt jobs: auth required)
const blob = await blackbox.jobs.download(jobId, {
blackboxUrl: sdk.blackboxUrl,
// For decrypt jobs, also provide:
chainId: 8453,
secretId: 123n,
signer,
readClient: sdk.readClient,
});
// List jobs for wallet
const result = await blackbox.jobs.list({
chainId: 8453,
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
// Get data consumption stats
const stats = await blackbox.jobs.dataConsumption({ ... });
// Delete a job (mark for cleanup)
await blackbox.jobs.deleteJob(jobId, {
blackboxUrl: sdk.blackboxUrl,
chainId: 8453,
secretId: 123n,
signer,
readClient: sdk.readClient,
});
commitments Namespace
Store and retrieve encrypted data on-chain.
The commitment pattern:
- Only hashes stored in contract storage (gas efficient)
- Full encrypted bytes emitted in events
- Retrieve from logs using block number from metadata
// Check if commitment exists
const exists = await commitments.ciferDataExists(params, dataId);
// Get metadata
const metadata = await commitments.getCIFERMetadata(params, dataId);
// Returns: { secretId, storedAtBlock, ciferHash, encryptedMessageHash }
// Fetch encrypted data from logs
const data = await commitments.fetchCommitmentFromLogs({
chainId: 8453,
contractAddress: '0x...',
dataId: dataKey,
storedAtBlock: metadata.storedAtBlock,
readClient: sdk.readClient,
});
// Returns: { cifer, encryptedMessage, ciferHash, encryptedMessageHash }
// Verify integrity
const result = commitments.verifyCommitmentIntegrity(data, metadata);
// Verify and throw on failure
commitments.assertCommitmentIntegrity(data, metadata);
// Validate before storing (throws on invalid sizes)
commitments.validateForStorage(cifer, encryptedMessage);
// Build store transaction
const txIntent = commitments.buildStoreCommitmentTx({
chainId: 8453,
contractAddress: '0xYourContract...',
storeFunction: {
type: 'function',
name: 'store',
inputs: [
{ name: 'key', type: 'bytes32' },
{ name: 'encryptedMessage', type: 'bytes' },
{ name: 'cifer', type: 'bytes' },
],
},
args: {
key: dataKey,
secretId: 123n,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
},
validate: true, // optional, default: true — validate sizes before building tx
});
Constants:
CIFER_ENVELOPE_BYTES = 1104(fixed cifer size)MAX_PAYLOAD_BYTES = 16384(16KB max payload)
flows Namespace
High-level orchestrated operations.
Flows support two modes:
- plan: Returns a plan describing steps (dry run)
- execute: Actually performs the operations
Flow Context
const ctx = {
signer: SignerAdapter,
readClient: ReadClient,
blackboxUrl: string,
chainId: number,
controllerAddress?: Address,
txExecutor?: (intent: TxIntent) => Promise<TxExecutionResult>,
pollingStrategy?: { intervalMs: number, maxAttempts: number },
logger?: (message: string) => void,
abortSignal?: AbortSignal,
};
Create Secret and Wait
const result = await flows.createSecretAndWaitReady({
...ctx,
controllerAddress: sdk.getControllerAddress(8453),
txExecutor: async (intent) => {
const hash = await wallet.sendTransaction(intent);
return { hash, waitReceipt: () => provider.waitForTransaction(hash) };
},
});
if (result.success) {
console.log('Secret ID:', result.data.secretId);
console.log('Public Key CID:', result.data.state.publicKeyCid);
}
Encrypt and Prepare Commit
const result = await flows.encryptThenPrepareCommitTx(ctx, {
secretId: 123n,
plaintext: 'My secret data',
key: dataKey,
commitmentContract: '0x...',
storeFunction: storeAbi, // optional — uses default if omitted
});
if (result.success) {
await wallet.sendTransaction(result.data.txIntent);
}
Retrieve and Decrypt from Logs
const result = await flows.retrieveFromLogsThenDecrypt(ctx, {
secretId: 123n,
dataId: dataKey,
commitmentContract: '0x...',
storedAtBlock: metadata.storedAtBlock, // optional — fetched if not provided
skipIntegrityCheck: false, // optional
});
if (result.success) {
console.log('Decrypted:', result.data.decryptedMessage);
}
File Flows
// Encrypt file flow
const result = await flows.encryptFileJobFlow(ctx, {
secretId: 123n,
file: myFile,
});
// Returns: { jobId, job, encryptedFile: Blob }
// Decrypt file flow
const result = await flows.decryptFileJobFlow(ctx, {
secretId: 123n,
file: ciferFile,
});
// Returns: { jobId, job, decryptedFile: Blob }
// Decrypt from existing encrypt job without re-uploading
const result = await flows.decryptExistingFileJobFlow(ctx, {
secretId: 123n,
encryptJobId: previousJobId,
});
// Returns: { jobId, job, decryptedFile: Blob }
web2.auth Namespace
Registration and authentication for Web2 mode (two-phase flow).
import { web2 } from 'cifer-sdk';
// Phase 1: Register with email + password (sends OTP)
const reg = await web2.auth.register({
email: 'user@example.com',
password: 'securePassword123',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { principalId: string, message: string }
// Phase 2: Verify email OTP
const verified = await web2.auth.verifyEmail({
email: 'user@example.com',
otp: '123456',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { principalId: string, emailVerified: boolean }
// Phase 3: Register Ed25519 key (propagated to cluster nodes)
const keyResult = await web2.auth.registerKey({
principalId: reg.principalId,
password: 'securePassword123',
ed25519Signer,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { principalId: string, nodeRegistrationStatus: string }
// If nodeRegistrationStatus !== 'complete', retry:
await web2.auth.retryNodeRegistration({
principalId: reg.principalId,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Check node registration status
const status = await web2.auth.nodeRegistrationStatus({
principalId: reg.principalId,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
Helper Functions
// Resend OTP (60-second cooldown)
await web2.auth.resendOtp({
email: 'user@example.com',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Forgot password (sends OTP)
await web2.auth.forgotPassword({
email: 'user@example.com',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Reset password with OTP
await web2.auth.resetPassword({
email: 'user@example.com',
otp: '123456',
newPassword: 'newSecurePassword456',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
Account Deletion
Account deletion is a two-step, OTP-confirmed operation. These are stateless web2.auth functions; they are not methods on the client returned by web2.createClient().
Step 1 — request a deletion OTP
requestAccountDeletion(params): Promise<{ message: string }> accepts email, password, principalId, blackboxUrl, and an optional fetch implementation.
const deletionRequest = await web2.auth.requestAccountDeletion({
email: 'user@example.com',
password: 'securePassword123',
principalId: 'your-principal-uuid',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
console.log(deletionRequest.message);
For anti-enumeration, the Blackbox always returns a generic success message. It sends an OTP only when the email, password, and principalId match a verified, active account; a nonexistent or mismatched account does not throw solely for that mismatch.
Step 2 — confirm account deletion
confirmAccountDeletion(params): Promise<ConfirmAccountDeletionResult> accepts email, otp, blackboxUrl, and an optional fetch implementation.
const deletionResult = await web2.auth.confirmAccountDeletion({
email: 'user@example.com',
otp: '123456',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
console.log(deletionResult.success); // true
Confirmation soft-deletes the account: it becomes dormant and hidden from APIs while its records are retained for legal disclosure. Registering again with the same email reactivates the same principalId and restores access to its existing secrets. After successful confirmation, clear cached credentials, Ed25519 keys, session keys, and active sessions from local application state.
web2.session Namespace
Create and manage Web2 sessions. Two modes available:
Managed Session (Recommended)
SDK manages session lifecycle with auto-renewal:
const session = await web2.session.createManagedSession({
principalId: 'your-uuid',
ed25519Signer,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
ttl: 900, // seconds (default: 900 = 15 minutes)
});
// Session properties:
session.signer; // SignerAdapter (ephemeral EOA)
session.sessionAddress; // EOA address
session.principalId; // UUID
session.expiresAt; // ISO 8601 timestamp
session.isManaged; // true
// Auto-renew if near expiry (60s skew)
await session.ensureValid();
// Force renewal
await session.renew();
Existing Session Key (Advanced)
Wrap a pre-existing session private key (e.g. from a TEE web front):
const session = web2.session.useExistingSessionKey({
sessionPrivateKey: '0xabc123...', // hex-encoded secp256k1 private key
principalId: 'your-uuid', // optional
});
// session.isManaged === false
// session.renew() throws Web2SessionError
// session.ensureValid() is a no-op
web2.secret Namespace
Create and list Web2 secrets.
// Create a new secret
const result = await web2.secret.createSecret({
session,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { secretId: number }
// List all secrets for the principal
const list = await web2.secret.listSecrets({
session,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { secrets: Array<{ secretId, status }> }
web2.delegate Namespace
Set or remove delegates on Web2 secrets.
// Set a delegate
await web2.delegate.setDelegate({
session,
secretId: 42,
delegatePrincipalId: 'delegate-principal-uuid',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Remove a delegate (empty string)
await web2.delegate.setDelegate({
session,
secretId: 42,
delegatePrincipalId: '',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
web2.permit Namespace
Request permits for key rotation, ownership transfer, or delegation changes.
// Key rotation (email+password, no session needed)
const result = await web2.permit.requestPermit({
action: 'rotate',
email: 'user@example.com',
password: 'securePassword123',
payload: { newPublicKey: '...' },
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Returns: { permitId: string }
// Transfer ownership (session required)
const result = await web2.permit.requestPermit({
action: 'transfer',
session,
secretId: 42,
payload: { newOwnerPrincipalId: 'new-owner-uuid' },
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
// Delegate permit (session required)
const result = await web2.permit.requestPermit({
action: 'delegate',
session,
secretId: 42,
payload: { delegatePrincipalId: 'delegate-uuid' },
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
web2.principal Namespace
Look up principals by email.
const principal = await web2.principal.getByEmail(
'colleague@example.com',
'https://blackbox.cifersecurity.com:3010',
);
// Returns: { principalId: string, emailHex: string }
web2.blackbox Namespace
Session-first wrappers around the core blackbox.* functions. Automatically fills chainId = -1 and uses the session signer. Calls session.ensureValid() before each request.
web2.blackbox.payload
// Encrypt
const encrypted = await web2.blackbox.payload.encryptPayload({
session,
secretId: 42,
plaintext: 'Hello, Web2!',
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
readClient: sdk.readClient,
outputFormat: 'hex', // optional, default: 'hex'
});
// Returns: { cifer: string, encryptedMessage: string }
// Decrypt
const decrypted = await web2.blackbox.payload.decryptPayload({
session,
secretId: 42,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
readClient: sdk.readClient,
inputFormat: 'hex', // optional
});
// Returns: { decryptedMessage: string }
web2.blackbox.files
// Encrypt file
const job = await web2.blackbox.files.encryptFile({
session,
secretId: 42,
file: myFile,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
// Returns: { jobId: string, message: string }
// Decrypt file
const job = await web2.blackbox.files.decryptFile({
session,
secretId: 42,
file: encryptedFile,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
// Decrypt from existing encrypt job
const job = await web2.blackbox.files.decryptExistingFile({
session,
secretId: 42,
encryptJobId: previousJobId,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
web2.blackbox.jobs
// These are re-exported from core (no session needed):
const status = await web2.blackbox.jobs.getStatus(jobId, sdk.blackboxUrl);
const final = await web2.blackbox.jobs.pollUntilComplete(jobId, sdk.blackboxUrl, {
onProgress: (job) => console.log(`${job.progress}%`),
});
// These require a session:
const blob = await web2.blackbox.jobs.download(jobId, {
session,
secretId: 42,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
await web2.blackbox.jobs.deleteJob(jobId, {
session,
secretId: 42,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
const jobs = await web2.blackbox.jobs.list({
session,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
includeExpired: false,
});
const stats = await web2.blackbox.jobs.dataConsumption({
session,
blackboxUrl: sdk.blackboxUrl,
readClient: sdk.readClient,
});
web2.createClient Factory
Stateful client that stores session, blackboxUrl, and readClient so you don't pass them on every call.
import { web2 } from 'cifer-sdk';
const client = web2.createClient({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
readClient: sdk.readClient,
});
// Session is auto-stored after creation
await client.createManagedSession({
principalId: 'your-uuid',
ed25519Signer,
});
// No need to pass session or blackboxUrl!
const secret = await client.createSecret();
const encrypted = await client.payload.encryptPayload({
secretId: secret.secretId,
plaintext: 'Hello!',
});
const decrypted = await client.payload.decryptPayload({
secretId: secret.secretId,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
});
// Other client methods:
await client.listSecrets();
await client.setDelegate({ secretId: 42, delegatePrincipalId: 'uuid' });
await client.getByEmail('colleague@example.com');
client.setSession(anotherSession); // manually replace stored session
The Web2Client interface provides: session, blackboxUrl, readClient, createManagedSession(), useExistingSessionKey(), setSession(), createSecret(), listSecrets(), setDelegate(), requestPermit(), getByEmail(), payload.*, files.*, jobs.*.
Error Handling
All SDK errors extend CiferError with typed subclasses:
CiferError
├── ConfigError (code: CONFIG_ERROR)
│ ├── DiscoveryError
│ └── ChainNotSupportedError
├── AuthError (code: AUTH_ERROR)
│ ├── SignatureError
│ ├── BlockStaleError
│ └── SignerMismatchError
├── BlackboxError (code: BLACKBOX_ERROR)
│ ├── EncryptionError
│ ├── DecryptionError
│ ├── JobError
│ └── SecretNotReadyError
├── KeyManagementError (code: KEY_MANAGEMENT_ERROR)
│ ├── SecretNotFoundError
│ └── NotAuthorizedError
├── CommitmentsError (code: COMMITMENTS_ERROR)
│ ├── CommitmentNotFoundError
│ ├── IntegrityError
│ ├── InvalidCiferSizeError
│ └── PayloadTooLargeError
├── Web2Error (code: WEB2_ERROR)
│ ├── Web2SessionError (session expired, cannot renew)
│ └── Web2AuthError (registration, OTP, password errors)
└── FlowError (code: FLOW_ERROR)
├── FlowAbortedError
└── FlowTimeoutError
Type Guards
import {
isCiferError,
isBlockStaleError,
isSecretNotReadyError,
isWeb2Error,
isWeb2SessionError,
SecretNotFoundError,
SecretNotReadyError,
CommitmentNotFoundError,
} from 'cifer-sdk';
Error Handling Example
try {
await blackbox.payload.encryptPayload({ ... });
} catch (error) {
if (isBlockStaleError(error)) {
console.log('RPC returning stale blocks');
} else if (error instanceof SecretNotReadyError) {
console.log('Wait for secret to sync');
} else if (error instanceof SecretNotFoundError) {
console.log('Secret not found:', error.secretId);
} else if (error instanceof CommitmentNotFoundError) {
console.log('No data for key:', error.dataId);
} else if (isWeb2SessionError(error)) {
console.log('Web2 session expired or cannot renew');
} else if (isWeb2Error(error)) {
console.log('Web2 error:', error.message);
} else if (isCiferError(error)) {
console.log('CIFER error:', error.code, error.message);
} else {
throw error;
}
}
Common Scenarios
| Error | Cause | Solution |
|---|---|---|
| "Block number is too old" | RPC issues | SDK auto-retries 3x; check RPC reliability; minimize delay between signing and API call |
| "Secret is syncing" | Key generation in progress | Wait 30-60s; use isSecretReady() or createSecretAndWaitReady flow |
| "Signature verification failed" | Wrong signing method | Use EIP-191 personal_sign (not eth_sign or typed data) |
| "Not authorized" | Not owner/delegate | Check with isAuthorized() |
| "No active Web2 session" | Session not created or expired | Call createManagedSession() or pass session explicitly |
| "Web2 session expired" | Existing-key session cannot renew | Recreate session externally |
| "OTP verification failed" | Invalid or expired OTP | Use resendOtp() and try again |
Complete Examples
Browser: Encrypt/Decrypt Message
import { createCiferSdk, Eip1193SignerAdapter, blackbox } from 'cifer-sdk';
async function encryptDecryptExample() {
const sdk = await createCiferSdk({
blackboxUrl: 'https://blackbox.cifersecurity.com:3010',
});
const signer = new Eip1193SignerAdapter(window.ethereum);
const chainId = 8453;
const secretId = 123n;
// Encrypt
const encrypted = await blackbox.payload.encryptPayload({
chainId,
secretId,
plaintext: 'Hello, CIFER!',
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
// Decrypt
const decrypted = await blackbox.payload.decryptPayload({
chainId,
secretId,
encryptedMessage: encrypted.encryptedMessage,
cifer: encrypted.cifer,
signer,
readClient: sdk.readClient,
blackboxUrl: sdk.blackboxUrl,
});
console.log('Decrypted:', decrypted.decryptedMe
…(truncated)