-
Notifications
You must be signed in to change notification settings - Fork 654
Description
Background
As discussed in #36 and #40, currently Shadowsocks has a fundamental design flaw of potential key-nonce pair reuse. Specifically, we are using a long-term M-bit pre-shared key and N-bit random nonces, resulting in a (M+N)-bit key-nonce pair containing only N-bit randomness, where N is usually insufficient (N < 128).
#36 proposed to deprecate ciphers where N <= 96, which unfortunately eliminated many good ciphers like Chacha20 and Chacha20-Poly1305 where N=64. However the problem still exists, albeit at slightly lower probability. Even when N=96, there are practical concerns about the safety margin. Needless to say, we are operating against best practices and security recommendations.
#40 identified the design flaw and paved the way to this SIP.
Proposal
I propose to introduce a per-session subkey derived from the pre-shared master key using HKDF, and use the subkey to encrypt/decrypt in both stream ciphers and AEAD ciphers. Essentially it means we are moving from (M+N)-bit (PSK, nonce) pair to (M+N)-bit (HKDF(PSK, salt), nonce) pair. Because HKDF is a PRF, the new construction significantly expands the amount of randomness (from N to at least M where M is much greater than N), thus correcting the previously mentioned design flaw.
Additionally, because the pre-shared key is usually generated from a human-chosen text password of insufficient entropy, the result is not very strong. HKDF gives us the benefit of producing cryptographically strong derived keys even if the input master key is weak.
Details
Assuming we already have a user-supplied pre-shared master key PSK.
Function HKDF_SHA1 is a HKDF constructed using SHA1 hash. Its signature is
HKDF_SHA1(secret_key, salt, info)
The "info" string argument allows us to bind the derived subkey to a specific application context.
For stream ciphers, the revised encryption scheme is:
- Pick a random R-bit salt (R = max(128, len(SK)))
- Derive subkey SK = HKDF_SHA1(PSK, salt, "ss-subkey")
- Derive initialization vector IV = HKDF_SHA1(PSK, salt, "ss-iv")
- Send salt
- Send StreamEncrypt(SK, IV, payload)
Note that even with the above changes, the old approach using stream ciphers is still not secure. We encourage users to switch to AEAD ciphers as soon as possible.
For compatibility reasons, we will probably NOT adding per-session key to stream ciphers.
For AEAD ciphers, the revised encryption scheme is:
- Pick a random R-bit salt (R = max(128, len(SK)))
- Derive subkey SK = HKDF_SHA1(PSK, salt, "ss-subkey")
- Send salt
- For each chunk, encrypt and authenticate payload using SK with a counting nonce (starting from 0 and increment by 1 after each use)
- Send encrypted chunk