Diffie-Hellman Key Exchange implemented in lua

What is Diffie-Hellman key exchange?

Diffie-Hellman key exchange is a mathematical method/algorithm to establish a secure connection between two parties over an untrusted channel through the creation of a shared secret between them. The exchange is done in such a way so the server can’t see the shared secret, meaning that any messages encrypted through it are only readable by the intended recipients(and not the server). Diffie-Hellman key exchange is often used in cryptography for things such as end to end encryption.

What are the mathematics?

The mathematics of Diffie-Hellman are defined as following:

  • Private Key = random bytes
  • Public Key = Generator^PrivateKey mod prime
  • SharedSecret = OtherPublicKey^PrivateKey mod prime

The computed shared secret is the same value between both parties, and if it gets computed on the client the server doesn’t have access to it

Hyperparameters

In this implementation of Diffie-Hellman, the hyperparameters used are:

  1. The 2048-bit prime mentioned in rfc3526
  2. A generator of value 2
  3. 256-bit private keys

What issues does the module have?

Unfortunately, lua, unlike some other programming languages(for example python), doesn’t provide exact large integer arithmetic. More specifically lua integers stop being precise after their value surpasses 2^53. To counteract this, I had to handle calculations manually, this was done by doing calculations on lists of smaller numbers, and this in turn caused significant increase in processing time. After many optimizations, this module now takes 0.05 seconds per modular exponentiation. This means that it takes around 100 milliseconds for a user to compute their public key and shared secret, if calculations are done in parallel on two separate clients.

Other than that the module hasn’t been tested against side-channel attacks or attacks of any other form, and probably contains many vulnerabilities related to my code and not the encryption scheme itself. However the module does contain the option to create non-pseudorandom private keys through the use of API calls to random.org

API

The module currently contains the following functions:

  • .P property: key: returns the prime number being used
  • hexify(key): string: converts a generated key to a hex representation
  • randomPrivateKey(): key: creates pseudorandom 256 bit private key
  • securePrivateKey(): key: creates true random 256 bit key using random.org randomness
  • hexPrivateKey(hex: string): key: creates a private key from its hex representation
  • computePublicKey(privateKey: key): key: computes the public key tied to a private key (this is the one supposed to be sent over the network / server boundary)
  • computeSharedSecret(privateKey: key, publicKey: key): key: computes the shared secret between the two parties, this function is meant to be used on the client where the arguments being passed in are the private key of that client and the shared public key of the other recipient.

Example Usage

--Require the module
local DH = require(script.MainModule)

print("Prime:", DH.hexify(DH.P))

local privateKey = DH.randomPrivateKey()
local privateKey2 = DH.randomPrivateKey()

--Private keys are meant to be calculated on the client
--We do everything on the server here for example simplicity 
print("Bob's private key:", DH.hexify(privateKey))
warn("Alice's private key:", DH.hexify(privateKey2))

local t = os.clock()
local publicKey = DH.computePublicKey(privateKey)
local publicKey2 = DH.computePublicKey(privateKey2)
print("Public keys computed in:", os.clock()-t)

print("Bob's public key:", DH.hexify(publicKey))
warn("Alice's public key:", DH.hexify(publicKey2))

t = os.clock()
local sharedSecret1 = DH.computeSharedSecret(privateKey, publicKey2)
local sharedSecret2 = DH.computeSharedSecret(privateKey2, publicKey)
print("Shared secrets computed in:", os.clock()-t)

print("Bob's secret:", DH.hexify(sharedSecret1))
warn("Alice's secret:", DH.hexify(sharedSecret2))

local successful = DH.hexify(sharedSecret1) == DH.hexify(sharedSecret2)
print("Algorithm ran successfully:", successful)

Example Output

Other Uses

Apart from communication between clients, this module can also be used for secure communications between servers in cross-server messaging contexts where the servers are treated as if they’re talking clients and the server in between is Roblox systems.

Where to Find

The module is distributed to the creator store and available through the following link:
https://create.roblox.com/store/asset/72784368950427
It can also be used by simply requiring the module through the id:

local DH = require(72784368950427)
12 Likes

Great module! Have you tried using buffers to optimize processing time?

Update March 2026

I just released some huge optimizations for this module, including:

  • Montgomery Reduction
  • Karatsuba Multiplication
  • hi, lo multiplication for number segments
  • 48-bit number limbs
  • Native code generation

Now the module can run much faster, taking 50 milliseconds on average for a single modular exponentiation. This means that each separate client/recipient doing the calculations, needs 50 milliseconds for the public key and 50 milliseconds for the shared secret, needing a total of 100 milliseconds of computation time to complete a handshake. This means that if we take player ping into consideration, it is now realistic to complete a handshake in under half a second.

1 Like

Not a perfect comparison but Diffie hellman exchange is… slow compared to what else you could use, about 785x slower than even post quantum key exchanges uh yeah and about 120x slower than x25519 makes sense since its doing 2048 bit modexp


This is true, this project was more of a fun exercise to me. The reason I came back to optimize it was because I was curious to see how fast it can run in Roblox. For reference when I first managed to get this running, it was taking 20 seconds to run, now it takes a tenth of a second.

Is it still slow? Absolutely. However given that most of the bottlenecks come from big integer arithmetic (which creates loops of size N that have inner loops of size N) I doubt I can optimize it further, unless there’s something I’m missing or a cool trick I haven’t figured out yet.

1 Like

Yeah when i first saw DH i saw large primes and 1970’s and decided never to touch it :smiley: