disable logs in encrypted sessions.

This commit is contained in:
Brendan Taylor 2007-06-29 04:12:08 +00:00
parent 4bd805cf07
commit 6fe668d863
6 changed files with 256 additions and 64 deletions

View File

@ -1948,10 +1948,13 @@ class ChatControl(ChatControlBase):
def _on_toggle_e2e_menuitem_activate(self, widget):
if self.session.enable_encryption:
self.session.enable_encryption = False
self.session.terminate_e2e()
jid = str(self.session.jid)
gajim.connections[self.account].delete_session(jid, self.session.thread_id)
self.session = gajim.connections[self.account].make_new_session(jid)
else:
self.session.enable_encryption = True
self.session.negotiate_e2e()
def got_connected(self):

View File

@ -921,10 +921,7 @@ class Connection(ConnectionHandlers):
self.connection.send(msg_iq)
no_log_for = gajim.config.get_per('accounts', self.name, 'no_log_for')\
.split()
ji = gajim.get_jid_without_resource(jid)
if self.name not in no_log_for and ji not in no_log_for:
if session.is_loggable():
log_msg = msg
if subject:
log_msg = _('Subject: %s\n%s') % (subject, msg)

View File

@ -1461,11 +1461,6 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
tim = time.strptime(tim, '%Y%m%dT%H:%M:%S')
tim = time.localtime(timegm(tim))
jid = helpers.get_jid_from_iq(msg)
no_log_for = gajim.config.get_per('accounts', self.name,
'no_log_for')
if not no_log_for:
no_log_for = ''
no_log_for = no_log_for.split()
encrypted = False
chatstate = None
encTag = msg.getTag('x', namespace = common.xmpp.NS_ENCRYPTED)
@ -1525,7 +1520,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if not error_msg:
error_msg = msgtxt
msgtxt = None
if self.name not in no_log_for:
if session.is_loggable():
gajim.logger.write('error', frm, error_msg, tim = tim,
subject = subject)
self.dispatch('MSGERROR', (frm, msg.getErrorCode(), error_msg, msgtxt,
@ -1544,15 +1539,14 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if not self.last_history_line.has_key(jid):
return
self.dispatch('GC_MSG', (frm, msgtxt, tim, has_timestamp, msghtml))
if self.name not in no_log_for and not int(float(time.mktime(tim)))\
if session.is_loggable() and not int(float(time.mktime(tim)))\
<= self.last_history_line[jid] and msgtxt:
gajim.logger.write('gc_msg', frm, msgtxt, tim = tim)
return
elif mtype == 'chat': # it's type 'chat'
if not msg.getTag('body') and chatstate is None: #no <body>
return
if msg.getTag('body') and self.name not in no_log_for and jid not in\
no_log_for and msgtxt:
if msg.getTag('body') and session.is_loggable() and msgtxt:
msg_id = gajim.logger.write('chat_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
else: # it's single message
@ -1564,7 +1558,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
password = invite.getTagData('password')
self.dispatch('GC_INVITATION',(frm, jid_from, reason, password))
return
if self.name not in no_log_for and jid not in no_log_for and msgtxt:
if session.is_loggable()and msgtxt:
gajim.logger.write('single_msg_recv', frm, msgtxt, tim = tim,
subject = subject)
mtype = 'normal'
@ -1576,7 +1570,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
# END messageCB
def get_session(self, jid, thread_id, type):
'''returns an existing session between this connection and 'jid' or starts a new one.'''
'''returns an existing session between this connection and 'jid', returns a new one if none exist.'''
session = self.find_session(jid, thread_id, type)
if session:
@ -1588,6 +1582,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
if bare_jid != jid:
session = self.find_session(bare_jid, thread_id, type)
if session:
print repr(bare_jid), repr(thread_id), repr(jid.split("/")[1])
self.move_session(bare_jid, thread_id, jid.split("/")[1])
return session
@ -1609,6 +1604,7 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
del self.sessions[jid]
def move_session(self, original_jid, thread_id, to_resource):
'''moves a session to another resource.'''
session = self.sessions[original_jid][thread_id]
del self.sessions[original_jid][thread_id]
@ -1622,13 +1618,15 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionDisco,
self.sessions[new_jid][thread_id] = session
def find_null_session(self, jid):
'''returns the session between this connecting and 'jid' that we last sent a message in.
this is needed to handle clients that don't support threads; see XEP-0201.'''
all = self.sessions[jid].values()
null_sessions = filter(lambda s: not s.received_thread_id, all)
null_sessions.sort(key=lambda s: s.last_send)
'''finds all of the sessions between us and jid that jid hasn't sent a thread_id in yet.
return null_sessions[-1]
returns the session that we last sent a message to.'''
sessions_with_jid = self.sessions[jid].values()
no_threadid_sessions = filter(lambda s: not s.received_thread_id, sessions_with_jid)
no_threadid_sessions.sort(key=lambda s: s.last_send)
return no_threadid_sessions[-1]
def make_new_session(self, jid, thread_id = None, type = 'chat'):
sess = EncryptedStanzaSession(self, jid, thread_id, type)

View File

@ -41,7 +41,7 @@ class StanzaSession(object):
self.last_send = 0
self.status = None
self.features = {}
self.negotiated = {}
def generate_thread_id(self):
return "".join([random.choice(string.letters) for x in xrange(0,32)])
@ -55,6 +55,29 @@ class StanzaSession(object):
self.last_send = time.time()
def reject_negotiation(self, body = None):
msg = xmpp.Message()
feature = msg.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='accept', value='0'))
feature.addChild(node=x)
if body:
msg.setBody(body)
self.send(msg)
self.cancelled_negotiation()
def cancelled_negotiation(self):
'''A negotiation has been cancelled, so reset this session to its default state.'''
self.status = None
self.negotiated = {}
def terminate(self):
msg = xmpp.Message()
feature = msg.NT.feature
@ -101,6 +124,8 @@ class EncryptedStanzaSession(StanzaSession):
def __init__(self, conn, jid, thread_id, type = 'chat'):
StanzaSession.__init__(self, conn, jid, thread_id, type = 'chat')
self.loggable = True
self.xes = {}
self.es = {}
@ -195,12 +220,24 @@ class EncryptedStanzaSession(StanzaSession):
def hmac(self, key, content):
return HMAC.new(key, content, self.hash_alg).digest()
# this should be more generic?
def sha256(self, string):
sh = SHA256.new()
sh.update(string)
return sh.digest()
base28_chr = "acdefghikmopqruvwxy123456789"
def sas_28x5(self, m_a, form_b):
sha = self.sha256(m_a + form_b + 'Short Authentication String')
lsb24 = self.decode_mpi(sha[-3:])
return self.base28(lsb24)
def base28(self, n):
if n >= 28:
return self.base28(n / 28) + self.base28_chr[n % 28]
else:
return self.base28_chr[n]
def generate_initiator_keys(self, k):
return (self.hmac(k, 'Initiator Cipher Key'),
self.hmac(k, 'Initiator MAC Key'),
@ -265,7 +302,15 @@ class EncryptedStanzaSession(StanzaSession):
def decrypt(self, ciphertext):
return self.decrypter.decrypt(ciphertext)
def logging_preference(self):
if gajim.config.get('log_encrypted_sessions'):
return ["may", "mustnot"]
else:
return ["mustnot", "may"]
def negotiate_e2e(self):
self.negotiated = {}
request = xmpp.Message()
feature = request.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
@ -276,8 +321,7 @@ class EncryptedStanzaSession(StanzaSession):
x.addChild(node=xmpp.DataField(name='accept', value='1', typ='boolean', required=True))
# this field is incorrectly called 'otr' in XEPs 0116 and 0217
# unsupported options: 'mustnot'
x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=['may'], required=True))
x.addChild(node=xmpp.DataField(name='logging', typ='list-single', options=self.logging_preference(), required=True))
# unsupported options: 'disabled', 'enabled'
x.addChild(node=xmpp.DataField(name='disclosure', typ='list-single', options=['never'], required=True))
@ -317,12 +361,10 @@ class EncryptedStanzaSession(StanzaSession):
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)
x = xmpp.DataForm(typ='submit')
def verify_options_bob(self, form):
negotiated = {}
not_acceptable = []
ask_user = {}
fixed = { 'disclosure': 'never',
'security': 'e2e',
@ -333,21 +375,16 @@ class EncryptedStanzaSession(StanzaSession):
'init_pubkey': 'none',
'resp_pubkey': 'none',
'ver': '1.0',
'sas_algs': 'sas28x5',
'logging': 'may' }
not_acceptable = []
'sas_algs': 'sas28x5' }
self.encryptable_stanzas = ['message']
self.sas_algs = 'sas28x5'
self.cipher = AES
self.hash_alg = SHA256
self.compression = None
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='accept', value='true'))
for name, field in map(lambda name: (name, request_form.getField(name)), request_form.asDict().keys()):
for name, field in map(lambda name: (name, form.getField(name)), form.asDict().keys()):
options = map(lambda x: x[1], field.getOptions())
values = field.getValues()
@ -356,28 +393,61 @@ class EncryptedStanzaSession(StanzaSession):
if name in fixed:
if fixed[name] in options:
x.addChild(node=xmpp.DataField(name=name, value=fixed[name]))
negotiated[name] = fixed[name]
else:
not_acceptable.append(name)
elif name == 'modp':
elif name == 'rekey_freq':
preferred = int(options[0])
negotiated['rekey_freq'] = preferred
self.rekey_freq = preferred
elif name == 'logging':
my_prefs = self.logging_preference()
if my_prefs[0] in options:
pref = my_prefs[0]
negotiated['logging'] = pref
else:
for pref in my_prefs:
if pref in options:
ask_user['logging'] = pref
break
if not 'logging' in ask_user:
not_acceptable.append(name)
else:
# some things are handled elsewhere, some things are not-implemented
pass
return (negotiated, not_acceptable, ask_user)
# 4.3 esession response (bob)
def respond_e2e_bob(self, form, negotiated, not_acceptable):
response = xmpp.Message()
feature = response.NT.feature
feature.setNamespace(xmpp.NS_FEATURE)
x = xmpp.DataForm(typ='submit')
x.addChild(node=xmpp.DataField(name='FORM_TYPE', value='urn:xmpp:ssn'))
x.addChild(node=xmpp.DataField(name='accept', value='true'))
for name in negotiated:
x.addChild(node=xmpp.DataField(name=name, value=negotiated[name]))
self.negotiated = negotiated
# the offset of the group we chose (need it to match up with the dhhash)
group_order = 0
self.modp = int(options[group_order])
self.modp = int(form.getField('modp').getOptions()[group_order][1])
x.addChild(node=xmpp.DataField(name='modp', value=self.modp))
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))
self.rekey_freq = preferred
elif name == 'my_nonce':
self.n_o = base64.b64decode(field.getValue())
self.n_o = base64.b64decode(form['my_nonce'])
# XXX do something with not_acceptable
self.He = request_form.getField('dhhashes').getValues()[group_order].encode("utf8")
dhhashes = form.getField('dhhashes').getValues()
self.He = dhhashes[group_order].encode("utf8")
bytes = int(self.n / 8)
@ -389,30 +459,61 @@ class EncryptedStanzaSession(StanzaSession):
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 }
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_a = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), x.getChildren()))
self.status = 'responded-e2e'
feature.addChild(node=x)
if not_acceptable:
pass
# XXX
# <error code='406' type='modify'>
# <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
# <feature xmlns='http://jabber.org/protocol/feature-neg'>
# <field var='security'/>
# </feature>
# </error>
self.send(response)
# 'Alice Accepts'
def accept_e2e_alice(self, form):
def verify_options_alice(self, form):
# 1. Verify that the ESession options selected by Bob are acceptable
negotiated = {}
ask_user = {}
not_acceptable = []
if not form['logging'] in self.logging_preference():
not_acceptable.append(form['logging'])
elif form['logging'] != self.logging_preference()[0]:
ask_user['logging'] = form['logging']
else:
negotiated['logging'] = self.logging_preference()[0]
return (negotiated, not_acceptable, ask_user)
# 'Alice Accepts', continued
def accept_e2e_alice(self, form, negotiated):
self.encryptable_stanzas = ['message']
self.sas_algs = 'sas28x5'
self.cipher = AES
self.hash_alg = SHA256
self.compression = None
self.negotiated = negotiated
# 2. Return a <not-acceptable/> error to Bob unless: 1 < d < p - 1
self.form_b = ''.join(map(lambda el: xmpp.c14n.c14n(el), form.getChildren()))
@ -457,6 +558,11 @@ class EncryptedStanzaSession(StanzaSession):
m_a = self.hmac(self.km_s, self.encode_mpi(old_c_s) + id_a)
# check for a retained secret
# if none exists, prompt the user with the SAS
if self.sas_algs == 'sas28x5':
print "sas: %s" % self.sas_28x5(m_a, self.form_b)
result.addChild(node=xmpp.DataField(name='identity', value=base64.b64encode(id_a)))
result.addChild(node=xmpp.DataField(name='mac', value=base64.b64encode(m_a)))
@ -520,6 +626,11 @@ class EncryptedStanzaSession(StanzaSession):
self.srs = ''
oss = ''
# check for a retained secret
# if none exists, prompt the user with the SAS
if self.sas_algs == 'sas28x5':
print "sas: %s" % self.sas_28x5(m_a, self.form_b)
k = self.sha256(k + self.srs + oss)
# XXX I can skip generating ks_o here
@ -556,6 +667,10 @@ class EncryptedStanzaSession(StanzaSession):
self.srs = self.hmac(k, 'New Retained Secret')
# destroy k
if self.negotiated['logging'] == 'mustnot':
self.loggable = False
self.status = 'active'
self.enable_encryption = True
@ -595,6 +710,9 @@ class EncryptedStanzaSession(StanzaSession):
# Note: If Alice discovers an error then she SHOULD ignore any encrypted content she received in the stanza.
# XXX check for MAC equality?
if self.negotiated['logging'] == 'mustnot':
self.loggable = False
self.status = 'active'
self.enable_encryption = True
@ -647,3 +765,14 @@ class EncryptedStanzaSession(StanzaSession):
StanzaSession.acknowledge_termination(self)
self.enable_encryption = False
def is_loggable(self):
name = self.conn.name
no_log_for = gajim.config.get_per('accounts', name, 'no_log_for')
if not no_log_for:
no_log_for = ''
no_log_for = no_log_for.split()
return self.loggable and name not in no_log_for and self.jid not in no_log_for

View File

@ -1658,12 +1658,63 @@ class Interface:
def handle_session_negotiation(self, account, data):
jid, session, form = data
# encrypted session states
if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()):
session.respond_e2e_bob(form)
if form.getField('accept') and not form['accept'] in ('1', 'true'):
dialogs.InformationDialog(_('Session negotiation cancelled.'),
_('The client at %s cancelled the session negotiation.') % (jid))
session.cancelled_negotiation()
return
# encrypted session states. these are descriped in stanza_session.py
# bob responds
if form.getType() == 'form' and u'e2e' in map(lambda x: x[1], form.getField('security').getOptions()):
negotiated, not_acceptable, ask_user = session.verify_options_bob(form)
if ask_user:
def accept_nondefault_options(widget):
negotiated.update(ask_user)
session.respond_e2e_bob(form, negotiated, not_acceptable)
dialog.destroy()
def reject_nondefault_options(widget):
for key in ask_user.keys():
not_acceptable.append(key) # XXX for some reason I can't concatenate using += here?
session.respond_e2e_bob(form, negotiated, not_acceptable)
dialog.destroy()
dialog = dialogs.ConfirmationDialog(_('confirm these negotiation options'),
_('are the following options acceptable? %s') % (ask_user),
on_response_ok = accept_nondefault_options,
on_response_cancel = reject_nondefault_options)
else:
session.respond_e2e_bob(form, negotiated, not_acceptable)
return
# alice accepts
elif session.status == 'requested-e2e' and form.getType() == 'submit':
session.accept_e2e_alice(form)
negotiated, not_acceptable, ask_user = session.verify_options_alice(form)
if ask_user:
def accept_nondefault_options(widget):
negotiated.update(ask_user)
session.accept_e2e_alice(form, negotiated)
dialog.destroy()
def reject_nondefault_options(widget):
session.reject_negotiation()
dialog.destroy()
dialog = dialogs.ConfirmationDialog(_('confirm these negotiation options'),
_('are the following options acceptable? %s') % (ask_user),
on_response_ok = accept_nondefault_options,
on_response_cancel = reject_nondefault_options)
else:
session.accept_e2e_alice(form, negotiated)
return
elif session.status == 'responded-e2e' and form.getType() == 'result':
session.accept_e2e_bob(form)
@ -1672,6 +1723,18 @@ class Interface:
session.final_steps_alice(form)
return
if form.getField('terminate'):
if form.getField('terminate').getValue() in ('1', 'true'):
session.acknowledge_termination()
gajim.connections[account].delete_session(str(jid), session.thread_id)
ctrl = gajim.interface.msg_win_mgr.get_control(str(jid), account)
if ctrl:
ctrl.session = gajim.connections[self.account].make_new_session(str(jid))
return
# non-esession negotiation. this isn't very useful, but i'm keeping it around
# to test my test suite.
if form.getType() == 'form':

View File

@ -115,6 +115,8 @@ class MessageControl:
return
if self.session:
print "starting a new session, forgetting about the old one!"
gajim.connections[self.account].delete_session(self.contact.jid, self.session.thread_id)
self.session = session
def send_message(self, message, keyID = '', type = 'chat',