Version 1.0
CRAN release: 2026-10-03
Encryption backends
- New OpenFHE backend, via the
openfhe.Rpackage. 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
-
Siteis now abstract, with two concrete kinds.LocalSiteholds its data in the current R session.RemoteSiteis for a party whose contribution is produced elsewhere; no transport ships with the package, so subclass it and register acontribute()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
NAmeansthetais non-evaluable at a site that answered, whilesite_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)replacesmake_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 frozenmake_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()andpartial_decrypt(), dispatching onSite. ARemoteSitesubclass 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. TheSitemethod stores the bundle; theRemoteSitemethod 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 intosite@state$params. - Public parameters are now typed S7 objects —
PublicParamswithOpenFHEParamsand the frozenPaillierParams— 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-sidepartial_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. -
?RemoteSitenow separates three cases that were previously run together: aLocalSitedemonstration 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()anddecrypt()areopenfhe.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 followopenfhe.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 — andsite_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 noencrypt()method onMaster. 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 vialen. - 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 of0.9into0and 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;?ThresholdMastersays so. -
make_ckks_master()requires a CKKS context. Exact-integer work goes throughmake_threshold_master(), which is scheme-agnostic by design. - Crypto contexts, key pairs, public keys,
stateenvironments, andcontribution_fnare now typed S7 properties rather thanclass_any, and a party’snamemust be a single non-empty string.
Data
-
DLBCL(235 patients: survival, subgroup, gene-expression signatures) andDLBCL_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, andsimilarity_results. Each is produced by adata-raw/script from its vignette’s own chunks;HOMOMORPHER_RECOMPUTE=trueruns 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
R6dependency is removed. - New
PaillierCiphertextclass 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 S3Opsgroup 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
@andchooseOpsMethod).digestdropped from Suggests. Messaging goes throughcli.
