Ephemeral, Edge, End-to-End Encrypted Direct Messaging
5EDM uses the recent Hybrid Public Key Encryption (HPKE) standard to establish an end-to-end encrypted and deniable messaging session between two parties. New keys are generated before each session, providing anonymity and forward-secrecy across sessions. With no persistent storage of keys or messages the app's only dependency is Deno Deploy, an edge computing platform with a cross-region message bus.
Note: This is a proof-of-concept. Use Signal if you need the real deal.
In addition to deno
you'll need npx
installed to compile tailwindcss.
The app is built on fresh. To start it, run:
deno task start
This will watch the project directory and restart as necessary.
The app is deployed to Deno Deploy via github actions.
The pseudocode below borrows definitions from the spec unless otherwise defined. The app uses the hpke-js implementation of HPKE.
pkR
is generated by the Recipient and preshared with the Sender over a secure channel.
pkS, skS = GenerateKeyPair()
enc, contextS = SetupAuthS(pkR, info, skS)
channelId = LabeledExtract(0, "channel_id", pkR)[0:16]
ciphertext = contextS.Seal(channelId, greeting)
enc2, ciphertext2 = Seal(pkR, info, ciphertext, pkS)
The Sender generates a key pair and sets up an authenticated encryption context using the preshared pkR
and their private key. The context is used to encrypt a greeting using a channelId
derived from pkR
as additional data. To protect metadata, the single-shot API is used to encrypt pkS
using the first ciphertext
as additional data.
pkS = Open(enc2, skR, info, ciphertext, ciphertext2)
contextR = SetupAuthR(enc, skR, info, pkS)
channelId = LabeledExtract(0, "channel_id", pkR)[0:16]
greeting = contextR.Open(channelId, ciphertext)
The Recipient uses the single-shot API to open ciphertext2
and obtain the Sender's public key pkS
. They can then setup their own encryption context and open the greeting ciphertext.
The initial setup allows the Sender to seal
messages and the Recipient to open
them but additional setup is needed to perform the operations in reverse.
key = contextR.Export("5edm key", 32)
nonce = contextR.Export("5edm nonce", 32)
sessionIdR = contextR.Export("5edm session id", 16)
sessionIdS = contextR.Export("5edm session id", 16)
contextR.SetupBidirectional(key, nonce)
ciphertext = contextR.Seal(sessionIdS, plaintext)
key = contextS.Export("5edm key", 32)
nonce = contextS.Export("5edm nonce", 32)
sessionIdR = contextS.Export("5edm recipient session id", 16)
sessionIdS = contextS.Export("5edm sender session id", 16)
contextS.SetupBidirectional(key, nonce)
ciphertext = contextS.Open(sessionIdS, ciphertext)
The Sender context can now open
and the Recipient context can now seal
. Session IDs are passed along with the encrypted messages to route them, so they are supplied as additional data when opening/sealing.
The pseudocode below defines Context<ROLE>.SetupBidirectional
. It aligns with the hpke-js implementation.
def Context<ROLE>.SetupBidirectional(key, base_nonce):
self.key_r = key
self.base_nonce_r = base_nonce
def ContextR.Seal(aad, pt):
if self.base_nonce_r == Nil:
raise SealError
ct = Seal(self.key_r, self.ComputeNonce_r(self.seq_r), aad, pt)
self.IncrementSeq_r()
return ct
def ContextS.Open(aad, ct):
if self.base_nonce_r == Nil:
raise OpenError
pt = Open(self.key_r, self.ComputeNonce_r(self.seq_r), aad, ct)
if pt == OpenError:
raise OpenError
self.IncrementSeq_r()
return pt
def Context<ROLE>.ComputeNonce_r(seq):
seq_bytes = I2OSP(seq, Nn)
return xor(self.base_nonce_r, seq_bytes)
def Context<ROLE>.IncrementSeq_r():
if self.seq_r >= (1 << (8*Nn)) - 1:
raise MessageLimitReachedError
self.seq_r += 1
- HPKE isn't resiliant against dropped or out-of-order messages so sessions can easily become out of sync on a shaky connection. A backend queue would help (or an alternative protocol) but I opted to keep things simple and rely on bare bones Deno Deploy. Instead, clients attempt to recover when they suspect they're out of sync. However, if both directions are out of sync it's game over for that session.
- I'm not a cryptographer but I did stay at a holiday inn express last night