Every encryption scheme has an uncomfortable truth at its center: whoever holds the private key can read everything. That makes the private key a single point of failure in two directions. If it is stolen, or its holder goes rogue, every ciphertext is exposed. If it is lost, every ciphertext is gone forever.
Pirates had a classic answer to the same problem: tear the treasure map into pieces, and give each crew member one. Nobody can sail off with the gold alone. But a torn map has two flaws. Every piece still shows part of the route, so a crew member with a good piece and a good guess might get there first. And if a single crew member walks the plank with their piece, nobody finds the treasure ever again, because every piece is needed.
🙋♂️ You may consider to enroll my top-rated cryptography course on Udemy

Cryptography has a much better way to tear the map, called threshold decryption. The private key is split into n pieces, called shares, so that any t of them are enough to decrypt, while fewer than t reveal nothing at all—not even part of the route. For instance, a board of 5 directors can let any 3 of them decrypt the company’s encrypted data, so neither a single director can read it alone nor losing 2 of them blocks access. In this post, we’ll see how it works and build it with LightPHE in Python, Go, Java and TypeScript.
How Threshold Decryption Works
Splitting the Key with Shamir’s Secret Sharing
The private key is split with Shamir’s secret sharing. A dealer picks a random polynomial of degree t - 1 whose constant term is the secret key, and gives each shareholder one point on it:
f(x) = sk + a_1 x + a_2 x^2 + ... + a_(t-1) x^(t-1) random a_1, ..., a_(t-1) share of shareholder i = f(i) for i = 1, 2, ..., n
Any t points determine a polynomial of degree t - 1, so any t shareholders could rebuild sk = f(0) with Lagrange interpolation. With only t - 1 points, every possible secret is equally likely, so fewer shareholders learn literally nothing.
This is where our treasure map gets fixed. A torn piece of map shows part of the route, but a single point on a random polynomial says nothing about the polynomial’s value at zero, where the key is hidden, so a share reveals nothing about the key. And since any t points are enough, the crew no longer needs every piece: a 3-of-5 crew can lose 2 members and still find the treasure.
Decrypting Without Ever Rebuilding the Key
Here is the clever part: the shareholders never put the key back together. If they did, whoever assembled it would become the single point of failure again. Instead, each shareholder applies its own share to the ciphertext, producing a partial decryption, and the Lagrange interpolation happens on the partial decryptions, in the exponent. Take ElGamal, whose ciphertext is a pair (c1, c2) and whose decryption computes m = c2 / c1^sk:
partial decryption of shareholder i: d_i = c1^(share_i)
combining any t of them: c1^sk = d_1^(λ_1) * d_2^(λ_2) * ... * d_t^(λ_t)
m = c2 / c1^sk
λ_i are the lagrange coefficients of the participating shareholders
A single partial decryption reveals nothing about the plaintext, and combining them needs no secret at all, so anyone holding the public key can be the combiner. The key itself never exists in one place after the split.
Required Cryptographic Features
1. Decryption that is an exponentiation with the private key. The trick above only works if decryption raises something to the power of the private key, as c1^sk in ElGamal or c^d in Paillier. Then shares of the key turn into partial decryptions, and Lagrange coefficients combine them in the exponent.
2. Lagrange coefficients that can be computed. Lagrange coefficients are fractions, so they need division modulo the order of the group. For the ElGamal family, the order is a public prime q, which is why ElGamal and Exponential ElGamal keys must be generated with prime_order = True for threshold decryption. For Paillier and Damgard-Jurik, the order is secret, since it reveals the factorization of the modulus. The fix comes from Shoup’s threshold signatures: multiply every coefficient by Δ = n!, where n is the number of shareholders, which turns them into integers, and divide the extra factor out at the end.
3. Homomorphism, to decrypt only aggregates. Strictly speaking, threshold decryption doesn’t need homomorphic encryption, but together they are far more powerful. Anyone can add up ciphertexts with the public key, and the shareholders decrypt only the final result—a vote tally, a total, an average—never an individual ciphertext. This is exactly what we did in our verifiable referendum, where trustees decrypt the tally, not the ballots.
4. Verifiable partial decryptions. Threshold decryption spreads trust, but it doesn’t make shareholders honest. A cheating shareholder can send a wrong partial decryption, and the combined plaintext will be silently wrong. Worse, since Lagrange coefficients are public, a cheater can shift the result by any amount it likes, e.g. add 50 votes to its own party. So each shareholder can attach a zero-knowledge proof that its partial decryption was computed with its own share, and the combiner can discard any partial decryption whose proof fails. For the ElGamal family, this is a Chaum-Pedersen proof that works with any prime order keys. For Paillier and Damgard-Jurik, it needs keys generated with safe_primes = True.
Which Algorithms Can Be Used, and Why
LightPHE supports threshold decryption for five cryptosystems, the ones with well-established threshold variants. I ran the examples below against all of them, and asked the others for threshold keys as well:
| Algorithm | Threshold decryption | Notes |
| ElGamal | ✅ | Multiplicatively homomorphic. Keys need prime_order = True |
| Exponential ElGamal | ✅ | Additively homomorphic. Keys need prime_order = True. Decryption solves a discrete logarithm, so plaintexts must stay small |
| Elliptic Curve ElGamal | ✅ | Additively homomorphic with short keys. The base point of the curve already has a prime order, so no extra parameter is needed. Plaintexts must stay small, too |
| Paillier | ✅ | Additively homomorphic with large plaintexts. Shoup’s Δ trick handles the secret group order. Keys need safe_primes = True to prove partial decryptions |
| Damgard-Jurik | ✅ | Same as Paillier, with an even larger plaintext space |
| RSA | ❌ | Threshold RSA exists for signatures—Shoup’s paper above is exactly that—but RSA encryption is deterministic and only multiplicative, a poor fit for protecting data, and LightPHE doesn’t offer it |
| Okamoto-Uchiyama, Benaloh, Goldwasser-Micali, Damgard-Geisler-Kroigaard, Boneh-Goh-Nissim and others | ❌ | LightPHE refuses them with does not support threshold decryption. Threshold variants of these schemes are either not standard or not implemented in LightPHE, so pick one of the five above when you need a quorum |
One more restriction: ciphertexts boosted with Catalano-Fiore to support a multiplication cannot be threshold-decrypted, so threshold decryption works on additive (or, for ElGamal, multiplicative) results only.
Example 1: A Board of Directors
A company’s departments submit encrypted budgets, and the board has 5 directors. Any 3 of them can decrypt the total together, but no director can read it alone, and the board still works when 2 directors are on vacation.
from lightphe import LightPHE
# dealer generates a key pair, and splits the private key into 5 shares, any 3 of them can decrypt
dealer_cs = LightPHE(algorithm_name = "Paillier")
share_keys = dealer_cs.generate_threshold_keys(threshold = 3, parties = 5)
# dealer forgets the private key, and publishes the public key with threshold parameters
dealer_cs.export_keys(target_file = "public.txt", public = True)
# each director receives its own share key secretly
directors = [LightPHE(algorithm_name = "Paillier", keys = keys) for keys in share_keys]
# anyone encrypts and computes with the public key, e.g. departments submit their budgets
public_cs = LightPHE(algorithm_name = "Paillier", key_file = "public.txt")
total = public_cs.encrypt(120) + public_cs.encrypt(80) + public_cs.encrypt(45)
# directors 1, 3 and 5 decrypt the total together
partials = [directors[i].partial_decrypt(total) for i in [0, 2, 4]]
assert public_cs.combine_partial_decryptions(total, partials) == 245
# directors 2, 3 and 4 get the same result
partials = [directors[i].partial_decrypt(total) for i in [1, 2, 3]]
assert public_cs.combine_partial_decryptions(total, partials) == 245
# 2 directors are not enough
try:
public_cs.combine_partial_decryptions(total, partials[:2])
raise AssertionError("2 directors must not be able to decrypt")
except ValueError as err:
print(err) # At least 3 valid partial decryptions from distinct parties are required, got 2
package main
import (
"fmt"
"log"
"os"
"github.com/serengil/lightphe-go/lightphe"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// dealer generates a key pair, and splits the private key into 5 shares, any 3 of them can decrypt
dealerCs, err := lightphe.New(lightphe.Paillier)
check(err)
publicKey, shareKeys, err := dealerCs.GenerateThresholdKeys(3, 5)
check(err)
// dealer publishes the public key with threshold parameters, and discards its own keys
check(os.WriteFile("public.txt", publicKey, 0644))
// each director receives its own share key secretly
var directors []*lightphe.Cryptosystem
for _, keys := range shareKeys {
director, err := lightphe.New(lightphe.Paillier, lightphe.WithKeys(keys))
check(err)
directors = append(directors, director)
}
// anyone encrypts and computes with the public key, e.g. departments submit their budgets
publicCs, err := lightphe.New(lightphe.Paillier, lightphe.WithKeyFile("public.txt"))
check(err)
total, err := publicCs.EncryptInt(120)
check(err)
for _, budget := range []int64{80, 45} {
c, err := publicCs.EncryptInt(budget)
check(err)
total, err = total.Add(c)
check(err)
}
// decrypt partially decrypts the total by the given directors
decrypt := func(indices []int) []*lightphe.PartialDecryption {
var partials []*lightphe.PartialDecryption
for _, i := range indices {
partial, err := directors[i].PartialDecrypt(total, false)
check(err)
partials = append(partials, partial)
}
return partials
}
// directors 1, 3 and 5 decrypt the total together
m, err := publicCs.CombinePartialDecryptions(total, decrypt([]int{0, 2, 4}), false)
check(err)
fmt.Println(m) // 245
// directors 2, 3 and 4 get the same result
partials := decrypt([]int{1, 2, 3})
m, err = publicCs.CombinePartialDecryptions(total, partials, false)
check(err)
fmt.Println(m) // 245
// 2 directors are not enough
if _, err := publicCs.CombinePartialDecryptions(total, partials[:2], false); err != nil {
fmt.Println(err) // at least 3 valid partial decryptions from distinct parties are required, got 2
} else {
log.Fatal("2 directors must not be able to decrypt")
}
}
import io.github.serengil.lightphe.LightPHE;
import io.github.serengil.lightphe.model.Ciphertext;
import io.github.serengil.lightphe.model.Keys;
import io.github.serengil.lightphe.model.PartialDecryption;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.List;
// dealer generates a key pair, and splits the private key into 5 shares, any 3 of them can decrypt
LightPHE dealerCs = LightPHE.builder().algorithmName("Paillier").build();
List<Keys> shareKeys = dealerCs.generateThresholdKeys(3, 5);
// dealer forgets the private key, and publishes the public key with threshold parameters
dealerCs.exportKeys(Path.of("public.txt"), true);
// each director receives its own share key secretly
List<LightPHE> directors = shareKeys.stream()
.map(keys -> LightPHE.builder().algorithmName("Paillier").keys(keys).build())
.toList();
// anyone encrypts and computes with the public key, e.g. departments submit their budgets
LightPHE publicCs = LightPHE.builder()
.algorithmName("Paillier")
.keyFile(Path.of("public.txt"))
.build();
Ciphertext total = publicCs.encrypt(120).add(publicCs.encrypt(80)).add(publicCs.encrypt(45));
// directors 1, 3 and 5 decrypt the total together
List<PartialDecryption> partials = List.of(0, 2, 4).stream()
.map(i -> directors.get(i).partialDecrypt(total))
.toList();
assert publicCs.combinePartialDecryptions(total, partials).equals(BigInteger.valueOf(245));
// directors 2, 3 and 4 get the same result
List<PartialDecryption> others = List.of(1, 2, 3).stream()
.map(i -> directors.get(i).partialDecrypt(total))
.toList();
assert publicCs.combinePartialDecryptions(total, others).equals(BigInteger.valueOf(245));
// 2 directors are not enough
try {
publicCs.combinePartialDecryptions(total, others.subList(0, 2));
throw new AssertionError("2 directors must not be able to decrypt");
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // At least 3 valid partial decryptions from distinct parties are required, got 2
}
import { LightPHE } from 'lightphe-ts';
// dealer generates a key pair, and splits the private key into 5 shares, any 3 of them can decrypt
const dealerCs = new LightPHE({ algorithmName: 'Paillier' });
const shareKeys = dealerCs.generateThresholdKeys(3, 5);
// dealer forgets the private key, and publishes the public key with threshold parameters
dealerCs.exportKeys('public.txt', true);
// each director receives its own share key secretly
const directors = shareKeys.map((keys) => new LightPHE({ algorithmName: 'Paillier', keys }));
// anyone encrypts and computes with the public key, e.g. departments submit their budgets
const publicCs = new LightPHE({ algorithmName: 'Paillier', keyFile: 'public.txt' });
const total = publicCs.encrypt(120).add(publicCs.encrypt(80)).add(publicCs.encrypt(45));
// directors 1, 3 and 5 decrypt the total together
const partials = [0, 2, 4].map((i) => directors[i].partialDecrypt(total));
console.assert(publicCs.combinePartialDecryptions(total, partials) === 245n);
// directors 2, 3 and 4 get the same result
const others = [1, 2, 3].map((i) => directors[i].partialDecrypt(total));
console.assert(publicCs.combinePartialDecryptions(total, others) === 245n);
// 2 directors are not enough
try {
publicCs.combinePartialDecryptions(total, others.slice(0, 2));
throw new Error('2 directors must not be able to decrypt');
} catch (err) {
console.log((err as Error).message); // At least 3 valid partial decryptions from distinct parties are required, got 2
}
Two different groups of 3 directors get the same 245, and 2 directors get an error, not a wrong number. After generate_threshold_keys, the dealer’s cryptosystem drops its private key in Python, Java and TypeScript. In Go, a cryptosystem is immutable, so the dealer keeps its keys: publish the returned public key and discard the dealer, as the Go tab does. The same code works for the ElGamal family as well: just build the dealer with Exponential-ElGamal and prime_order = True, or with EllipticCurve-ElGamal.
Example 2: Hospitals and a Cheating Shareholder
Five hospitals want to publish the total number of flu patients in their region this year, without revealing their individual numbers. Each hospital encrypts its count, the counts are added up encrypted, and the hospitals hold the 5 shares of the key, any 3 of which can decrypt. But one hospital cheats: instead of its partial decryption of the total, it sends a partial decryption of another ciphertext to distort the published figure.
from lightphe import LightPHE # dealer splits the private key among 5 hospitals, any 3 of them can decrypt. # safe primes let hospitals prove their partial decryptions with Paillier dealer_cs = LightPHE(algorithm_name = "Paillier", safe_primes = True) share_keys = dealer_cs.generate_threshold_keys(threshold = 3, parties = 5) dealer_cs.export_keys(target_file = "public.txt", public = True) hospitals = [LightPHE(algorithm_name = "Paillier", keys = keys) for keys in share_keys] public_cs = LightPHE(algorithm_name = "Paillier", key_file = "public.txt") # each hospital encrypts its number of flu patients this year, and the counts are summed encrypted counts = [1250, 830, 2100, 640, 975] total = public_cs.encrypt(counts[0]) for count in counts[1:]: total = total + public_cs.encrypt(count) # 4 hospitals decrypt partially with proofs, but the first one cheats: # it sends a partial decryption of another ciphertext to bias the result forged = hospitals[0].partial_decrypt(public_cs.encrypt(9999), prove = True) partials = [forged] + [hospital.partial_decrypt(total, prove = True) for hospital in hospitals[1:4]] # anyone verifies each partial decryption with the public key, and catches the cheater assert [public_cs.verify_partial_decryption(total, partial) for partial in partials] == [False, True, True, True] # without verification, the forged partial decryption silently gives a wrong total assert public_cs.combine_partial_decryptions(total, partials) != 5795 # with verification, it is discarded and the 3 honest hospitals still decrypt the total assert public_cs.combine_partial_decryptions(total, partials, verify = True) == 5795
package main
import (
"fmt"
"log"
"github.com/serengil/lightphe-go/lightphe"
)
func check(err error) {
if err != nil {
log.Fatal(err)
}
}
func main() {
// dealer splits the private key among 5 hospitals, any 3 of them can decrypt.
// safe primes let hospitals prove their partial decryptions with Paillier
dealerCs, err := lightphe.New(lightphe.Paillier, lightphe.WithSafePrimes(true))
check(err)
publicKey, shareKeys, err := dealerCs.GenerateThresholdKeys(3, 5)
check(err)
publicCs, err := lightphe.New(lightphe.Paillier, lightphe.WithKeys(publicKey))
check(err)
var hospitals []*lightphe.Cryptosystem
for _, keys := range shareKeys {
hospital, err := lightphe.New(lightphe.Paillier, lightphe.WithKeys(keys))
check(err)
hospitals = append(hospitals, hospital)
}
// each hospital encrypts its number of flu patients this year, and the counts are summed encrypted
counts := []int64{1250, 830, 2100, 640, 975}
total, err := publicCs.EncryptInt(counts[0])
check(err)
for _, count := range counts[1:] {
c, err := publicCs.EncryptInt(count)
check(err)
total, err = total.Add(c)
check(err)
}
// 4 hospitals decrypt partially with proofs, but the first one cheats:
// it sends a partial decryption of another ciphertext to bias the result
other, err := publicCs.EncryptInt(9999)
check(err)
forged, err := hospitals[0].PartialDecrypt(other, true)
check(err)
partials := []*lightphe.PartialDecryption{forged}
for _, hospital := range hospitals[1:4] {
partial, err := hospital.PartialDecrypt(total, true)
check(err)
partials = append(partials, partial)
}
// anyone verifies each partial decryption with the public key, and catches the cheater
for i, partial := range partials {
valid, err := publicCs.VerifyPartialDecryption(total, partial)
check(err)
fmt.Printf("hospital %d honest: %v\n", i+1, valid) // false, then true, true, true
}
// without verification, the forged partial decryption silently gives a wrong total
wrong, err := publicCs.CombinePartialDecryptions(total, partials, false)
check(err)
fmt.Println(wrong) // a random looking number, not 5795
// with verification, it is discarded and the 3 honest hospitals still decrypt the total
right, err := publicCs.CombinePartialDecryptions(total, partials, true)
check(err)
fmt.Println(right) // 5795
}
import io.github.serengil.lightphe.LightPHE;
import io.github.serengil.lightphe.model.Ciphertext;
import io.github.serengil.lightphe.model.Keys;
import io.github.serengil.lightphe.model.PartialDecryption;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
// dealer splits the private key among 5 hospitals, any 3 of them can decrypt.
// safe primes let hospitals prove their partial decryptions with Paillier
LightPHE dealerCs = LightPHE.builder().algorithmName("Paillier").safePrimes(true).build();
List<Keys> shareKeys = dealerCs.generateThresholdKeys(3, 5);
dealerCs.exportKeys(Path.of("public.txt"), true);
List<LightPHE> hospitals = shareKeys.stream()
.map(keys -> LightPHE.builder().algorithmName("Paillier").keys(keys).build())
.toList();
LightPHE publicCs = LightPHE.builder()
.algorithmName("Paillier")
.keyFile(Path.of("public.txt"))
.build();
// each hospital encrypts its number of flu patients this year, and the counts are summed encrypted
List<Long> counts = List.of(1250L, 830L, 2100L, 640L, 975L);
Ciphertext total = publicCs.encrypt(counts.get(0));
for (long count : counts.subList(1, counts.size())) {
total = total.add(publicCs.encrypt(count));
}
// 4 hospitals decrypt partially with proofs, but the first one cheats:
// it sends a partial decryption of another ciphertext to bias the result
List<PartialDecryption> partials = new ArrayList<>();
partials.add(hospitals.get(0).partialDecrypt(publicCs.encrypt(9999), true));
for (LightPHE hospital : hospitals.subList(1, 4)) {
partials.add(hospital.partialDecrypt(total, true));
}
// anyone verifies each partial decryption with the public key, and catches the cheater
List<Boolean> honest = new ArrayList<>();
for (PartialDecryption partial : partials) {
honest.add(publicCs.verifyPartialDecryption(total, partial));
}
assert honest.equals(List.of(false, true, true, true));
// without verification, the forged partial decryption silently gives a wrong total
assert !publicCs.combinePartialDecryptions(total, partials).equals(BigInteger.valueOf(5795));
// with verification, it is discarded and the 3 honest hospitals still decrypt the total
assert publicCs.combinePartialDecryptions(total, partials, true).equals(BigInteger.valueOf(5795));
import { LightPHE } from 'lightphe-ts';
// dealer splits the private key among 5 hospitals, any 3 of them can decrypt.
// safe primes let hospitals prove their partial decryptions with Paillier
const dealerCs = new LightPHE({ algorithmName: 'Paillier', safePrimes: true });
const shareKeys = dealerCs.generateThresholdKeys(3, 5);
dealerCs.exportKeys('public.txt', true);
const hospitals = shareKeys.map((keys) => new LightPHE({ algorithmName: 'Paillier', keys }));
const publicCs = new LightPHE({ algorithmName: 'Paillier', keyFile: 'public.txt' });
// each hospital encrypts its number of flu patients this year, and the counts are summed encrypted
const counts = [1250, 830, 2100, 640, 975];
const total = counts
.map((count) => publicCs.encrypt(count))
.reduce((sum, c) => sum.add(c));
// 4 hospitals decrypt partially with proofs, but the first one cheats:
// it sends a partial decryption of another ciphertext to bias the result
const forged = hospitals[0].partialDecrypt(publicCs.encrypt(9999), { prove: true });
const partials = [
forged,
...hospitals.slice(1, 4).map((hospital) => hospital.partialDecrypt(total, { prove: true })),
];
// anyone verifies each partial decryption with the public key, and catches the cheater
const honest = partials.map((partial) => publicCs.verifyPartialDecryption(total, partial));
console.assert(honest.join() === 'false,true,true,true');
// without verification, the forged partial decryption silently gives a wrong total
console.assert(publicCs.combinePartialDecryptions(total, partials) !== 5795n);
// with verification, it is discarded and the 3 honest hospitals still decrypt the total
console.assert(publicCs.combinePartialDecryptions(total, partials, { verify: true }) === 5795n);
Without verification, the forged partial decryption doesn’t cause an error—it just produces a meaningless number, over 300 digits long in my run, instead of 5795. A cheater who is more careful than ours could make that number look perfectly plausible. With verification, anyone holding the public key can check each partial decryption and see exactly which hospital cheated, and the combiner discards the forged one and logs a warning: Partial decryption of party 1 has an invalid proof. It is discarded. Since 3 honest hospitals remain, the correct total is still restored. Java snippets use assert for brevity, so run them with -ea to enable assertions.
More Scenarios
- E-voting: Trustees nominated by competing parties hold the shares of the election key, so no single trustee can decrypt a ballot, and a trustee who tries to add votes to its own party is caught by the proofs.
- Federated learning: Thousands of phones encrypt their model updates, a server adds them up, and a committee of servers decrypts only the sum, so no single server sees an individual update or slips in a poisoned one.
- Backup recovery: A company encrypts its backups, and any 3 of 5 executives can restore them. A single compromised laptop doesn’t expose the backups, and losing two keys doesn’t lose the backups either.
- Auditing: Two of three auditors are required to open the total of a company’s encrypted transactions, so a single bribed auditor can neither peek early nor misreport the total.
- Sealed-bid auctions: Bids stay encrypted until the auction closes, and only a quorum of independent auctioneers can open them, so no single insider can leak the leading bid.
Caveats
- The dealer must be trusted: The dealer knows the whole private key while splitting it. LightPHE drops the key right after the split, but if a trusted dealer is not acceptable, the shareholders need to generate the key together with a distributed key generation protocol, which LightPHE doesn’t provide.
- t colluding shareholders can decrypt anything: The threshold protects against fewer than
tshareholders. Iftof them collude, they can decrypt any ciphertext, including individual ballots or records. Choose shareholders with competing interests, and have them refuse to partially decrypt anything but aggregates. - Shares are static: If shares leak one by one over the years, an attacker eventually collects
tof them. Some systems refresh shares periodically without changing the key, which LightPHE doesn’t do, so rotate keys instead when shareholders change. - Prove partial decryptions: Without proofs, a single cheating shareholder silently corrupts the result. Turn on
proveandverifywhenever shareholders don’t fully trust each other, which is usually the reason to use threshold decryption in the first place.
Conclusion
Threshold decryption removes the single point of failure of a private key. Shamir’s secret sharing splits the key into shares, each shareholder decrypts partially with its own share, and Lagrange interpolation in the exponent combines any t partial decryptions into the plaintext, without ever rebuilding the key. That requires a decryption that is an exponentiation with the private key, and Lagrange coefficients that can be computed, which is why LightPHE supports it for ElGamal, Exponential ElGamal, Elliptic Curve ElGamal, Paillier and Damgard-Jurik. Combined with homomorphic encryption, shareholders only ever decrypt aggregates, and with proofs of partial decryption, a cheating shareholder is caught instead of corrupting the result.
In other words: no single person can read your data, no single person can lose it, and no single person can lie about it—all in a few lines of Python, Go, Java or TypeScript.
Support this blog financially if you do like!
