Why is RFC 2865's User-Password hiding a keystream rather than encryption, and what does a repeated Request Authenticator hand an eavesdropper?
answer
- exclusive-OR, not a cipher
- the nonce is the whole uniqueness budget
- same stream twice cancels to a difference
- one known password unwraps the rest
- padded to sixteen-octet blocks, 128 ceiling
basics
~20 sThe password is exclusive-ORed with MD5 output derived from the shared secret and the Request Authenticator, so the construction is a keystream. Reuse the same Request Authenticator with the same secret and the keystream repeats, letting an observer exclusive-OR two captures together and recover their difference.
solid answer
~50 sRFC 2865 hides `User-Password (2)` by padding it to a multiple of 16 octets and exclusive-ORing it with MD5 output: `b1 = MD5(S + RA)`, `c(1) = p1 xor b1`, then `bi = MD5(S + c(i-1))` and `c(i) = pi xor bi`. Nothing is encrypted — a pseudorandom stream is generated and XORed, which makes the result **malleable** (flipping a ciphertext bit flips the plaintext bit) and leaves the padded length visible. The whole construction's uniqueness rests on `RA`, the 16-octet `Request Authenticator`. If an access device reuses one — a weak generator, a reboot that resets it, a counter — then two captures produce `c xor c' = p xor p'`. If the attacker knows one of those passwords, because it is their own account, they recover the keystream and read the other outright. The attribute also caps at 128 characters.
code
pseudocode · 15 linesfunction hide_user_password(password, secret, request_authenticator):
p = pad_right_with_zeros(password, multiple_of = 16) // ceiling 128 characters
previous = request_authenticator
c = empty
for each 16-octet block pi in p:
b = MD5(secret + previous) // stream block, not a cipher
ci = pi xor b
c = c + ci
previous = ci // chain on the CIPHERTEXT block
return c
// reuse request_authenticator with the same secret and every b repeats,
// so c xor c_other = p xor p_othergo deeper
Take away one fact: the password is not encrypted, it is masked with a pattern derived from the shared secret and a per-request nonce.
Be able to write the first two steps of the construction and say which value makes each request's masking different from the last.
Reason about the failure: given two captures with the same nonce, state precisely what an attacker recovers and what they still need to guess.
Judge the exposure across an estate: unpredictable nonce generation is per-device firmware behaviour you cannot audit remotely, which is an argument for changing transport rather than tightening configuration.
## The construction, one block at a time RFC 2865 §5.2 does not encrypt the password. It builds a pseudorandom stream and exclusive-ORs the password with it: - pad the password on the right with zero octets to a multiple of **16 octets**; - `b1 = MD5(S + RA)`, where `S` is the shared secret and `RA` is the request's 16-octet `Request Authenticator`; - `c(1) = p1 xor b1` — the first 16 plaintext octets XORed with the first block of stream; - for each further block, `bi = MD5(S + c(i-1))` and `c(i) = pi xor bi`, chaining on the previous **ciphertext** block; - the concatenated `c(i)` blocks become the `User-Password (2)` value, and the attribute tops out at **128 characters**. The server, holding the same `S` and reading `RA` out of the packet header, regenerates the identical stream and XORs it back off. ## Why "keystream" is the right word, and what follows from it Calling it encryption invites three wrong expectations. Calling it a keystream predicts its actual behaviour: - **It is malleable.** XOR is its own inverse, so an on-path attacker who flips a bit in the ciphertext flips the same bit in the password the server recovers. Nothing in the construction detects that; only `Message-Authenticator (80)` does. - **It leaks length.** The value's size is the password's length rounded up to the next multiple of 16 octets, so a capture separates short credentials from long ones. - **It is only as unique as its nonce.** Every block of stream descends from `MD5(S + RA)`. Same `S`, same `RA`, same stream — exactly the failure mode of any stream cipher whose nonce repeats. ## What a repeated Request Authenticator hands over The specification requires the `Request Authenticator` to be **unpredictable and non-repeating**. Where an implementation falls short — a poor generator, a value reset on reboot, an incrementing counter — the consequences arrive in order of severity: 1. **Two captures under one `RA` and one secret.** `c xor c' = p xor p'`. The attacker does not have either password, but they have their difference, which collapses the search space for both and immediately reveals whether two accounts share a password. 2. **One known plaintext.** If the attacker can cause one of those requests themselves — authenticating with an account they control — then `b1 = c xor p` recovers the first block of keystream, and every other password sent under the same `RA` decrypts directly. No guessing at all. 3. **A predictable `RA`.** If future values can be foreseen, the attacker can precompute stream blocks for candidate secrets before the traffic exists, and the replay protection of the `Response Authenticator` weakens alongside, since it too is computed over that nonce. ## The offline problem underneath all of it Even with a perfectly unpredictable nonce, a single capture gives an attacker `RA` and `c(1)` and a guess at the plaintext shape. Candidate secrets can be tested locally: compute `MD5(S_guess + RA)`, XOR it against `c(1)`, and look for something that looks like a password. There is no round trip, no rate limit and nothing for the server to log. The only parameter the operator still controls is the secret's length and randomness, and `Message-Authenticator (80)` does not change this — RFC 3579 §3.2 is explicit that it affords no protection against an offline attack. | Property | What the construction gives | What it does not give | |---|---|---| | Confidentiality of the password | Yes, against an observer without the secret | No, once the secret is guessed offline | | Integrity of the password | None; bit flips pass through | Requires `Message-Authenticator (80)` | | Length hiding | Rounded to 16 octets | Exact length is inferable in blocks | | Freshness | Only as good as the `Request Authenticator` | Nothing if the nonce repeats | ## What actually fixes it Not a longer password and not a different attribute. The hiding scheme is load-bearing in a protocol that predates HMAC's general adoption, and it cannot be repaired without changing what is on the wire. The standards answer is to stop protecting the payload with the secret at all: RADIUS over TLS on TCP port 2083 (RFC 6614) and RADIUS/DTLS on UDP port 2083 (RFC 7360) let the transport carry confidentiality and integrity, and RADIUS/1.1 (RFC 9765, Experimental) removes the shared secret and every MD5-based construction, this one included. Until that migration lands, the honest interim is a long random per-device secret, `Message-Authenticator (80)` wherever the deployment can carry it, and verifying that each access device's `Request Authenticator` really is unpredictable.
- What does the length of a captured User-Password (2) value tell an observer?The password's length rounded up to the next multiple of 16 octets. A 32-octet value means a password of 17 to 32 characters. That is coarse, but it separates short credentials from long ones across a population of captures and is information the construction cannot withhold.
- Can an on-path attacker change the password the server recovers, without knowing the secret?Yes, blindly. Because the hiding is an exclusive-OR, flipping a ciphertext bit flips the corresponding plaintext bit at the server. They cannot choose the resulting password, so in practice this causes a rejection rather than an intrusion — but it shows the value has no integrity of its own. `Message-Authenticator (80)` is what detects it.
- Why does chaining on the previous ciphertext block not save a repeated nonce?Because the chain starts from `MD5(S + RA)`. If `S` and `RA` are the same, the first stream block is the same, so the first ciphertext block is a straight XOR of two plaintexts; and if the plaintexts happen to share a prefix, the identical ciphertext propagates the identical stream into the next block too.
Two letters written in the same batch of invisible ink: each alone stays unreadable, but laid one over the other the places where they differ show through — and if you wrote one of them yourself, the other is simply legible.
saying these in an interview costs you the question
- Calls it encryption and expects a cipher's integrity
- Thinks a long password compensates for a weak secret
- Assumes the padding hides the password's length entirely
- Believes a repeated Request Authenticator only risks a duplicate
- Says MD5 being one-way makes the hiding non-malleable
- Claims Message-Authenticator (80) blocks offline guessing of the secret