[GH-ISSUE #36] Cryptographic suggestions #12

Closed
opened 2026-03-03 01:21:08 +03:00 by kerem · 5 comments
Owner

Originally created by @colmmacc on GitHub (May 31, 2021).
Original GitHub issue: https://github.com/cs01/termpair/issues/36

Termpair looks awesome and personally I am excited to start using it! I came to it via a thread on HN ... https://news.ycombinator.com/item?id=27338479 . That thread also includes some dissection of the cryptography in TermPair, some of the thoughts there are good, and some are off base. This issue is a summary of my own observations/suggestions in case they are interesting and useful:

  1. Termpair could use a key agreement protocol. Termpair is designed to avoid revealing keys to the server by encoding the key in the anchor fragment of the URL, which a server never sees. This is clever, but a server could still MITM by serving its own malicious HTML. It's actually very possible for two endpoints to share a key in a MITM proof way ... use a key agreement protocol such as ECDH. It's somewhat magical, but they can exchange material and agree a key without anyone in the middle ever being able to figure out what it is. There's some details to get right, like it's useful to have the ECDH be signed and verified by the parties involved, but it's all pretty straightforward.

  2. Keystroke timing is a practical attack. One of the perils of interactive terminal type applications is that human keystrokes can be timed and this timing can reveal what keys are being pressed. It's important to make sure that when a terminal is line-buffered, that the keys aren't sent one by one, but that the whole buffered line is encrypted and sent in one pass. Password inputs (like ssh and sudo) are expected to use line-buffering to mitigate keystroke timing issues. Additionally, it's a good idea to pad a line-buffered input up to a fixed size - this avoid leaking the length of the password entered.

  3. AES-GCM can safely use counters as nonces. This came from the HN thread, but they have a point; it's ok, and actually better to use the message count as the nonce for AES-GCM and it avoids the small odds of collisions occurring.

  4. It's good to use different keys in different directions. It's also a good idea to use a different encryption key in the direction of A to B than from B to A. These keys can be derived from a shared secret using HKDF.

Some of these may be bewildering, but I'm happy to answer questions. Again, Termpair looks awesome and I don't mean these suggestions as some kind of ding!

Originally created by @colmmacc on GitHub (May 31, 2021). Original GitHub issue: https://github.com/cs01/termpair/issues/36 Termpair looks awesome and personally I am excited to start using it! I came to it via a thread on HN ... https://news.ycombinator.com/item?id=27338479 . That thread also includes some dissection of the cryptography in TermPair, some of the thoughts there are good, and some are off base. This issue is a summary of my own observations/suggestions in case they are interesting and useful: 1. **Termpair could use a key agreement protocol.** Termpair is designed to avoid revealing keys to the server by encoding the key in the anchor fragment of the URL, which a server never sees. This is clever, but a server could still MITM by serving its own malicious HTML. It's actually very possible for two endpoints to share a key in a MITM proof way ... use a key agreement protocol such as ECDH. It's somewhat magical, but they can exchange material and agree a key without anyone in the middle ever being able to figure out what it is. There's some details to get right, like it's useful to have the ECDH be signed and verified by the parties involved, but it's all pretty straightforward. 2. **Keystroke timing is a practical attack.** One of the perils of interactive terminal type applications is that human keystrokes can be timed and this timing can reveal what keys are being pressed. It's important to make sure that when a terminal is line-buffered, that the keys aren't sent one by one, but that the whole buffered line is encrypted and sent in one pass. Password inputs (like ssh and sudo) are expected to use line-buffering to mitigate keystroke timing issues. Additionally, it's a good idea to pad a line-buffered input up to a fixed size - this avoid leaking the length of the password entered. 3. **AES-GCM can safely use counters as nonces.** This came from the HN thread, but they have a point; it's ok, and actually better to use the message count as the nonce for AES-GCM and it avoids the small odds of collisions occurring. 4. **It's good to use different keys in different directions.** It's also a good idea to use a different encryption key in the direction of A to B than from B to A. These keys can be derived from a shared secret using HKDF. Some of these may be bewildering, but I'm happy to answer questions. Again, Termpair looks awesome and I don't mean these suggestions as some kind of ding!
kerem closed this issue 2026-03-03 01:21:08 +03:00
Author
Owner

@ignoramous commented on GitHub (May 31, 2021):

re: key exchange: I've used CPACE in the past for device-to-device key exchange https://github.com/jedisc1/cpace which is much simpler than SPAKE2+ that croc uses: https://redrocket.club/posts/croc/

(not an expert on cryptography but @colmmacc can certainly help us out here).

<!-- gh-comment-id:851538854 --> @ignoramous commented on GitHub (May 31, 2021): re: key exchange: I've used CPACE in the past for device-to-device key exchange https://github.com/jedisc1/cpace which is much simpler than SPAKE2+ that croc uses: https://redrocket.club/posts/croc/ (not an expert on cryptography but @colmmacc can certainly help us out here).
Author
Owner

@cs01 commented on GitHub (Jun 1, 2021):

Thanks for the suggestions. I'm glad to have someone with expertise chiming in here.

but a server could still MITM by serving its own malicious HTML

I guess technically it would be malicious JavaScript, but my question here would be whether ECDH (or something else like CPACE as @ignoramous mentioned) would somehow ensure the JavaScript payload hasn't been tampered with. If all it did was ensure the key made it from terminal to browser without being seen by the server, the compromised JavaScript could still decode the data and then leak it back to the server unencrypted, all while keeping the key secret.

Another question: would this work in the one to many setup TermPair uses? One terminal can be connected to many browsers.

Keystroke timing is a practical attack.
One of the perils of interactive terminal type applications is that human keystrokes can be timed and this timing can reveal what keys are being pressed.

Is this assuming someone making the keystrokes doesn't have the key? (this is never the case) Or is this from the perspective of a malicious server viewing the content being transmitted?

Regarding line buffering, the goal of TermPair is to mirror exactly what is being done in the terminal, and to keep all connected terminals in sync. TermPair is naive to what is running in the pty itself. It sets the pty to "raw" mode, which turns off line buffering, then it just listens for new data on the pty's output file descriptor and writes input to the pty's input file descriptor.

Additionally, it's a good idea to pad a line-buffered input up to a fixed size - this avoid leaking the length of the password entered.

How would this be done? Some kind of special padding character that is stripped out in the decoding process?

AES-GCM can safely use counters as nonces.

This would be a pretty easy change to make. IIRC it was a coin flip for me whether to use a random number or an incrementing number, but it sounds like there is a slight benefit to the latter.

It's also a good idea to use a different encryption key in the direction of A to B than from B to A. These keys can be derived from a shared secret using HKDF.

I like this idea. I would ask the same question here whether it's possible to have this work in a one to many scenario.

<!-- gh-comment-id:851844358 --> @cs01 commented on GitHub (Jun 1, 2021): Thanks for the suggestions. I'm glad to have someone with expertise chiming in here. > but a server could still MITM by serving its own malicious HTML I guess technically it would be malicious JavaScript, but my question here would be whether ECDH (or something else like CPACE as @ignoramous mentioned) would somehow ensure the JavaScript payload hasn't been tampered with. If all it did was ensure the key made it from terminal to browser without being seen by the server, the compromised JavaScript could still decode the data and then leak it back to the server unencrypted, all while keeping the key secret. Another question: would this work in the one to many setup TermPair uses? One terminal can be connected to many browsers. > Keystroke timing is a practical attack. > One of the perils of interactive terminal type applications is that human keystrokes can be timed and this timing can reveal what keys are being pressed. Is this assuming someone making the keystrokes doesn't have the key? (this is never the case) Or is this from the perspective of a malicious server viewing the content being transmitted? Regarding line buffering, the goal of TermPair is to mirror exactly what is being done in the terminal, and to keep all connected terminals in sync. TermPair is naive to what is running in the pty itself. It sets the pty to "raw" mode, which turns off line buffering, then it just listens for new data on the pty's output file descriptor and writes input to the pty's input file descriptor. > Additionally, it's a good idea to pad a line-buffered input up to a fixed size - this avoid leaking the length of the password entered. How would this be done? Some kind of special padding character that is stripped out in the decoding process? > AES-GCM can safely use counters as nonces. This would be a pretty easy change to make. IIRC it was a coin flip for me whether to use a random number or an incrementing number, but it sounds like there is a slight benefit to the latter. > It's also a good idea to use a different encryption key in the direction of A to B than from B to A. These keys can be derived from a shared secret using HKDF. I like this idea. I would ask the same question here whether it's possible to have this work in a one to many scenario.
Author
Owner

@ignoramous commented on GitHub (Jun 1, 2021):

I guess technically it would be malicious JavaScript, but my question here would be whether ECDH (or something else like CPACE as @ignoramous mentioned) would somehow ensure the JavaScript payload hasn't been tampered with.

No, it wouldn't since browsers don't support CPACE (or any PAKE) out-of-the-box to ensure any sort of guarantee here.

You could consider using CSP attributes to shore up the MiTM part, but the user needs to know to trust them (like how WhatsApp and Signal require offline confirmation of security codes, and PGP requires a "web of trust", which are an overkill for termpair no doubt). An offline HTML page that a user could download (or self-host over github pages or vercel or netlify) sounds like a better solution, since they then would control what is deployed and served to them?

I like this idea. I would ask the same question here whether it's possible to have this work in a one to many scenario.

Signal's double-ratchet and Keybase's ephemeral-user-keys are a good starting point to see how it could be done with strict forward-secrecy. If you're using something like CPACE, a new shared-key could (and should) be derived locally at each client using any KDF keyed to the CPACE exchanged ISK (Intermediate Session Key).

Over an encrypted channel from this newly derived shared-key (sk1), you could exchange random 32-byte string generated at each client (ra, rb), from which other clients could derive client-specific shared-key from (ska1, skb1); or you could even exchange ephemeral public-keys generated (preferably using WebCrypto) at the clients (like in the Signal example) and build on top of it.

(caution: this is me, a non-cryptographer, casually rolling my own crypto)
                                     A                                        B
                      sk1 = kdf(isk, "key-1")                   sk1 = kdf(isk, "key-1")
                      ra = random-32-bytes                        rb = random-32-bytes
                       ska1 = kdf(ra, sk1)                        skb1 = kdf(rb, sk1)
                      ea = enc(key=sk1, msg=ra)                  eb = enc(key=sk1, msg=rb)
                            send(ea)                                  send(eb)
                         rb = dec(sk1, eb)                          ra = dec(sk1, ea)
                      skb1 = kdf(rb, sk1)                         ska1 = kdf(ra, sk1)
<!-- gh-comment-id:852368695 --> @ignoramous commented on GitHub (Jun 1, 2021): > I guess technically it would be malicious JavaScript, but my question here would be whether ECDH (or something else like CPACE as @ignoramous mentioned) would somehow ensure the JavaScript payload hasn't been tampered with. No, it wouldn't since browsers don't support CPACE (or any PAKE) out-of-the-box to ensure any sort of guarantee here. You _could_ consider using [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) attributes to shore up the MiTM part, but the user needs to know to trust them (like how WhatsApp and Signal require offline confirmation of security codes, and PGP requires a "web of trust", which are an overkill for `termpair` no doubt). An offline HTML page that a user could download (or self-host over github pages or vercel or netlify) sounds like a better solution, since they then would control what is deployed _and_ served to them? > I like this idea. I would ask the same question here whether it's possible to have this work in a one to many scenario. Signal's [double-ratchet](https://signal.org/docs/specifications/doubleratchet/) and Keybase's [ephemeral-user-keys](https://book.keybase.io/docs/chat/ephemeral) are a good starting point to see how it could be done with strict forward-secrecy. If you're using something like CPACE, a new shared-key _could_ (and should) be derived locally at each client using any KDF keyed to the CPACE exchanged ISK (Intermediate Session Key). Over an encrypted channel from this newly derived shared-key (`sk1`), you could exchange random 32-byte string generated at each client (`ra`, `rb`), from which other clients could derive client-specific shared-key from (`ska1`, `skb1`); or you could even exchange ephemeral public-keys generated (preferably using WebCrypto) at the clients (like in the Signal example) and build on top of it. ``` (caution: this is me, a non-cryptographer, casually rolling my own crypto) A B sk1 = kdf(isk, "key-1") sk1 = kdf(isk, "key-1") ra = random-32-bytes rb = random-32-bytes ska1 = kdf(ra, sk1) skb1 = kdf(rb, sk1) ea = enc(key=sk1, msg=ra) eb = enc(key=sk1, msg=rb) send(ea) send(eb) rb = dec(sk1, eb) ra = dec(sk1, ea) skb1 = kdf(rb, sk1) ska1 = kdf(ra, sk1) ```
Author
Owner

@cs01 commented on GitHub (Jun 3, 2021):

An offline HTML page that a user could download (or self-host over github pages or vercel or netlify) sounds like a better solution, since they then would control what is deployed and served to them?

This is an awesome idea. I started working on this and just got a proof of concept working locally.

Regarding using counters as nonces: I started working on this as well, but quickly realized the terminal counter and browser counters would have to continue counting together, they could not start at their own 0 because then there would be a repeated nonce with the same key. This makes the implementation much harder, and I'm not sure it's worth it anymore.

Re other suggestions, I am leaning toward leaving things as they are (unless someone wants to submit a pull request) because a) this is just a side project and b) this looks like considerable work, and I am not sure I understand what is wrong with the current scenario besides the theoretical malicious JavaScript which I am addressing with your suggestion.

<!-- gh-comment-id:853524646 --> @cs01 commented on GitHub (Jun 3, 2021): > An offline HTML page that a user could download (or self-host over github pages or vercel or netlify) sounds like a better solution, since they then would control what is deployed and served to them? This is an awesome idea. I started working on this and just got a proof of concept working locally. Regarding using counters as nonces: I started working on this as well, but quickly realized the terminal counter and browser counters would have to continue counting together, they could not start at their own 0 because then there would be a repeated nonce with the same key. This makes the implementation much harder, and I'm not sure it's worth it anymore. Re other suggestions, I am leaning toward leaving things as they are (unless someone wants to submit a pull request) because a) this is just a side project and b) this looks like considerable work, and I am not sure I understand what is wrong with the current scenario besides the theoretical malicious JavaScript which I am addressing with your suggestion.
Author
Owner

@ignoramous commented on GitHub (Jun 3, 2021):

Regarding using counters as nonces: I started working on this as well, but quickly realized the terminal counter and browser counters would have to continue counting together, they could not start at their own 0 because then there would be a repeated nonce with the same key. This makes the implementation much harder, and I'm not sure it's worth it anymore.

If the participants can't count (usually Alice nonces in even number increments per message whilst Bob nonces in odd), generate random nonces (12 bytes is good enough for AES) and send it along with the cipher text in the clear (this is okay). Alternatively, use libsodium instead, which forces some sane defaults: https://doc.libsodium.org/secret-key_cryptography/encrypted-messages

Re other suggestions, I am leaning toward leaving things as they are

That's fair. As to what's wrong with the current setup... Well, it isn't as water tight as it could be, given terminal information is all sorts of critical in any setting for a business of any size. Folks could be typing passwords, generating or viewing private keys, viewing sensitive logs, and so on.

<!-- gh-comment-id:853676176 --> @ignoramous commented on GitHub (Jun 3, 2021): > Regarding using counters as nonces: I started working on this as well, but quickly realized the terminal counter and browser counters would have to continue counting together, they could not start at their own 0 because then there would be a repeated nonce with the same key. This makes the implementation much harder, and I'm not sure it's worth it anymore. If the participants can't count (usually Alice nonces in even number increments per message whilst Bob nonces in odd), generate random nonces (12 bytes is good enough for AES) and send it along with the cipher text in the clear (this is okay). Alternatively, use libsodium instead, which forces some sane defaults: https://doc.libsodium.org/secret-key_cryptography/encrypted-messages > Re other suggestions, I am leaning toward leaving things as they are That's fair. As to what's wrong with the current setup... Well, it isn't as water tight as it could be, given terminal information is all sorts of critical in any setting for a business of any size. Folks could be typing passwords, generating or viewing private keys, viewing sensitive logs, and so on.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/termpair#12
No description provided.