Skip to content

zaach/5edm

Repository files navigation

5EDM

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.

Running Locally

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.

Deploying

The app is deployed to Deno Deploy via github actions.

Protocol

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.

Bidirectional Encryption

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

Caveats

  • 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

Releases

No releases published

Packages

No packages published

Languages