https://github.com/zaach/5edm
end-to-end encrypted messaging on deno
https://github.com/zaach/5edm
chat deno e2ee hpke privacy
Last synced: about 1 year ago
JSON representation
end-to-end encrypted messaging on deno
- Host: GitHub
- URL: https://github.com/zaach/5edm
- Owner: zaach
- Created: 2022-10-19T20:46:27.000Z (over 3 years ago)
- Default Branch: main
- Last Pushed: 2022-11-30T21:03:54.000Z (over 3 years ago)
- Last Synced: 2024-04-16T00:26:29.505Z (about 2 years ago)
- Topics: chat, deno, e2ee, hpke, privacy
- Language: TypeScript
- Homepage: https://5edm.deno.dev
- Size: 258 KB
- Stars: 6
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README

Ephemeral, Edge, End-to-End Encrypted Direct Messaging
5EDM uses the recent [Hybrid Public Key Encryption (HPKE)](https://www.rfc-editor.org/rfc/rfc9180.html) 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](https://deno.com/deploy/docs/runtime-broadcast-channel).
_Note: This is a proof-of-concept. Use [Signal](https://signal.org/) 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](https://fresh.deno.dev/). 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](https://deno.com/deploy/docs/deployctl#deployctl-github-action).
## Protocol
The pseudocode below borrows definitions from the [spec](https://www.rfc-editor.org/rfc/rfc9180.html) unless otherwise defined. The app uses the [hpke-js](https://github.com/dajiaji/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](https://www.rfc-editor.org/rfc/rfc9180.html#name-authentication-using-an-asy) 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](https://www.rfc-editor.org/rfc/rfc9180.html#name-metadata-protection), the [single-shot API](https://www.rfc-editor.org/rfc/rfc9180.html#name-encryption-and-decryption-2) 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](https://www.rfc-editor.org/rfc/rfc9180.html#bidirectional) 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.SetupBidirectional`. It aligns with the [hpke-js implementation](https://github.com/dajiaji/hpke-js#base-mode-with-bidirectional-encryption).
```
def Context.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.ComputeNonce_r(seq):
seq_bytes = I2OSP(seq, Nn)
return xor(self.base_nonce_r, seq_bytes)
def Context.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](https://www.rfc-editor.org/rfc/rfc9180.html#name-message-order-and-message-l) so sessions can easily become out of sync on a shaky connection. A backend queue would help (or an [alternative protocol)](https://datatracker.ietf.org/doc/draft-harkins-cfrg-dnhpke/) 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