NEWS
homomorpheR 1.0 (2026-10-03)
Encryption backends
- New OpenFHE backend, via the
openfhe.R package. CKKS carries
real-valued data natively, so the fixed-point denominator the Paillier
path needs is gone; BFV and BGV carry exact integers. The scheme is
read back from the crypto context, so one master drives any of them.
- The Paillier implementation remains and is frozen: it is still
exported and tested, but new work should use the OpenFHE backends.
Multi-party actors
Site is now abstract, with two concrete kinds. LocalSite holds its
data in the current R session. RemoteSite is for a party whose
contribution is produced elsewhere; no transport ships with the
package, so subclass it and register a contribute() method.
contribute(site, theta) is the single call a master makes on a site.
It returns the site's contribution already encrypted, so no
individual cleartext value reaches the aggregator.
- Two failure modes are deliberately distinct: returning
NA means
theta is non-evaluable at a site that answered, while
site_unavailable() signals that the site could not be reached and
aborts the round rather than silently changing the set of sites being
summed over.
make_worker(name, data, contribution_fn) replaces
make_site(name, data, local_fn).
- Masters:
make_ckks_master() when one party may hold the secret key,
make_threshold_master() when none may, and the frozen
make_master() for Paillier.
master_aggregate(master, theta) runs the star (master/worker)
topology that distcomp- and DataSHIELD-style analyses use.
round_robin_chain() / run_round_robin() remain for the frozen
Paillier chain idiom.
Threshold key generation
make_threshold_master(name, cc, sites) runs the key-generation chain
through the sites: each generates its own secret share, keeps it,
and passes on only a public key. The master holds the crypto context
and the joint public key, and no secret material at all.
- Sites must therefore be constructed before the master, since the joint
public key is a function of all of them.
decrypt() recovers a value by asking every site for a partial
decryption and fusing the results. No party, the master included, can
decrypt alone.
- New generics
keygen_round() and partial_decrypt(), dispatching on
Site. A RemoteSite subclass must implement both; the defaults
refuse rather than generate a remote party's share locally.
- The help page states what the construction does not defend against:
participants are assumed to follow the protocol, and a deviating site
can return a dishonest contribution or a malformed partial that
corrupts the result silently.
Setup, and what a remote site must implement
set_public_params(site, params) is the setup seam: the one moment a
coordinating party hands a site anything, apart from a round itself.
The Site method stores the bundle; the RemoteSite method
refuses, because storing it would configure the local proxy and
leave the far endpoint untold. Wiring a remote subclass that has not
implemented provisioning now fails immediately instead of producing a
site that looks configured and is not.
site_params(site) reads back what a site holds, and errors if it
was never configured. Use it instead of reaching into
site@state$params.
- Public parameters are now typed S7 objects —
PublicParams with
OpenFHEParams and the frozen PaillierParams — rather than a list
with a scheme string. "The bundle carries no secret material" is now
a property of the class rather than a promise about a list, and the
objects print to show it.
master_aggregate() checks each reply before adding it to a total: a
site that answered in cleartext, or with a value produced under some
other key, is refused. Previously a cleartext reply was folded in by
ordinary scalar addition and the round returned the right answer,
having been handed the one quantity the protocol exists to hide.
decrypt() and site-side partial_decrypt() likewise verify
that a value belongs to this protocol's key, using OpenFHE's key tag.
A site therefore refuses to apply its own share to a ciphertext from
a protocol it did not join.
make_threshold_master() rejects a site listed twice, two sites with
the same name, and a site already serving another protocol; a
ceremony that fails part-way rolls back, leaving the sites it had
visited clean enough to retry.
?RemoteSite now separates three cases that were previously run
together: a LocalSite demonstration models the protocol's roles in
one R session and is not a trust boundary; a single-decrypter
deployment relies on honest execution rather than cryptography; only
a remote threshold deployment gives a real party boundary, and only
if your transport, authentication, and key storage provide one.
Encryption surface
encrypt() and decrypt() are openfhe.R's generics, imported,
extended, and re-exported. homomorpheR no longer defines generics of
its own under these names; its protocol-actor methods and the frozen
Paillier methods register on the upstream generics, so there is one
method table per verb and the class of the first argument selects
the layer. Argument names follow openfhe.R, which follows the
OpenFHE C++ signatures; every call in this package is positional.
See ?actor-encryption.
encrypt(site, value) is the one encryption entry point a party
needs. A site holds its public parameters, so it does not fetch them
and hand them back: the site is the whole of the argument.
encrypt(params, value) serves anyone holding a bundle without
being a site — a querier, say — and site_params(site) still hands
one out.
- A site is autonomous once configured. It is given its public
parameters once, and from then on computes and encrypts without
consulting anyone. There is deliberately no exported function that
reaches from a site back to a master; the master-side
public_params() is not exported either, since fetching it at
encryption time would mean asking for something already held.
- Consequently there is no encryption entry point taking a master:
no
master_encrypt(), and no encrypt() method on Master. Naming
or taking one party would advertise a privilege that does not exist,
and would invite site-side code to hold a master it has no use for.
decrypt(master, ciphertext, len) does take a master, and that
asymmetry is the point: decryption is privileged, requiring secret
material or the standing to convene every site, while encryption is
not. It is vector-aware via len.
- Under BFV and BGV a value the scheme cannot carry is now refused
rather than coerced: a non-integer, a non-finite value, one outside
R's integer range, or one at or beyond half the plaintext modulus.
as.integer() previously turned a contribution of 0.9 into 0
and reported a total of zero without a warning. The one thing no
party can check is the total, which still wraps if it exceeds the
modulus; ?ThresholdMaster says so.
make_ckks_master() requires a CKKS context. Exact-integer work goes
through make_threshold_master(), which is scheme-agnostic by
design.
- Crypto contexts, key pairs, public keys,
state environments, and
contribution_fn are now typed S7 properties rather than
class_any, and a party's name must be a single non-empty string.
Data
DLBCL (235 patients: survival, subgroup, gene-expression
signatures) and DLBCL_gex (235 x 6416 Lymphochip probes).
- Precomputed results of the encrypted chunks, which a vignette build
shows but does not run:
cox_results, cox_threshold_results,
cox_threshold_dp_results, cvxr_consensus,
cvxr_admm_dp_results, and similarity_results. Each is produced
by a data-raw/ script from its vignette's own chunks;
HOMOMORPHER_RECOMPUTE=true runs the chunks for real.
Vignettes
- Thirteen vignettes covering queries and aggregation, model fitting
across sites, prediction and retrieval, and Gaussian-noise
variants; the Paillier-era vignettes have moved to
paillier-archive/ and are no longer built.
Infrastructure
- Migrated from R6 to S7 throughout. The
R6 dependency is removed.
- New
PaillierCiphertext class wraps encrypted values together with
the public key they were encrypted under. R's arithmetic operators
(+, -, * against a cleartext scalar) now dispatch directly on
encrypted values via an S3 Ops group handler, so computations on
encrypted data read like ordinary R arithmetic.
- Paillier API renames:
PaillierKeyPair$new(bits) → paillier_keypair(modulus_bits)
pubkey$encrypt(m) → encrypt(pubkey, m) (S7 generic)
privkey$decrypt(ct) → decrypt(privkey, ct) (S7 generic)
pubkey$add(a, b) / pubkey$sub(a, b) → a + b / a - b
pubkey$mult(ct, k) → ct * k (cleartext scalar)
keys$getPrivateKey() → get_private_key(keys)
privkey$getLambda() → get_lambda(privkey)
- Field access:
keys$pubkey → keys@pubkey, etc.
- Frozen Paillier-era code is separated into its own source file from
the supported actor surface.
- Minimum R version bumped to 4.3.0 (required for S7
@ and
chooseOpsMethod). digest dropped from Suggests. Messaging goes
through cli.
homomorpheR 0.3 (2025-04-09)
homomorpheR 0.2