diff --git a/data/glade/esession_info_window.glade b/data/glade/esession_info_window.glade new file mode 100644 index 0000000000000000000000000000000000000000..2951b4913d5573e220d593aa5784f8a510a04539 --- /dev/null +++ b/data/glade/esession_info_window.glade @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd"> +<!--Generated with glade3 3.4.0 on Sat Jun 28 20:51:00 2008 --> +<glade-interface> + <widget class="GtkDialog" id="esession_info_window"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="border_width">5</property> + <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property> + <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property> + <property name="has_separator">False</property> + <child internal-child="vbox"> + <widget class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="spacing">2</property> + <child> + <widget class="GtkLabel" id="info_display"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">(ESession info)</property> + <property name="wrap">True</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkHBox" id="verification_info"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <child> + <widget class="GtkImage" id="warning"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="stock">gtk-dialog-warning</property> + </widget> + </child> + <child> + <widget class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">This contact's identity has not been verified.</property> + <property name="wrap">True</property> + <property name="width_chars">0</property> + </widget> + <packing> + <property name="position">1</property> + </packing> + </child> + <child> + <widget class="GtkButton" id="verify_now_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Verify now</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_verify_now_button_clicked"/> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + </widget> + <packing> + <property name="position">2</property> + </packing> + </child> + <child internal-child="action_area"> + <widget class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="layout_style">GTK_BUTTONBOX_END</property> + <child> + <widget class="GtkButton" id="close_button"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="label" translatable="yes">Close</property> + <property name="response_id">0</property> + <signal name="clicked" handler="on_close_button_clicked"/> + </widget> + </child> + </widget> + <packing> + <property name="expand">False</property> + <property name="pack_type">GTK_PACK_END</property> + </packing> + </child> + </widget> + </child> + </widget> +</glade-interface> diff --git a/data/glade/message_window.glade b/data/glade/message_window.glade index 4cb02eeac77c613d20499dd459b1cf2eac8f0afa..be0da608d029df6f7a10631d428c006730f31a53 100644 --- a/data/glade/message_window.glade +++ b/data/glade/message_window.glade @@ -111,12 +111,20 @@ <property name="visible">True</property> <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> <child> - <widget class="GtkImage" id="lock_image"> - <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> - <property name="no_show_all">True</property> - <property name="stock">gtk-dialog-authentication</property> - <property name="icon_size">1</property> - </widget> + <widget class="GtkButton" id="authentication_button"> + <property name="no_show_all">True</property> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="relief">GTK_RELIEF_NONE</property> + <property name="focus_on_click">False</property> + <property name="response_id">0</property> + <child> + <widget class="GtkImage" id="lock_image"> + <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property> + <property name="stock">gtk-dialog-authentication</property> + <property name="icon_size">1</property> + </widget> + </child> + </widget> <packing> <property name="expand">False</property> <property name="padding">2</property> diff --git a/src/chat_control.py b/src/chat_control.py index 1eba60c082e7f54fa338a78cdda2bca8244d8c6d..2ec9cd477d2647c23764e9096ff1e71a2883376d 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -1056,6 +1056,10 @@ class ChatControl(ChatControlBase): self.widget_set_visible(self.xml.get_widget('banner_eventbox'), gajim.config.get('hide_chat_banner')) + self.authentication_button = self.xml.get_widget('authentication_button') + id = self.authentication_button.connect('clicked', self._on_authentication_button_clicked) + self.handlers[id] = self.authentication_button + # Add lock image to show chat encryption self.lock_image = self.xml.get_widget('lock_image') self.lock_tooltip = gtk.Tooltips() @@ -1115,11 +1119,18 @@ class ChatControl(ChatControlBase): self.on_avatar_eventbox_button_press_event) self.handlers[id] = widget - self.session = session + if not session: + session = gajim.connections[self.account].find_controlless_session(self.contact.jid) + self.session = session + if session: session.control = self + self.session = session - # Enable ecryption if needed + if session.enable_encryption: + self.print_esession_details() + + # Enable encryption if needed e2e_is_active = hasattr(self, 'session') and self.session and self.session.enable_encryption self.gpg_is_active = False gpg_pref = gajim.config.get_per('contacts', contact.jid, 'gpg_enabled') @@ -1134,7 +1145,7 @@ class ChatControl(ChatControlBase): if self.session: self.session.loggable = gajim.config.get('log_encrypted_sessions') self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, self.session and \ - self.session.is_loggable()) + self.session.is_loggable(), self.session and self.session.verified_identity) self.status_tooltip = gtk.Tooltips() @@ -1365,7 +1376,7 @@ class ChatControl(ChatControlBase): self.gpg_is_active) self._show_lock_image(self.gpg_is_active, 'GPG', self.gpg_is_active, self.session and \ - self.session.is_loggable()) + self.session.is_loggable(), self.session and self.session.verified_identity) def _show_lock_image(self, visible, enc_type = '', enc_enabled = False, chat_logged = False, authenticated = False): '''Set lock icon visibility and create tooltip''' @@ -1373,22 +1384,26 @@ class ChatControl(ChatControlBase): status_string = enc_enabled and 'is' or 'is NOT' logged_string = chat_logged and 'will' or 'will NOT' - if enc_type == 'OTR': - authenticated_string = authenticated \ - and ' and authenticated' \ - or ' and NOT authenticated' + if authenticated: + authenticated_string = ' and authenticated' + self.lock_image.set_from_stock('gtk-dialog-authentication', 1) else: - authenticated_string = '' + authenticated_string = ' and NOT authenticated' + self.lock_image.set_from_stock('gtk-dialog-warning', 1) - tooltip = '%s Encryption %s active%s.\n' \ + tooltip = '%s encryption %s active%s.\n' \ 'Your chat session %s be logged.' % \ - (enc_type, status_string, authenticated_string, + (enc_type, status_string, authenticated_string, logged_string) - self.lock_tooltip.set_tip(self.lock_image, tooltip) - self.widget_set_visible(self.lock_image, not visible) + self.lock_tooltip.set_tip(self.authentication_button, tooltip) + self.widget_set_visible(self.authentication_button, not visible) self.lock_image.set_sensitive(enc_enabled) + def _on_authentication_button_clicked(self, widget): + if self.session and self.session.enable_encryption: + dialogs.ESessionInfoWindow(self.session) + def _process_command(self, message): if message[0] != '/': return False @@ -1588,11 +1603,15 @@ class ChatControl(ChatControlBase): msg = _('Session WILL NOT be logged') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) + + if not self.session.verified_identity: + ChatControlBase.print_conversation_line(self, 'SAS not verified', 'status', '', None) else: msg = _('E2E encryption disabled') ChatControlBase.print_conversation_line(self, msg, 'status', '', None) + self._show_lock_image(e2e_is_active, 'E2E', e2e_is_active, self.session and \ - self.session.is_loggable()) + self.session.is_loggable(), self.session and self.session.verified_identity) def print_conversation(self, text, frm='', tim=None, encrypted=False, subject=None, xhtml=None, simple=False): diff --git a/src/common/stanza_session.py b/src/common/stanza_session.py index d8ba17109dc3393070e091f2340d5c842f74d447..1f500d80cd641c77aa6d21a1b24cb99bd03d70ee 100644 --- a/src/common/stanza_session.py +++ b/src/common/stanza_session.py @@ -183,6 +183,9 @@ class EncryptedStanzaSession(StanzaSession): # _o denotes 'other' (ie. the client at the other end of the session) self._kc_o = None + # has the remote contact's identity ever been verified? + self.verified_identity = False + # keep the encrypter updated with my latest cipher key def set_kc_s(self, value): self._kc_s = value @@ -338,7 +341,8 @@ class EncryptedStanzaSession(StanzaSession): raise exceptions.NegotiationError, 'calculated m_%s differs from received m_%s' % (i_o, i_o) if i_o == 'a' and self.sas_algs == 'sas28x5': - # XXX not necessary if there's a verified retained secret + # we don't need to calculate this if there's a verified retained secret + # (but we do anyways) self.sas = crypto.sas_28x5(m_o, self.form_s) if self.negotiated['recv_pubkey']: @@ -844,26 +848,32 @@ class EncryptedStanzaSession(StanzaSession): if self.control: self.control.print_esession_details() - # calculate and store the new retained secret - # prompt the user to check the remote party's identity (if necessary) - def do_retained_secret(self, k, srs): + def do_retained_secret(self, k, old_srs): + '''calculate the new retained secret. determine if the user needs to check the remote party's identity. set up callbacks for when the identity has been verified.''' + new_srs = self.hmac(k, 'New Retained Secret') + self.srs = new_srs + account = self.conn.name bjid = self.jid.getStripped() - if srs: - if secrets.secrets().srs_verified(account, bjid, srs): - secrets.secrets().replace_srs(account, bjid, srs, new_srs, True) - else: - def _cb(verified): - secrets.secrets().replace_srs(account, bjid, srs, new_srs, verified) + self.verified_identity = False - self.check_identity(_cb) + if old_srs: + if secrets.secrets().srs_verified(account, bjid, old_srs): + # already had a stored secret verified by the user. + secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, True) + # continue without warning. + self.verified_identity = True + else: + # had a secret, but it wasn't verified. + secrets.secrets().replace_srs(account, bjid, old_srs, new_srs, False) else: - def _cb(verified): - secrets.secrets().save_new_srs(account, bjid, new_srs, verified) + # we don't even have an SRS + secrets.secrets().save_new_srs(account, bjid, new_srs, False) - self.check_identity(_cb) + def _verified_srs_cb(self): + secrets.secrets().replace_srs(self.conn.name, self.jid.getStripped(), self.srs, self.srs, True) def make_dhfield(self, modp_options, sigmai): dhs = [] diff --git a/src/dialogs.py b/src/dialogs.py index 07e88e70ec42216cfb488129715558c4f11430ea..3bff3c7ce6158cb6056343203563a61d382e660a 100644 --- a/src/dialogs.py +++ b/src/dialogs.py @@ -3780,7 +3780,7 @@ class DataFormWindow(Dialog): self.dataform_widget.data_form = self.dataform self.dataform_widget.show_all() self.vbox.pack_start(self.dataform_widget) - + def on_ok(self): form = self.dataform_widget.data_form if isinstance(self.df_response_ok, tuple): @@ -3788,3 +3788,42 @@ class DataFormWindow(Dialog): else: self.df_response_ok(form) self.destroy() + +class ESessionInfoWindow: + '''Class for displaying information about a XEP-0116 encrypted session''' + def __init__(self, session): + self.session = session + + self.xml = gtkgui_helpers.get_glade('esession_info_window.glade') + self.xml.signal_autoconnect(self) + + self.update_info() + + self.window = self.xml.get_widget('esession_info_window') + self.window.show_all() + + def update_info(self): + labeltext = _('''Your chat session with %s is encrypted.\n\nSAS is: %s''') % (self.session.jid, self.session.sas) + + if self.session.verified_identity: + labeltext += '\n\n' + _('''You have already verified this contact's identity.''') + w = self.xml.get_widget('verification_info') + w.set_no_show_all(True) + w.hide() + + self.xml.get_widget('info_display').set_text(labeltext) + + def on_close_button_clicked(self, widget): + self.window.destroy() + + def on_verify_now_button_clicked(self, widget): + pritext = _('''Have you verified the remote contact's identity?''') + sectext = _('''To prevent a man-in-the-middle attack, you should speak to this person directly (in person or on the phone) and verify that they see the same SAS as you.\n\nThis session's SAS: %s''') % self.session.sas + sectext += '\n\n' + _('Did you talk to the remote contact and verify the SAS?') + + dialog = YesNoDialog(pritext, sectext) + + if dialog.get_response() == gtk.RESPONSE_YES: + self.session._verified_srs_cb() + self.session.verified_identity = True + self.update_info() diff --git a/src/negotiation.py b/src/negotiation.py index 8cc4c9ad5f1c52da6d17a42197824ac746b66c23..c2b1fcf56f1972f6e2ff814190c6ae61ac3efef6 100644 --- a/src/negotiation.py +++ b/src/negotiation.py @@ -14,23 +14,6 @@ def describe_features(features): elif features['logging'] == 'mustnot': return _('- messages will not be logged') -def show_sas_dialog(session, jid, sas, on_success): - def success_cb(checked): - on_success(checked) - - def failure_cb(): - session.reject_negotiation() - - dialogs.ConfirmationDialogCheck(_('''OK to continue with negotiation?'''), - _('''You've begun an encrypted session with %s, but it can't be guaranteed that you're talking directly to the person you think you are. - -You should speak with them directly (in person or on the phone) and confirm that their Short Authentication String is identical to this one: %s - -Would you like to continue with the encrypted session?''') % (jid, sas), - - _('Yes, I verified the Short Authentication String'), - on_response_ok=success_cb, on_response_cancel=failure_cb, is_modal=False) - class FeatureNegotiationWindow: '''FeatureNegotiotionWindow class''' def __init__(self, account, jid, session, form): @@ -67,8 +50,6 @@ class FeatureNegotiationWindow: self.window.destroy() def on_cancel_button_clicked(self, widget): - # XXX determine whether to reveal presence - rejection = xmpp.Message(self.jid) rejection.setThread(self.session.thread_id) feature = rejection.NT.feature @@ -80,8 +61,6 @@ class FeatureNegotiationWindow: feature.addChild(node=x) - # XXX optional <body/> - gajim.connections[self.account].send_stanza(rejection) self.window.destroy() diff --git a/src/secrets.py b/src/secrets.py index a95f4cd0d609f82c54ca7e2446b39380fa81b508..0caeda7bfebcf08c5429858e8e78d4214acb4b05 100644 --- a/src/secrets.py +++ b/src/secrets.py @@ -136,7 +136,7 @@ class Secrets: # has the user verified this retained secret? def srs_verified(self, account, jid, srs): return self.find_srs(account, jid, srs)[1] - + def replace_srs(self, account, jid, old_secret, new_secret, verified): our_secrets = self.srs[account][jid] diff --git a/src/session.py b/src/session.py index 651ce2795dcf5a9f2513a4def51e0b1640a7676c..5016e3d5abcbf5d3d6f6726d6eeab6723c5db0c2 100644 --- a/src/session.py +++ b/src/session.py @@ -334,9 +334,6 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): # ---- ESessions stuff --- - def check_identity(self, on_success): - negotiation.show_sas_dialog(self, self.jid, self.sas, on_success) - def handle_negotiation(self, form): if form.getField('accept') and not form['accept'] in ('1', 'true'): self.cancelled_negotiation()