Skip to content

Commit

Permalink
do dah dooo
Browse files Browse the repository at this point in the history
  • Loading branch information
ahdinosaur committed Nov 27, 2023
1 parent 92bd291 commit f6473c4
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 18 deletions.
17 changes: 14 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@

- The channel must be reliable and ordered: i.e. TCP.
- Each channel key must be an ephemeral key for a single channel and discarded when the channel ends.
- To get an ephemeral key for a session, do a secure key exchange, such as [Noise](https://noiseprotocol.org/noise.html) or [Secret Handshake](https://dominictarr.github.io/secret-handshake-paper/shs.pdf) first.
- For a duplex (bi-directional) connection between peers, create two secret channels (with separate keys), one in each direction.
- A (key, nonce) pair must NEVER be re-used.

## Security Guarantees

`secret-channel` protects the stream from:

- Stream truncation: avoided by checking for "end-of-stream" as the final chunk.
- Chunk removal: the wrong nonce would be used, producing an AEAD decryption error.
- Chunk reordering: the wrong nonce would be used, producing an AEAD decryption error.
- Chunk duplication: the wrong nonce would be used, producing an AEAD decryption error.
- Chunk modification: this is what an AEAD is designed to detect.

## Stream

Data is sent over the channel in chunks.
Expand Down Expand Up @@ -164,9 +176,7 @@ Libsodium's secretstream has more features not included in Secret Channel:
- secretstream gives no guidance on how to handle variable length messages.
- Libsodium provides functions `sodium_pad` and `sodium_unpad` to pad messages to fixed lengths.

Overlap:

- secretstream and Secret Channel both use ChaCha20-Poly1305 for encryption.
Both secretstream and Secret Channel use ChaCha20-Poly1305 for encryption.

secretstream has affordances that Secret Channel doesn't need:

Expand All @@ -185,6 +195,7 @@ STREAM is designed to avoid nonce-reuse in practical settings where keys may be
- STREAM encodes the last message with a tag in the AD.
- STREAM creates each nonce from a random 64-bit prefix and a 32-bit counter.
- The likelihood of a collision, even when re-using keys, is considered safe enough.
- Secret Channel avoids this problem by explicitly disallowing any key re-use.
- STREAM gives no guidance on how to handle variable length messages.

## References
Expand Down
44 changes: 29 additions & 15 deletions js/secret-channel/src/protocol.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const {
LENGTH_OR_END_CIPHERTEXT,
} = require('./constants')

class StreamEncrypter {
class Encrypter {
#crypto
#key
#nonce
Expand All @@ -16,34 +16,40 @@ class StreamEncrypter {
this.#crypto = crypto

if (!b4a.isBuffer(key)) {
throw new Error('secret-channel/StreamEncrypter: key must be a buffer')
throw new Error('secret-channel/Encrypter: key must be a buffer')
}
if (key.length !== KEY_SIZE) {
throw new Error(`secret-channel/StreamEncrypter: key must be ${KEY_SIZE} bytes`)
throw new Error(`secret-channel/Encrypter: key must be ${KEY_SIZE} bytes`)
}
this.#key = key

if (!b4a.isBuffer(nonce)) {
throw new Error('secret-channel/StreamEncrypter: nonce must be a buffer')
throw new Error('secret-channel/Encrypter: nonce must be a buffer')
}
if (nonce.length !== NONCE_SIZE) {
throw new Error(`secret-channel/StreamEncrypter: nonce must be ${NONCE_SIZE} bytes`)
throw new Error(`secret-channel/Encrypter: nonce must be ${NONCE_SIZE} bytes`)
}
// clone the nonce so is owned and mutable
this.#nonce = b4a.allocUnsafe(NONCE_SIZE)
b4a.copy(nonce, this.#nonce)
}

next(plaintext) {
if (this.#key === null) {
throw new Error('secret-channel/Encrypter: stream has already ended')
}
const plaintextBuffer = b4a.from(plaintext)
const length = this.#chunkLength(plaintextBuffer.length)
const content = this.#chunkContent(plaintextBuffer)
return [length, content]
}

end() {
if (this.#key === null) {
throw new Error('secret-channel/Encrypter: stream has already ended')
}
const eos = this.#chunkEndOfStream()
// TODO delete the key
this.#key = null
return eos
}

Expand All @@ -70,7 +76,7 @@ class StreamEncrypter {
}
}

class StreamDecrypter {
class Decrypter {
#crypto
#key
#nonce
Expand All @@ -79,35 +85,40 @@ class StreamDecrypter {
this.#crypto = crypto

if (!b4a.isBuffer(key)) {
throw new Error('secret-channel/StreamDecrypter: key must be a buffer')
throw new Error('secret-channel/Decrypter: key must be a buffer')
}
if (key.length !== KEY_SIZE) {
throw new Error(`secret-channel/StreamDecrypter: key must be ${KEY_SIZE} bytes`)
throw new Error(`secret-channel/Decrypter: key must be ${KEY_SIZE} bytes`)
}
this.#key = key

if (!b4a.isBuffer(nonce)) {
throw new Error('secret-channel/StreamDecrypter: nonce must be a buffer')
throw new Error('secret-channel/Decrypter: nonce must be a buffer')
}
if (nonce.length !== NONCE_SIZE) {
throw new Error(`secret-channel/StreamEncrypter: nonce must be ${NONCE_SIZE} bytes`)
throw new Error(`secret-channel/Encrypter: nonce must be ${NONCE_SIZE} bytes`)
}
// clone the nonce so is owned and mutable
this.#nonce = b4a.allocUnsafe(NONCE_SIZE)
b4a.copy(nonce, this.#nonce)
}

lengthOrEnd(ciphertext) {
if (this.#key === null) {
throw new Error('secret-channel/Decrypter: stream has already ended')
}

if (ciphertext.length !== LENGTH_OR_END_CIPHERTEXT) {
throw new Error(
`secret-channel/StreamDecrypter: length / end ciphertext must be ${LENGTH_OR_END_CIPHERTEXT} bytes`,
`secret-channel/Decrypter: length / end ciphertext must be ${LENGTH_OR_END_CIPHERTEXT} bytes`,
)
}

const plaintext = this.#decrypt(ciphertext)

if (this.#crypto.isZero(plaintext)) {
// TODO delete the key
// delete the key
this.#key = null
return {
type: 'end-of-stream',
}
Expand All @@ -123,6 +134,9 @@ class StreamDecrypter {
}

content(ciphertext) {
if (this.#key === null) {
throw new Error('secret-channel/Decrypter: stream has already ended')
}
return this.#decrypt(ciphertext)
}

Expand All @@ -136,10 +150,10 @@ class StreamDecrypter {
function protocol(crypto) {
return {
createEncrypter(key, nonce) {
return new StreamEncrypter(crypto, key, nonce)
return new Encrypter(crypto, key, nonce)
},
createDecrypter(key, nonce) {
return new StreamDecrypter(crypto, key, nonce)
return new Decrypter(crypto, key, nonce)
},
}
}
Expand Down

0 comments on commit f6473c4

Please sign in to comment.