From abd773b52e88afcbba91c9bf71ba00c921379c1f Mon Sep 17 00:00:00 2001 From: Brendan Taylor Date: Wed, 20 Jun 2007 20:44:33 +0000 Subject: [PATCH] can respond to an esession request --- src/common/stanza_session.py | 267 +++++++++++++++++++++++++++++------ src/gajim.py | 16 ++- 2 files changed, 233 insertions(+), 50 deletions(-) diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index 075fb9382..875c786e3 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -160,7 +160,7 @@ class EncryptedStanzaSession(StanzaSession): sh = SHA256.new() sh.update(string) return sh.digest() - + def generate_initiator_keys(self, k): return (self.hmac(k, 'Initiator Cipher Key'), self.hmac(k, 'Initiator MAC Key'), @@ -186,9 +186,13 @@ class EncryptedStanzaSession(StanzaSession): return self.encrypter.encrypt(encryptable) + # FIXME: get a real PRNG + def random_bytes(self, bytes): + return os.urandom(bytes) + def generate_nonce(self): # FIXME: this isn't a very good PRNG - return os.urandom(8) + return self.random_bytes(8) def decrypt_stanza(self, stanza): c = stanza.getTag(name='c', namespace='http://www.xmpp.org/extensions/xep-0200.html#ns') @@ -271,54 +275,92 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'requested-e2e' self.send(request) + + # 4.3 esession response (bob) + def respond_e2e_bob(self, request_form): + response = xmpp.Message() + feature = response.NT.feature + feature.setNamespace(xmpp.NS_FEATURE) - # generate a random number between 'bottom' and 'top' - def srand(self, bottom, top): - # minimum number of bytes needed to represent that range - bytes = int(math.ceil(math.log(top - bottom, 256))) + x = xmpp.DataForm(typ='submit') - # FIXME: use a real PRNG - # in retrospect, this is horribly inadequate. - return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + fixed = { 'disclosure': 'never', + 'security': 'e2e', + 'crypt_algs': 'aes128-ctr', + 'hash_algs': 'sha256', + 'compress': 'none', + 'stanzas': 'message', + 'init_pubkey': 'none', + 'resp_pubkey': 'none', + 'ver': '1.0', + 'sas_algs': 'sas28x5', + 'logging': 'may' } - def make_dhhash(self, modp): - p = dh.primes[modp] - g = dh.generators[modp] + not_acceptable = [] - x = self.srand(2 ** (2 * self.n - 1), p - 1) + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='accept', value='true')) - # XXX this may be a source of performance issues - e = self.powmod(g, x, p) + for name, field in map(lambda name: (name, request_form.getField(name)), request_form.asDict().keys()): + options = map(lambda x: x[1], field.getOptions()) + values = field.getValues() - self.xes[modp] = x - self.es[modp] = e + if not field.getType() in ('list-single', 'list-multi'): + options = values - He = self.sha256(self.encode_mpi(e)) + if name in fixed: + if fixed[name] in options: + x.addChild(node=xmpp.DataField(name=name, value=fixed[name])) + else: + not_acceptable.append(name) + elif name == 'modp': + # the offset of the group we chose (need it to match up with the dhhash) + group_order = 0 + self.modp = int(options[group_order]) + x.addChild(node=xmpp.DataField(name='modp', value=self.modp)) - return base64.b64encode(He) + g = dh.generators[self.modp] + p = dh.primes[self.modp] + elif name == 'rekey_freq': + preferred = int(options[0]) + x.addChild(node=xmpp.DataField(name='rekey_freq', value=preferred)) - # a faster version of (base ** exp) % mod - # taken from - def powmod(self, base, exp, mod): - square = base % mod - result = 1 + self.rekey_freq = preferred + elif name == 'my_nonce': + self.n_o = base64.b64decode(field.getValue()) - while exp > 0: - if exp & 1: # exponent is odd - result = (result * square) % mod + # XXX do something with not_acceptable - square = (square * square) % mod - exp /= 2 + self.He = request_form.getField('dhhashes').getValues()[group_order].encode("utf8") - return result + bytes = int(self.n / 8) - def terminate_e2e(self): - self.status = None + self.n_s = self.generate_nonce() + + self.c_o = self.decode_mpi(self.random_bytes(bytes)) # n-bit random number + self.c_s = self.c_o ^ (2 ** (self.n - 1)) + + self.y = self.srand(2 ** (2 * self.n - 1), p - 1) + self.d = self.powmod(g, self.y, p) + + to_add = { 'my_nonce': self.n_s, 'dhkeys': self.encode_mpi(self.d), 'counter': self.encode_mpi(self.c_o), 'nonce': self.n_o } + + for name in to_add: + b64ed = base64.b64encode(to_add[name]) + x.addChild(node=xmpp.DataField(name=name, value=b64ed)) + + self.form_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), request_form.getChildren())) + self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + + self.status = 'responded-e2e' + + feature.addChild(node=x) + self.send(response) # 'Alice Accepts' def accept_e2e_alice(self, form): -# 1. Verify that the ESession options selected by Bob are acceptable -# 2. Return a error to Bob unless: 1 < d < p - 1 +# 1. Verify that the ESession options selected by Bob are acceptable +# 2. Return a error to Bob unless: 1 < d < p - 1 self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren())) accept = xmpp.Message() @@ -369,6 +411,100 @@ class EncryptedStanzaSession(StanzaSession): self.send(accept) self.status = 'identified-alice' + + # 4.5 esession accept (bob) + def accept_e2e_bob(self, form): + response = xmpp.Message() + + init = response.NT.init + init.setNamespace('http://www.xmpp.org/extensions/xep-0116.html#ns-init') + + x = xmpp.DataForm(typ='result') + + for field in ('nonce', 'dhkeys', 'rshashes', 'identity', 'mac'): + assert field in form.asDict(), "your acceptance form didn't have a %s field" % repr(field) + + # 4.5.1 generating provisory session keys + e = self.decode_mpi(base64.b64decode(form['dhkeys'])) + p = dh.primes[self.modp] + + if not self.sha256(self.encode_mpi(e)) == self.He: + # XXX return + pass + + if not e > 1 and e < (p - 1): + # XXX return + pass + + k = self.sha256(self.encode_mpi(self.powmod(e, self.y, p))) + + self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) + + # 4.5.2 verifying alice's identity + id_a = base64.b64decode(form['identity']) + m_a = base64.b64decode(form['mac']) + + m_a_calculated = self.hmac(self.km_o, self.encode_mpi(self.c_o) + id_a) + + if m_a_calculated != m_a: + # XXX return + pass + + mac_a = self.decrypt(id_a) + + macable_children = filter(lambda x: x.getVar() not in ('mac', 'identity'), form.getChildren()) + form_a2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), macable_children)) + + mac_a_calculated = self.hmac(self.ks_o, self.n_s + self.n_o + self.encode_mpi(e) + self.form_a + form_a2) + + if mac_a_calculated != mac_a: + # XXX return + pass + + # TODO: 4.5.3 + + # 4.5.4 generating bob's final session keys + self.srs = '' + oss = '' + + k = self.sha256(k + self.srs + oss) + + # XXX I can skip generating ks_o here + self.kc_s, self.km_s, self.ks_s = self.generate_responder_keys(k) + self.kc_o, self.km_o, self.ks_o = self.generate_initiator_keys(k) + + # 4.5.5 + if self.srs: + srshash = self.hmac(self.srs, 'Shared Retained Secret') + else: + srshash = self.random_bytes(32) + + x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn')) + x.addChild(node=xmpp.DataField(name='nonce', value=base64.b64encode(self.n_o))) + x.addChild(node=xmpp.DataField(name='srshash', value=base64.b64encode(srshash))) + + form_b2 = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren())) + + old_c_s = self.c_s + mac_b = self.hmac(self.n_o + self.n_s + self.encode_mpi(self.d) + self.form_b + form_b2, self.ks_s) + id_b = self.encrypt(mac_b) + + m_b = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_b) + + x.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_b))) + x.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_b))) + + init.addChild(node=x) + + self.send(response) + + # destroy all copies of srs + + self.srs = self.hmac(k, 'New Retained Secret') + + # destroy k + self.status = 'active' + self.enable_encryption = True def final_steps_alice(self, form): # Alice MUST identify the shared retained secret (SRS) by selecting from her client's list of the secrets it retained from sessions with Bob's clients (the most recent secret for each of the clients he has used to negotiate ESessions with Alice's client). @@ -409,24 +545,63 @@ class EncryptedStanzaSession(StanzaSession): self.status = 'active' self.enable_encryption = True - def accept_e2e_bob(self): - pass - + # generate a random number between 'bottom' and 'top' + def srand(self, bottom, top): + # minimum number of bytes needed to represent that range + bytes = int(math.ceil(math.log(top - bottom, 256))) + # FIXME: use a real PRNG + # in retrospect, this is horribly inadequate. + return (self.decode_mpi(os.urandom(bytes)) % (top - bottom)) + bottom + + def make_dhhash(self, modp): + p = dh.primes[modp] + g = dh.generators[modp] + + x = self.srand(2 ** (2 * self.n - 1), p - 1) + + # XXX this may be a source of performance issues + e = self.powmod(g, x, p) + + self.xes[modp] = x + self.es[modp] = e + + He = self.sha256(self.encode_mpi(e)) + + return base64.b64encode(He) + + # a faster version of (base ** exp) % mod + # taken from + def powmod(self, base, exp, mod): + square = base % mod + result = 1 + + while exp > 0: + if exp & 1: # exponent is odd + result = (result * square) % mod + + square = (square * square) % mod + exp /= 2 + + return result + + def terminate_e2e(self): + self.status = None + # # ffd7076498744578d10edabfe7f4a866 # -# ** Base64 encoded encrypted terminate form ** -# ** Base64 encoded old MAC key ** -# ** Base64 encoded a_mac ** +# ** Base64 encoded encrypted terminate form ** +# ** Base64 encoded old MAC key ** +# ** Base64 encoded a_mac ** # # # -# -# -# urn:xmpp:ssn -# -# 1 -# +# +# +# urn:xmpp:ssn +# +# 1 +# # diff --git a/src/gajim.py b/src/gajim.py index ee2eac9a4..aad978fde 100755 --- a/src/gajim.py +++ b/src/gajim.py @@ -1665,14 +1665,22 @@ class Interface: # 2. Alice: requested-e2e # 3. Bob: responded-e2e # 4. Alice: identified-alice - # 5. Bob: identified-bob - # 6. Alice, Bob: active + # 5. Alice, Bob: active - if session.status == 'requested-e2e' and form.getType() == 'submit': - print 'accepting' + if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()): + print 'responding' + session.respond_e2e_bob(form) + return + elif session.status == 'requested-e2e' and form.getType() == 'submit': + print 'accepting (alice)' session.accept_e2e_alice(form) return + elif session.status == 'responded-e2e' and form.getType() == 'result': + print 'accepting (bob)' + session.accept_e2e_bob(form) + return elif session.status == 'identified-alice' and form.getType() == 'result': + print 'completing' session.final_steps_alice(form) return