Node-SEAL Basics (with explanations)
November 8, 2025 · View on GitHub
Import the library
Emscripten-built WASM needs to be initialized asynchronously. Browsers and
some runtimes don't always allow a big wasm to be pulled in synchronously, so
node-seal exposes a factory you must await.
import SEAL from 'node-seal'
// returns a fully instantiated SEAL runtime (wasm + JS glue)
const seal = await SEAL()
Encryption parameters
Creating parameters is the first and most important step. They control:
- security (how hard it is to break)
- capacity (how many operations you can do)
- performance (bigger params = slower/larger ciphertexts)
A good workflow is:
- pick a scheme (
bfv/bgv/ckks) - start with 128-bit security
- start with
polyModulusDegree = 4096 - use helper functions to get coeff/plain modulus
// 1) choose a scheme. BFV/BGV = exact ints, CKKS = approximate reals
const scheme = seal.SchemeType.bfv
// 2) choose a security level (128-bit is a typical baseline)
const security = seal.SecLevelType.tc128
// 3) choose the polynomial modulus degree (must be power of 2)
const polyModulusDegree = 4096
// 4) for BFV/BGV, choose a plaintext modulus size (20 bits is a common start)
const plainBitSize = 20
const parms = new seal.EncryptionParameters(scheme)
// tell SEAL how big the polynomials should be
parms.setPolyModulusDegree(polyModulusDegree)
// choose coefficient moduli compatible with this degree + security
// this helper gives you a "standard" set for BFV at that degree
parms.setCoeffModulus(seal.CoeffModulus.BFVDefault(polyModulusDegree, security))
// BFV/BGV need a plaintext modulus to define the plaintext space
parms.setPlainModulus(
seal.PlainModulus.Batching(polyModulusDegree, plainBitSize)
)
Notes:
- CKKS doesn't use a
PlainModuluslike this. - You can fine-tune by giving your own coeff-modulus bit sizes.
SEALContext
A SEALContext freezes these parameters and checks they make sense. Everything else
(keys, encoders, encryptors, etc.) is created off the same SEALContext so they're all
compatible.
const context = new seal.SEALContext(
parms, // the parameters we just built
true, // expandModChain: pre-compute key switching data
security // enforce the security level we picked
)
// always check this: if it fails, your params weren't acceptable
if (!context.parametersSet()) {
throw new Error('Invalid encryption parameters for this SEALContext.')
}
Keys
We need keys to encrypt/decrypt and sometimes to do advanced ops.
- Secret key: decrypt
- Public key: encrypt
- Relinearization keys: shrink ciphertext size after multiplies
- Galois keys: rotate/permute slots (for batching)
// builds a fresh key set tied to this SEALContext
const keygen = new seal.KeyGenerator(context)
const secretKey = keygen.secretKey() // for decryption
const publicKey = keygen.createPublicKey() // for encryption
const relinKeys = keygen.createRelinKeys() // for relinearization
const galoisKeys = keygen.createGaloisKeys() // for rotations (can be large)
// you can serialize to send/store
const secretB64 = secretKey.saveToBase64(seal.ComprModeType.zstd)
// and later deserialize in another process/machine
const newSecretKey = new seal.SecretKey()
newSecretKey.loadFromBase64(context, secretB64)
Helper instances
These are the things you actually call during HE operations:
- Encoder: turn JS typed arrays → SEAL plaintexts (and back)
- Encryptor: plaintext → ciphertext
- Decryptor: ciphertext → plaintext
- Evaluator: homomorphic ops on ciphertexts (add, multiply, rotate…)
// for BFV/BGV batching
const encoder = new seal.BatchEncoder(context)
// for CKKS you'd use: const encoder = new seal.CKKSEncoder(context);
const encryptor = new seal.Encryptor(context, publicKey)
const decryptor = new seal.Decryptor(context, secretKey)
const evaluator = new seal.Evaluator(context)
Example: encode → encrypt → operate → decrypt
// 1) start with plain JS data
const data = BigInt64Array.from([1n, 2n, 3n])
// 2) encode into a SEAL plaintext (structure suitable for HE)
const plainA = new seal.Plaintext()
encoder.encode(data, plainA)
// 3) encrypt the plaintext → ciphertext
const cipherA = new seal.Ciphertext()
encryptor.encrypt(plainA, cipherA)
// 4) homomorphic op: add ciphertext to itself
// the first argument gets overwritten with the result
evaluator.addInplace(cipherA, cipherA)
// 5) decrypt back to plainA, overwriting the original
decryptor.decrypt(cipherA, plainA)
// 6) decode back to a BigInt64Array
const decoded = encoder.decodeBigInt64(plainA) as BigInt64Array
console.log('decoded:', decoded)