library(openfhe.R) knitr::opts_chunk$set( collapse = TRUE, comment = "#>" )
A statistician fitting a Cox model on hospital records, or a data scientist pooling case counts across institutions, often hits the same wall: the data cannot leave the institution that collected it. Privacy regulations, consent agreements, and plain institutional caution all conspire to keep the rows where they are. The usual workaround — summary statistics shipped between sites — works for some questions and falls short for others, especially anything that needs a joint likelihood, an iterative optimizer, or a model owner who is not allowed to see the raw features.
Fully Homomorphic Encryption (FHE) gives us another option.
A data holder encrypts the data once, ships the encrypted
copy to whoever is doing the computation, and that party
can add, multiply, and otherwise manipulate the encrypted
values without ever seeing the underlying numbers. Only the
key holder can decrypt the final answer. The openfhe.R
package wraps the OpenFHE
C++ library so that R users can do this from the same
environment they already use for analysis.
The package's function names mirror the underlying C++ API, which means a handful of crypto-world words show up in arguments and return values. None of them require a cryptography background to use; this is the dictionary you need to read the rest of this vignette.
make_packed_plaintext() (for the integer schemes)
or make_ckks_packed_plaintext() (for real-valued data).
Think of it as the moral equivalent of as.numeric() for
the encryption pipeline.decrypt() plus an unpacker like
get_packed_value() or get_real_packed_value().fhe_context(). It pins down the scheme, the precision
budget, and the layout of the slots. Almost every other
call takes a context as the cc = argument.key_gen() returns a KeyPair with @public and
@secret slots. Encryption uses the public key,
decryption uses the secret key.eval_mult = TRUE to key_gen(), for example) and
travel with the public key when you ship encrypted data.set_length() on the
decrypted result to trim back.That is the whole vocabulary and the rest of this vignette describes the package facilities using only those terms.
The rest of this introduction walks five short demonstrations, in the order most R users will meet them:
openfhe.R can be written to a
file and read back.Each step builds on the previous one. By the end you will
have seen the full set of building blocks that the rest of
the package's vignettes — and the companion homomorpheR
application package (version >= 1.0) — assemble into worked
examples.
The BFV scheme handles exact integer arithmetic modulo a plaintext modulus. There is no rounding error: an addition of two encrypted vectors decrypts to exactly the same vector the cleartext addition would have produced.
A CryptoContext holds the scheme parameters; from it we
generate a public/secret key pair (and the evaluation keys
that authorize multiplication of encrypted values).
library(openfhe.R) cc <- fhe_context("BFV", plaintext_modulus = 65537, multiplicative_depth = 2 ) keys <- key_gen(cc, eval_mult = TRUE)
With keys in hand, encrypt two integer vectors. In openfhe.R,
encryption takes a plaintext object — a packed, scheme-aware
representation of the input — rather than a raw R vector,
so the workflow is "pack, then encrypt".
x <- c(1, 2, 3, 4, 5, 6, 7, 8) y <- c(10, 20, 30, 40, 50, 60, 70, 80) ct_x <- encrypt(keys@public, make_packed_plaintext(cc, x), cc = cc) ct_y <- encrypt(keys@public, make_packed_plaintext(cc, y), cc = cc)
ct_x and ct_y are the encrypted versions of x and y.
A computing party with no access to keys@secret can still
add and multiply them.
ct_sum <- ct_x + ct_y ct_prod <- ct_x * ct_y
Decryption requires the secret key. The set_length() call
trims the result to the original input length, since the
underlying packed representation has many more slots than
we used.
result_sum <- decrypt(ct_sum, keys@secret, cc = cc) result_prod <- decrypt(ct_prod, keys@secret, cc = cc) set_length(result_sum, 8) set_length(result_prod, 8) sum_vec <- get_packed_value(result_sum) prod_vec <- get_packed_value(result_prod) sum_vec prod_vec
The decrypted sum is exactly x + y and the decrypted
product is exactly x * y — element by element, no
approximation. The first entries are
r sum_vec[1] and r prod_vec[1], matching 1 + 10 and
1 * 10. BFV is the right tool whenever the analytic
quantity is an integer count, an exact aggregate, or
anything that should be reproduced bit-for-bit.
Most statistical work involves real numbers. The CKKS scheme
encrypts vectors of doubles and supports addition,
multiplication, and scalar operations with bounded
approximation error. The error is controlled by a scaling_mod_size
parameter at context construction; for the values below it
sits well under one part in a million.
cc <- fhe_context("CKKS", multiplicative_depth = 1, scaling_mod_size = 50, batch_size = 8 ) keys <- key_gen(cc, eval_mult = TRUE)
Encrypt a real-valued vector and run a few operations on it.
x <- c(0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0) ct <- encrypt(keys@public, make_ckks_packed_plaintext(cc, x), cc = cc) ct_doubled <- ct + ct ct_squared <- ct * ct ct_scaled <- ct * 4.0
Decryption returns a packed plaintext; get_real_packed_value()
extracts the underlying doubles.
result <- decrypt(ct_doubled, keys@secret, cc = cc) set_length(result, 8) doubled_vec <- get_real_packed_value(result) doubled_vec
The decrypted "double" of x matches 2 * x to within the
CKKS tolerance. The maximum elementwise error is
max_err <- max(abs(doubled_vec - 2 * x)) max_err
r signif(max_err, 3) — small enough to be invisible at the
precisions reported in most statistical work. CKKS is the
scheme to reach for when the encrypted computation is a
likelihood, a gradient step, a regression coefficient, or
anything else where a real-valued answer with bounded error
is acceptable.
Some computations need comparisons or branching — "is this encrypted value greater than zero?" — which do not translate cleanly into polynomial operations on real numbers. For those tasks OpenFHE provides a separate scheme (BinFHE) that operates on encrypted single bits, each a 0 or 1.
The unit of computation here is the logic gate: a basic
Boolean operation — AND, OR, NOT, and a few relatives —
that takes one or two bits in and returns one bit. Wiring
gates together, the output of one feeding the input of the
next, builds a Boolean circuit that computes a larger
function; a comparator that tests whether one encrypted
number exceeds another, or an adder, is just a fixed
arrangement of gates. BinFHE evaluates one gate at a time on
encrypted bits, and every gate evaluation runs an internal
refresh, so a circuit can be made arbitrarily deep — at the
cost of each gate being comparatively expensive.
ctx <- bin_fhe_context(BinFHEParamSet$STD128, BinFHEMethod$GINX) sk <- bin_key_gen(ctx) bin_bt_key_gen(ctx, sk)
Encrypt two bits and evaluate AND and OR on them.
ct_a <- bin_encrypt(ctx, sk, 1L) ct_b <- bin_encrypt(ctx, sk, 0L) ct_and <- eval_bin_gate(ctx, BinGate$AND, ct_a, ct_b) ct_or <- eval_bin_gate(ctx, BinGate$OR, bin_encrypt(ctx, sk, 1L), bin_encrypt(ctx, sk, 0L)) and_bit <- bin_decrypt(ctx, sk, ct_and) or_bit <- bin_decrypt(ctx, sk, ct_or) and_bit or_bit
AND(1, 0) decrypts to r and_bit and OR(1, 0) decrypts
to r or_bit, as expected. The binfhe-boolean-circuits
vignette explores this scheme in much more detail —
including comparators, arbitrary-function evaluation, and
multi-input gates.
Federated and threshold protocols need to move encrypted values and keys between processes, machines, or institutions. Every object the package produces — contexts, keys, ciphertexts, evaluation keys — has a binary serialization format. The example below writes a few objects to a temporary directory and reads them back; in a real protocol the same calls would write to network sockets or storage buckets.
tdir <- tempdir() fhe_serialize(cc, file.path(tdir, "context.bin")) fhe_serialize(keys@public, file.path(tdir, "pubkey.bin")) fhe_serialize(ct, file.path(tdir, "ciphertext.bin")) serialize_eval_keys(file.path(tdir, "mult_keys.bin"), "mult") cc2 <- fhe_deserialize(file.path(tdir, "context.bin"), "CryptoContext") ct2 <- fhe_deserialize(file.path(tdir, "ciphertext.bin"), "Ciphertext")
cc2 and ct2 are independent objects that behave exactly
like the originals — a different R session, possibly on a
different machine, can decrypt or further process the
encrypted data once it has the appropriate keys.
The previous examples gave one party the full secret key. In a federated setting, no single party should hold enough key material to decrypt on their own. OpenFHE supports threshold decryption: two or more parties each generate a key share, encryption uses the joint public key, and decryption is a small protocol where each party contributes a partial result.
The flow below sketches the two-party version. Party A generates initial keys, Party B daisy-chains its own keys off A's public key, and encryption is done under B's public key (which carries both contributions).
cc_mp <- fhe_context("BFV", plaintext_modulus = 65537, multiplicative_depth = 2, features = c(Feature$MULTIPARTY) ) kp_a <- key_gen(cc_mp) kp_b <- multiparty_key_gen(cc_mp, kp_a@public) ct_mp <- encrypt(kp_b@public, make_packed_plaintext(cc_mp, 1:8), cc = cc_mp)
Decryption proceeds in three steps: a lead partial decryption from the first party, a main partial decryption from the second party, and a fusion step that combines the partials into the final plaintext.
partial_a <- multiparty_decrypt_lead(cc_mp, kp_a@secret, ct_mp) partial_b <- multiparty_decrypt_main(cc_mp, kp_b@secret, ct_mp) result_mp <- multiparty_decrypt_fusion(cc_mp, partial_a, partial_b) set_length(result_mp, 8) mp_vec <- get_packed_value(result_mp) mp_vec
The decrypted vector matches 1:8 — neither party alone
could have produced it, but together they recovered the
plaintext. This is the foundation that the worked
applications build on. Cox regression across hospitals,
consensus ADMM for distributed convex optimization, secure
inference, and privacy-preserving aggregation all reduce
to "encrypt under a joint key, run the optimizer over the
encrypted channel, threshold-decrypt the final answer".
Two further vignettes in openfhe.R itself dig into the schemes in
depth:
ckks-bootstrapping — what
to do when a long CKKS computation runs out of
multiplicative depth, including the multi-party variant
that composes with the threshold flow above.binfhe-boolean-circuits
— comparators, arbitrary-function evaluation, and the
full set of BinFHE operations.For worked statistical applications — Cox regression, MLE, CVXR-based
consensus ADMM, encrypted regression, secure inference, and
privacy-preserving aggregation, plus differentially private variants —
see the companion application package
homomorpheR
(version >= 1.0). The
two packages are siblings: openfhe.R provides the encryption
primitives, homomorpheR hosts the statistical demos that run on top
of them, ordered from simple aggregation through to advanced threshold
and differential privacy workflows.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.