### anyway isn't an interesting attack, but we must certainly require
### something stronger for state-change requests. Here, we also check a
### special request parameter `%nonce': forms setting up a `POST' action must
-### include an appropriate hidden input element. The `%nonce' parameter has
-### the form `LEFT.RIGHT', where LEFT and RIGHT are two base-64 strings such
-### that their XOR is the (deterministic) MAC tag on `chpwd-nonce.DATE.USER'.
-### (The LEFT string is chosen at random, and the RIGHT string is set to the
-### appropriate TAG XOR LEFT.)
+### include an appropriate hidden input element.
+###
+### The `%nonce' parameter encodes a randomized `all-or-nothing transform' of
+### the (deterministic) MAC tag on `chpwd-nonce.DATE.USER'. The standard
+### advice for defeating the BREACH attack (which uses differential
+### compression of HTTP payloads which include attacker-provided data to
+### recover CSRF tokens) is to transmit an XOR-split of the token; but that
+### allows an adversary to recover the token two bytes at a time; this makes
+### the attack take 256 times longer, which doesn't really seem enough. A
+### proper AONT, on the other hand, means that the adversary gets nothing if
+### he can't guess the entire transformed token -- and if he could do that,
+### he might as well just carry out the CSRF attack without messing with
+### BREACH in the first place.
###
### Messing about with cookies is a bit annoying, but it's hard to come up
### with alternatives. I'm trying to keep the URLs fairly pretty, and anyway
"""Return the bitwise XOR of two octet strings."""
return ''.join(chr(ord(xc) ^ ord(yc)) for xc, yc in I.izip(x, y))
-def mint_csrf_nonce(sec, ntag):
- left = OS.urandom(len(ntag))
- right = xor_strings(left, ntag)
- return '%s.%s' % (hack_octets(left), hack_octets(right))
+def aont_step(x, y):
+ """Perform a step of the OAEP-based all-or-nothing transform."""
+ return xor_strings(y, CFG.AUTHHASH(x).digest())
+
+def aont_transform(m):
+ """
+ Apply an all-or-nothing transform to a (short, binary) message M.
+
+ The result is returned as a binary string.
+ """
+
+ ## The current all-or-nothing transform is basically OAEP: a two-round
+ ## Feistel network applied to a (possibly lopsided) block consisting of the
+ ## message and a random nonce. Showing that this is an AONT (in the
+ ## random-oracle model) is pretty easy.
+ hashsz = CFG.AUTHHASH().digest_size
+ assert len(m) <= hashsz
+ r = OS.urandom(hashsz)
+ m = aont_step(r, m)
+ r = aont_step(m, r)
+ return r + m
+
+def aont_recover(c):
+ """
+ Recover a message from an all-or-nothing transform C (as a binary string).
+ """
+ hashsz = CFG.AUTHHASH().digest_size
+ if not (hashsz <= len(c) <= 2*hashsz):
+ raise AuthenticationFailed, 'BADNONCE'
+ r, m = c[:hashsz], c[hashsz:]
+ r = aont_step(m, r)
+ m = aont_step(r, m)
+ return m
def mint_token(user):
"""Make and return a fresh token for USER."""
## Check that the nonce matches, if one was supplied.
if nonce is not None:
- bits = nonce.split('.', 2)
- if len(bits) != 2: raise AuthenticationFailed, 'BADNONCE'
- try: left, right = map(unhack_octets, bits)
- except TypeError: raise AuthenticationFailed, 'BADNONCE'
- if len(left) != len(right) or len(left) != len(ntag):
- raise AuthenticationFailed, 'BADNONCE'
- gtag = xor_strings(left, right)
+ gtag = aont_recover(unhack_octets(nonce))
if gtag != ntag: raise AuthenticationFailed, 'BADNONCE'
## Make a new nonce string for use in forms.
- NONCE = mint_csrf_nonce(sec, ntag)
+ NONCE = hack_octets(aont_transform(ntag))
## Make sure the user still exists.
try: acct = S.SERVICES['master'].find(user)