A collection of players wish to play a game of cards without having a centralized server or a trusted peer to hold the state of the game and to ensure fairness.
The question is how can they play this game while each player only trusts their machine and some math.
The first option, described a while ago by the same folks that brought us RSA, is for a 2 player game using a simple commutative encryption function:
- Alice encrypts each card in the deck using her key
$A$ , - Then she shuffles the encrypted deck and sends it to Bob.
$E_A(Deck)$ - Bob randomly picks 5 cards from the encrypted deck and sends them back to Alice.
$E_A(Hand_A)$ - Alice can decrypt them to learn her hand.
$D_A(E_A(Hand_A)) = Hand_A$ - Bob then picks 5 other cards from the deck, encrypts them with his key
$B$ and sends them to Alice.$E_B(E_A(Hand_B))$ - Alice decrypts the now doubly encrypted cards using her key so that the cards remain encrypted only by Bob's key.
These cards get sent back to Bob.
$D_A(E_B(E_A(Hand_B))) = \cancel{D_A}(\cancel{E_A}(E_B(Hand_B))) = E_B(Hand_B)$ - Bob can decrypt these last cards to learn his own hand.
$D_B(E_B(Hand_B)) = Hand_B$
The original paper states that the keys should be revealed at the end to prove that the actual cards have been dealt and that there was no cheating.
The requirement to reveal the keys at the end of the game has some downsides. For a poker game, it's not really ok to reveal your hand if the opponent did not pay to see your hands. It also hints at one of the possible exploits of this otherwise very elegant protocol: The dealer can create a deck with a preferred set of cards instead of using the entire original deck. I suppose there are more, let's see what we can discover down the line.
Players agree on a deck of cards, they each take turns shuffling and masking the deck and then cards are dealt.
Dealing a card to a player, means having every other player unmask the card publicly. Finally, the player getting the card unmasks the card privately and ends up with the card in plain-text.
Showing a card to other players means doing the final unmasking publicly.
To be able to make this masking system work, a commutative encryption scheme is required.
In my exploration of this topic I've often come across the ElGamal scheme as well as claims that it is commutative.
As with many cryptographic schemes, every operation happens within a cyclic group
In this encryption scheme, the plaintext messages are group elements. There are encoding/decoding schemes that can be used to convert arbitrary bit string messages to/from group elements, but I won't cover these since we can pre-define our cards as a collection of random or predetermined group elements.
Consider a group element
- Generate a random secret integer
$x$ in the interval${1,\dots,q-1}$ . This is the private key. - Compute
$h:=g^x$ which is the corresponding public key.
- Generate a random nonce
$y$ in the interval${1,\dots,q-1}$ - Compute
$s:=h^y$ . This is called the shared secret. - Compute
$c_1:=g^y$ . The ephemeral public key. - Compute
$c_2:=m * s$ - The ciphertext is the pair
$(c_1, c_2)$
- Compute the shared secret
$s:=c_1^x$ - recover the original group element
$m:=c_2*s^{-1}$
Since we will be working with masked/unmasked cards as a result of ElGamal encryption operations, we can use the same representation for both masked and unmasked; a pair of group elements.
A masked card is the tuple
An unmasked card can also be written as the pair of group elements
These next steps are based on the paper Mental Poker Revisited
First, the joint public key of all
A player wishing to mask this card would choose a random nonce
The player
The combined operation of shuffling the deck and masking the cards.
- Each player generates one shuffle keypair and a number of re-masking key-pairs (one for each card in the deck).
- Each player shuffles the deck and masks every card using their shuffling key, then passes the shuffled deck to the
next player. - After the deck is shuffled by every player, the order of the cards remains fixed, but each player re-masks each card
in the shuffled deck with a unique key, corresponding to the index of the card in the shuffled deck.
Each card is encoded as 3 points on an elliptic curve (public keys).
- 1 point for the (masked) card value (
msg
) - 1 point for a joint public key of all the players (
pk
) - 1 point for a joint ephemeral public key (
epk
)
Operations on points using snarkyjs@0.7.x:
- Calculate a point (PublicKey) from a Scalar (PrivateKey):
$g^s$ =>Group.generator.scale(s)
- Exponentiation:
$A^s$ =>A.scale(s)
- Add 2 points
$A*B$ =>A.add(B)
- Subtract
$A/B$ =>A.sub(B)
- Compute a shared secret point:
With
$A = g^a$ ;$B = g^b$ ; each can compute$A^b = (g^a)^b = (g^ab) = (g^b)^a = B^a$
Player generates a secret Scalar s
and shares the public key corresponding to it with the other
players ( Group.generator.scale(s)
) )
(msg
, pk
, epk
) => (
generate a nonce Scalar r
as an ephemeral private key
(msg
, pk
, epk
) => (
Masking can be done more than once.
Player uses their secret s
to partially unmask:
(msg
, pk
, epk
) => (
This operation removes the player key from the joint pk
, so the player can no longer participate in
masking/unmasking.
The card is completely unmasked (msg
represents the encoded card value) when pk == Group(0,0)
.
Player partially unmasks a card and re-masks it using a new key
- partially unmask
- generate new secret and compute public key from it
- add player public key to card
- apply one or more masking operations
// init
msg => (msg, pk: 0, epk: 0)
// add player 1 (s1, g^s1)
(msg, 0, 0 ) => (msg, g^s1, 0)
// mask with r1 nonce
(msg, g^s1, 0) => (msg * (g^s1)^r1, g^s1, g^r1)
// add player 2 (s2, g^s2)
(msg * g^s1^r1, g^s1, g^r1) => (msg * g^s1^r1 * g^r1^s2, g^s1 * g^s2, g^r1)
// mask with r2 nonce
(msg * g^s1^r1 * g^r1^s2, g^s1 * g^s2, g^r1) => (msg * g^s1^r1 * g^r1^s2 * (g^s1 * g^s2)^r2, g^s1 * g^s2, g^r1 * g^r2)
=== (msg * g^((s1+s2)(r1+r2)), g^(s1+s2), g^(r1+r2))
// unmasking by player 1 (and removing player 1 from the mask)
(msg * g^((s1+s2)(r1+r2)), g^(s1+s2), g^(r1+r2)) => (msg * g^((s1+s2)(r1+r2)) / g^(r1+r2)^s1, g^(s1+s2) / g^s1, g^(r1+r2))
=== (msg * g^(s2(r1+r2)), g^s2, g^(r1+r2))
// unmasking by player 2 (and removing player 2 from the mask)
(msg * g^(s2(r1+r2)), g^s2, g^(r1+r2)) => (msg * g^(s2(r1+r2)) / g^(r1+r2)^s2, g^s2 / g^s2, g^(r1+r2))
=== (msg, 0, g^(r1+r2))
// when pk reaches 0, no more players are masking the value; epk is also cleared for consistency
(msg, 0, g^(r1+r2)) => (msg, 0, 0) => msg