Commit 0f2f3ab8 authored by Dan Pascu's avatar Dan Pascu

Added OTR encryption support for chat

parent 8d54d56e
......@@ -3,16 +3,14 @@
from __future__ import division
__all__ = ['ChatWindow']
import locale
import os
import re
from PyQt4 import uic
from PyQt4.QtCore import Qt, QBuffer, QEasingCurve, QEvent, QPoint, QPointF, QPropertyAnimation, QRect, QRectF, QSettings, QSize, QSizeF, QTimer, QUrl, pyqtSignal
from PyQt4.QtGui import QAction, QBrush, QColor, QIcon, QLabel, QLinearGradient, QListView, QMenu, QPainter, QPalette, QPen, QPixmap, QPolygonF, QTextCursor, QTextDocument, QTextEdit, QToolButton
from PyQt4.QtGui import QApplication, QDesktopServices, QImageReader, QKeyEvent, QTextCharFormat
from PyQt4.QtGui import QApplication, QDesktopServices, QAction, QImageReader, QKeyEvent, QLabel, QListView, QMenu, QTextCursor, QTextDocument, QTextEdit, QToolButton
from PyQt4.QtGui import QBrush, QColor, QIcon, QLinearGradient, QPainter, QPalette, QPen, QPixmap, QPolygonF, QStyle, QStyleOption, QStylePainter, QTextCharFormat
from PyQt4.QtWebKit import QWebPage, QWebSettings, QWebView
from abc import ABCMeta, abstractmethod
......@@ -21,7 +19,7 @@ from application.python import Null, limit
from application.python.descriptor import WriteOnceAttribute
from application.python.types import MarkerType
from application.system import makedirs
from collections import MutableSet
from collections import MutableSet, deque
from datetime import datetime, timedelta
from itertools import count
from lxml import etree, html
......@@ -33,21 +31,26 @@ from sipsimple.account import AccountManager
from sipsimple.application import SIPApplication
from sipsimple.audio import WavePlayer
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.streams.msrp.chat import OTRState
from sipsimple.threading import run_in_thread
from blink.configuration.datatypes import FileURL, GraphTimeScale
from blink.configuration.settings import BlinkSettings
from blink.contacts import URIUtils
from blink.resources import IconManager, Resources
from blink.sessions import ChatSessionModel, ChatSessionListView, SessionManager, StreamDescription
from blink.sessions import ChatSessionModel, ChatSessionListView, SessionManager, StreamDescription, SMPVerification
from blink.util import run_in_gui_thread
from blink.widgets.color import ColorHelperMixin
from blink.widgets.graph import Graph
from blink.widgets.otr import OTRWidget
from blink.widgets.util import ContextMenuActions, QtDynamicProperty
from blink.widgets.video import VideoSurface
from blink.widgets.zrtp import ZRTPWidget
__all__ = ['ChatWindow']
class Container(object): pass
......@@ -395,13 +398,58 @@ class ChatWebView(QWebView):
self.sizeChanged.emit()
ui_class, base_class = uic.loadUiType(Resources.get('chat_input_lock.ui'))
class ChatInputLock(base_class, ui_class):
def __init__(self, parent=None):
super(ChatInputLock, self).__init__(parent)
with Resources.directory:
self.setupUi(self)
if parent is not None:
parent.installEventFilter(self)
def eventFilter(self, watched, event):
if event.type() == QEvent.Resize:
self.setGeometry(watched.contentsRect())
return False
def dragEnterEvent(self, event):
event.ignore() # let the parent process DND
def paintEvent(self, event):
option = QStyleOption()
option.initFrom(self)
painter = QStylePainter(self)
painter.setRenderHint(QStylePainter.Antialiasing, True)
painter.drawPrimitive(QStyle.PE_Widget, option)
class LockType(object):
__metaclass__ = MarkerType
note_text = None
button_text = None
class EncryptionLock(LockType):
note_text = u'Encryption has been terminated by the other party'
button_text = u'Confirm'
class ChatTextInput(QTextEdit):
textEntered = pyqtSignal(unicode)
lockEngaged = pyqtSignal(object)
lockReleased = pyqtSignal(object)
def __init__(self, parent=None):
super(ChatTextInput, self).__init__(parent)
self.setTabStopWidth(22)
self.lock_widget = ChatInputLock(self)
self.lock_widget.hide()
self.lock_widget.confirm_button.clicked.connect(self._SH_LockWidgetConfirmButtonClicked)
self.document().documentLayout().documentSizeChanged.connect(self._SH_DocumentLayoutSizeChanged)
self.lock_queue = deque()
self.history = []
self.history_index = 0 # negative indexes with 0 indicating the text being typed.
self.stashed_content = None
......@@ -412,12 +460,18 @@ class ChatTextInput(QTextEdit):
last_block = document.lastBlock()
return document.characterCount() <= 1 and not last_block.textList()
@property
def locked(self):
return bool(self.lock_queue)
def dragEnterEvent(self, event):
event.ignore() # let the parent process DND
def keyPressEvent(self, event):
key, modifiers = event.key(), event.modifiers()
if key in (Qt.Key_Enter, Qt.Key_Return) and modifiers == Qt.NoModifier:
if self.isReadOnly():
event.ignore()
elif key in (Qt.Key_Enter, Qt.Key_Return) and modifiers == Qt.NoModifier:
document = self.document()
last_block = document.lastBlock()
if document.characterCount() > 1 or last_block.textList():
......@@ -463,6 +517,33 @@ class ChatTextInput(QTextEdit):
def _SH_DocumentLayoutSizeChanged(self, new_size):
self.setFixedHeight(min(new_size.height()+self.contentsMargins().top()+self.contentsMargins().bottom(), self.parent().height()/2))
def _SH_LockWidgetConfirmButtonClicked(self):
self.lockReleased.emit(self.lock_queue.popleft())
if self.locked:
lock_type = self.lock_queue[0]
self.lock_widget.note_label.setText(lock_type.note_text)
self.lock_widget.confirm_button.setText(lock_type.button_text)
self.lockEngaged.emit(lock_type)
else:
self.lock_widget.hide()
self.setReadOnly(False)
def lock(self, lock_type):
if lock_type in self.lock_queue:
raise ValueError("already locked with {}".format(lock_type))
if not self.locked:
self.lock_widget.note_label.setText(lock_type.note_text)
self.lock_widget.confirm_button.setText(lock_type.button_text)
self.lock_widget.show()
self.setReadOnly(True)
self.lockEngaged.emit(lock_type)
self.lock_queue.append(lock_type)
def reset_locks(self):
self.setReadOnly(False)
self.lock_widget.hide()
self.lock_queue.clear()
def clear(self):
super(ChatTextInput, self).clear()
self.setCurrentCharFormat(QTextCharFormat()) # clear() doesn't clear the text formatting, only the content
......@@ -586,6 +667,7 @@ class ChatWidget(base_class, ui_class):
# connect to signals
self.chat_input.textChanged.connect(self._SH_ChatInputTextChanged)
self.chat_input.textEntered.connect(self._SH_ChatInputTextEntered)
self.chat_input.lockReleased.connect(self._SH_ChatInputLockReleased)
self.chat_view.sizeChanged.connect(self._SH_ChatViewSizeChanged)
self.chat_view.page().mainFrame().contentsSizeChanged.connect(self._SH_ChatViewFrameContentsSizeChanged)
self.composing_timer.timeout.connect(self._SH_ComposingTimerTimeout)
......@@ -759,6 +841,10 @@ class ChatWidget(base_class, ui_class):
sender = ChatSender(account.display_name, account.id, self.user_icon.filename)
self.add_message(ChatMessage(content, sender, 'outgoing'))
def _SH_ChatInputLockReleased(self, lock_type):
if lock_type is EncryptionLock:
self.session.chat_stream.encryption.stop()
def _SH_ComposingTimerTimeout(self):
self.composing_timer.stop()
chat_stream = self.session.chat_stream or Null
......@@ -774,6 +860,7 @@ class ChatWidget(base_class, ui_class):
def _NH_BlinkSessionDidEnd(self, notification):
self.composing_timer.stop()
self.chat_input.reset_locks()
def _NH_BlinkSessionWasDeleted(self, notification):
self.setParent(None)
......@@ -781,6 +868,7 @@ class ChatWidget(base_class, ui_class):
def _NH_BlinkSessionDidRemoveStream(self, notification):
if notification.data.stream.type == 'chat':
self.composing_timer.stop()
self.chat_input.reset_locks()
del ui_class, base_class
......@@ -1366,6 +1454,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.info_panel.installEventFilter(self)
self.audio_encryption_label.installEventFilter(self)
self.video_encryption_label.installEventFilter(self)
self.chat_encryption_label.installEventFilter(self)
self.latency_graph.installEventFilter(self)
self.packet_loss_graph.installEventFilter(self)
......@@ -1388,6 +1477,8 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.session_model.sessionRemoved.connect(self._SH_SessionModelSessionRemoved)
self.session_model.sessionAboutToBeRemoved.connect(self._SH_SessionModelSessionAboutToBeRemoved)
self.session_list.selectionModel().selectionChanged.connect(self._SH_SessionListSelectionChanged)
self.otr_widget.nameChanged.connect(self._SH_OTRWidgetNameChanged)
self.otr_widget.statusChanged.connect(self._SH_OTRWidgetStatusChanged)
self.zrtp_widget.nameChanged.connect(self._SH_ZRTPWidgetNameChanged)
self.zrtp_widget.statusChanged.connect(self._SH_ZRTPWidgetStatusChanged)
......@@ -1406,6 +1497,8 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
notification_center.add_observer(self, name='ChatStreamDidSendMessage')
notification_center.add_observer(self, name='ChatStreamDidDeliverMessage')
notification_center.add_observer(self, name='ChatStreamDidNotDeliverMessage')
notification_center.add_observer(self, name='ChatStreamOTREncryptionStateChanged')
notification_center.add_observer(self, name='ChatStreamOTRError')
notification_center.add_observer(self, name='MediaStreamDidInitialize')
notification_center.add_observer(self, name='MediaStreamDidNotInitialize')
notification_center.add_observer(self, name='MediaStreamDidStart')
......@@ -1427,6 +1520,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.no_sessions_label = NoSessionsLabel(self)
self.no_sessions_label.setObjectName('no_sessions_label')
self.otr_widget = OTRWidget(self.info_panel)
self.zrtp_widget = ZRTPWidget(self.info_panel)
self.zrtp_widget.stream_type = None
......@@ -1518,6 +1612,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.session_list.hide()
self.otr_widget.hide()
self.zrtp_widget.hide()
self.info_panel_files_button.hide()
self.info_panel_participants_button.hide()
......@@ -1536,6 +1631,8 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.audio_encryption_label.stream_type = 'audio'
self.video_encryption_label.stream_type = 'video'
self.chat_encryption_label.hovered = False
# prepare self.session_widget so we can take over some of its painting and behaviour
self.session_widget.setAttribute(Qt.WA_Hover, True)
self.session_widget.hovered = False
......@@ -1547,6 +1644,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
old_session = self.__dict__.get('selected_session', None)
new_session = self.__dict__['selected_session'] = session
if new_session != old_session:
self.otr_widget.hide()
self.zrtp_widget.hide()
self.zrtp_widget.stream_type = None
notification_center = NotificationCenter()
......@@ -1738,7 +1836,7 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.video_encryption_label.setVisible(video_info.encryption is not None)
if self.zrtp_widget.isVisibleTo(self.info_panel):
# refresh the zrtp widget (we need to hide/change/show because in certain configurations it flickers when changed while visible)
# refresh the ZRTP widget (we need to hide/change/show because in certain configurations it flickers when changed while visible)
stream_info = blink_session.info.streams[self.zrtp_widget.stream_type]
self.zrtp_widget.hide()
self.zrtp_widget.peer_name = stream_info.zrtp_peer_name
......@@ -1757,10 +1855,29 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
else:
self.chat_value_label.setText(u'N/A')
self.chat_encryption_label.setToolTip(u'Media is encrypted using TLS')
if chat_info.encryption is not None and chat_info.transport == 'tls':
self.chat_encryption_label.setToolTip(u'Media is encrypted using TLS and {0.encryption} ({0.encryption_cipher})'.format(chat_info))
elif chat_info.encryption is not None:
self.chat_encryption_label.setToolTip(u'Media is encrypted using {0.encryption} ({0.encryption_cipher})'.format(chat_info))
elif chat_info.transport == 'tls':
self.chat_encryption_label.setToolTip(u'Media is encrypted using TLS')
else:
self.chat_encryption_label.setToolTip(u'Media is not encrypted')
self._update_chat_encryption_icon()
self.chat_connection_label.setVisible(chat_info.remote_address is not None)
self.chat_encryption_label.setVisible(chat_info.remote_address is not None and chat_info.transport=='tls')
self.chat_encryption_label.setVisible(chat_info.remote_address is not None and (chat_info.encryption is not None or chat_info.transport == 'tls'))
if self.otr_widget.isVisibleTo(self.info_panel):
# refresh the OTR widget (we need to hide/change/show because in certain configurations it flickers when changed while visible)
stream_info = blink_session.info.streams.chat
self.otr_widget.hide()
self.otr_widget.peer_name = stream_info.otr_peer_name
self.otr_widget.peer_verified = stream_info.otr_verified
self.otr_widget.peer_fingerprint = stream_info.otr_peer_fingerprint
self.otr_widget.my_fingerprint = stream_info.otr_key_fingerprint
self.otr_widget.smp_status = stream_info.smp_status
self.otr_widget.show()
if screen_info.remote_address is not None and screen_info.mode == 'active':
self.screen_value_label.setText(u'Viewing remote')
......@@ -1804,6 +1921,17 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
else:
encryption_label.setPixmap(self.pixmaps.grey_lock)
def _update_chat_encryption_icon(self):
stream = self.selected_session.chat_stream
stream_info = self.selected_session.blink_session.info.streams.chat
if self.chat_encryption_label.isEnabled() and stream_info.encryption == 'OTR':
if self.chat_encryption_label.hovered and stream is not None and not stream._done:
self.chat_encryption_label.setPixmap(self.pixmaps.light_green_lock if stream_info.otr_verified else self.pixmaps.light_orange_lock)
else:
self.chat_encryption_label.setPixmap(self.pixmaps.green_lock if stream_info.otr_verified else self.pixmaps.orange_lock)
else:
self.chat_encryption_label.setPixmap(self.pixmaps.grey_lock)
def show(self):
super(ChatWindow, self).show()
self.raise_()
......@@ -1853,11 +1981,27 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
watched.setPixmap(self.pixmaps.grey_lock)
elif event_type in (QEvent.MouseButtonPress, QEvent.MouseButtonDblClick) and event.button() == Qt.LeftButton and event.modifiers() == Qt.NoModifier and watched.isEnabled():
self._EH_RTPEncryptionLabelClicked(watched)
elif watched is self.chat_encryption_label:
if event_type == QEvent.Enter:
watched.hovered = True
self._update_chat_encryption_icon()
elif event_type == QEvent.Leave:
watched.hovered = False
self._update_chat_encryption_icon()
elif event_type == QEvent.EnabledChange and not watched.isEnabled():
watched.setPixmap(self.pixmaps.grey_lock)
elif event_type in (QEvent.MouseButtonPress, QEvent.MouseButtonDblClick) and event.button() == Qt.LeftButton and event.modifiers() == Qt.NoModifier and watched.isEnabled():
self._EH_ChatEncryptionLabelClicked()
elif watched is self.info_panel:
if event_type == QEvent.Resize and self.zrtp_widget.isVisibleTo(self.info_panel):
rect = self.zrtp_widget.geometry()
rect.setWidth(self.info_panel.width())
self.zrtp_widget.setGeometry(rect)
if event_type == QEvent.Resize:
if self.zrtp_widget.isVisibleTo(self.info_panel):
rect = self.zrtp_widget.geometry()
rect.setWidth(self.info_panel.width())
self.zrtp_widget.setGeometry(rect)
if self.otr_widget.isVisibleTo(self.info_panel):
rect = self.otr_widget.geometry()
rect.setWidth(self.info_panel.width())
self.otr_widget.setGeometry(rect)
return False
def drawSessionWidgetIndicators(self):
......@@ -2116,6 +2260,25 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
return
# TODO: implement -Saul
def _NH_ChatStreamOTREncryptionStateChanged(self, notification):
session = notification.sender.blink_session.items.chat
if session is None:
return
if notification.data.new_state is OTRState.Encrypted:
session.chat_widget.add_message(ChatStatus('Encryption enabled'))
elif notification.data.old_state is OTRState.Encrypted:
session.chat_widget.add_message(ChatStatus('Encryption disabled'))
self.otr_widget.hide()
if notification.data.new_state is OTRState.Finished:
session.chat_widget.chat_input.lock(EncryptionLock)
# todo: play sound here?
def _NH_ChatStreamOTRError(self, notification):
session = notification.sender.blink_session.items.chat
if session is not None:
message = "OTR Error: {.error}".format(notification.data)
session.chat_widget.add_message(ChatStatus(message))
def _NH_MediaStreamDidInitialize(self, notification):
if notification.sender.type != 'chat':
return
......@@ -2153,6 +2316,8 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
def _NH_MediaStreamWillEnd(self, notification):
stream = notification.sender
if stream.type == 'chat' and stream.blink_session.items.chat is self.selected_session:
self.otr_widget.hide()
if stream.type == self.zrtp_widget.stream_type and stream.blink_session.items.chat is self.selected_session:
self.zrtp_widget.hide()
self.zrtp_widget.stream_type = None
......@@ -2268,6 +2433,14 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.participants_list.setModel(None)
self.control_button.setEnabled(False)
def _SH_OTRWidgetNameChanged(self):
stream = self.selected_session.chat_stream or Null
stream.encryption.peer_name = self.otr_widget.peer_name
def _SH_OTRWidgetStatusChanged(self):
stream = self.selected_session.chat_stream or Null
stream.encryption.verified = self.otr_widget.peer_verified
def _SH_ZRTPWidgetNameChanged(self):
stream = self.selected_session.blink_session.streams.get(self.zrtp_widget.stream_type, Null)
stream.encryption.zrtp.peer_name = self.zrtp_widget.peer_name
......@@ -2342,6 +2515,25 @@ class ChatWindow(base_class, ui_class, ColorHelperMixin):
self.session_list.show()
self.session_list.animation.start()
def _EH_ChatEncryptionLabelClicked(self):
stream = self.selected_session.chat_stream
stream_info = self.selected_session.blink_session.info.streams.chat
if stream is not None and not stream._done and stream_info.encryption == 'OTR':
if self.otr_widget.isVisible():
self.otr_widget.hide()
else:
encryption_label = self.chat_encryption_label
self.zrtp_widget.hide()
self.otr_widget.peer_name = stream_info.otr_peer_name
self.otr_widget.peer_verified = stream_info.otr_verified
self.otr_widget.peer_fingerprint = stream_info.otr_peer_fingerprint
self.otr_widget.my_fingerprint = stream_info.otr_key_fingerprint
self.otr_widget.smp_status = stream_info.smp_status
self.otr_widget.setGeometry(QRect(0, encryption_label.rect().translated(encryption_label.mapTo(self.info_panel, QPoint(0, 0))).bottom() + 3, self.info_panel.width(), 300))
self.otr_widget.verification_stack.setCurrentWidget(self.otr_widget.smp_panel)
self.otr_widget.show()
self.otr_widget.peer_name_value.setFocus(Qt.OtherFocusReason)
def _EH_RTPEncryptionLabelClicked(self, encryption_label):
stream = self.selected_session.blink_session.streams.get(encryption_label.stream_type)
stream_info = self.selected_session.blink_session.info.streams[encryption_label.stream_type]
......
......@@ -15,6 +15,7 @@ import uuid
from abc import ABCMeta, abstractproperty
from collections import defaultdict, deque
from datetime import datetime, timedelta
from enum import Enum
from itertools import chain
from operator import attrgetter
......@@ -23,6 +24,7 @@ from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEasingCurve, QEven
from PyQt4.QtGui import QBrush, QColor, QDialog, QDrag, QIcon, QLabel, QLinearGradient, QListView, QMenu, QPainter, QPainterPath, QPalette, QPen, QPixmap, QPolygonF, QShortcut
from PyQt4.QtGui import QApplication, QDesktopServices, QStyle, QStyledItemDelegate, QStyleOption
from application import log
from application.notification import IObserver, NotificationCenter, NotificationData, ObserverWeakrefProxy
from application.python import Null, limit
from application.python.types import MarkerType, Singleton
......@@ -39,6 +41,7 @@ from sipsimple.core import SIPCoreError, SIPURI, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.session import Session, IllegalStateError
from sipsimple.streams import MediaStreamRegistry
from sipsimple.streams.msrp.chat import OTRState, SMPStatus
from sipsimple.streams.msrp.filetransfer import FileSelector
from sipsimple.streams.msrp.screensharing import ExternalVNCServerHandler, ExternalVNCViewerHandler, ScreenSharingStream
from sipsimple.threading import run_in_thread, run_in_twisted_thread
......@@ -188,6 +191,7 @@ class ChatStreamInfo(MSRPStreamInfo):
self.otr_peer_fingerprint = None
self.otr_peer_name = ''
self.otr_verified = False
self.smp_status = SMPVerification.Unavailable
def update(self, stream):
super(ChatStreamInfo, self).update(stream)
......@@ -495,6 +499,8 @@ class BlinkSession(BlinkSessionBase):
self._sibling = None
self._smp_handler = Null
def _get_state(self):
return self.__dict__['state']
......@@ -917,6 +923,11 @@ class BlinkSession(BlinkSessionBase):
uri.user = self.account.pstn.prefix + uri.user
return uri
def _sync_chat_peer_name(self):
chat_stream = self.streams.active.get('chat', Null)
if chat_stream.encryption.active and chat_stream.encryption.peer_name == u'':
chat_stream.encryption.peer_name = self.info.streams.audio.zrtp_peer_name
def _SH_TimerFired(self):
self.info.duration += timedelta(seconds=1)
self.info.streams.audio.update_statistics(self.streams.get('audio', Null).statistics)
......@@ -1080,6 +1091,11 @@ class BlinkSession(BlinkSessionBase):
if audio_stream.encryption.type == 'ZRTP' and audio_stream.encryption.zrtp.sas is not None and not audio_stream.encryption.zrtp.verified and secure_chat:
stream.send_message(audio_stream.encryption.zrtp.sas, 'application/blink-zrtp-sas')
def _NH_MediaStreamWillEnd(self, notification):
if notification.sender.type == 'chat':
self._smp_handler.stop()
self._smp_handler = Null
def _NH_RTPStreamICENegotiationStateDidChange(self, notification):
if notification.data.state in {'GATHERING', 'GATHERING_COMPLETE', 'NEGOTIATING'}:
stream_info = self.info.streams[notification.sender.type]
......@@ -1124,14 +1140,18 @@ class BlinkSession(BlinkSessionBase):
secure_chat = chat_stream.transport == 'tls' and all(len(path) == 1 for path in (msrp_transport.full_local_path, msrp_transport.full_remote_path)) # tls & direct connection
if secure_chat:
chat_stream.send_message(notification.data.sas, 'application/blink-zrtp-sas')
self._sync_chat_peer_name()
self._smp_handler.handle_notification(notification)
def _NH_RTPStreamZRTPVerifiedStateChanged(self, notification):
self.info.streams[notification.sender.type].update(notification.sender)
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
self._smp_handler.handle_notification(notification)
def _NH_RTPStreamZRTPPeerNameChanged(self, notification):
self.info.streams[notification.sender.type].update(notification.sender)
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
self._sync_chat_peer_name()
def _NH_RTPStreamDidEnableEncryption(self, notification):
self.info.streams[notification.sender.type].update(notification.sender)
......@@ -1148,6 +1168,13 @@ class BlinkSession(BlinkSessionBase):
def _NH_ChatStreamOTREncryptionStateChanged(self, notification):
self.info.streams.chat.update(notification.sender)
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
if notification.data.new_state is OTRState.Encrypted:
self._smp_handler = SMPVerificationHandler(self)
self._smp_handler.start()
elif notification.data.old_state is OTRState.Encrypted:
self._smp_handler.stop()
self._smp_handler = Null
self._sync_chat_peer_name()
def _NH_ChatStreamOTRVerifiedStateChanged(self, notification):
self.info.streams.chat.update(notification.sender)
......@@ -1157,10 +1184,112 @@ class BlinkSession(BlinkSessionBase):
self.info.streams.chat.update(notification.sender)
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'media'}))
def _NH_ChatStreamSMPVerificationDidNotStart(self, notification):
self._smp_handler.handle_notification(notification)
def _NH_ChatStreamSMPVerificationDidStart(self, notification):
self._smp_handler.handle_notification(notification)
def _NH_ChatStreamSMPVerificationDidEnd(self, notification):
self._smp_handler.handle_notification(notification)
def _NH_BlinkContactDidChange(self, notification):
notification.center.post_notification('BlinkSessionContactDidChange', sender=self)
class SMPVerification(Enum):
Unavailable = u'Unavailable'
InProgress = u'In progress'
Succeeded = u'Succeeded'
Failed = u'Failed'
class SMPVerificationHandler(object):
implements(IObserver)
question = u'What is the ZRTP authentication string?'.encode('utf-8')
def __init__(self, blink_session):
"""@type blink_session: BlinkSession"""
self.blink_session = blink_session
self.chat_stream = self.blink_session.streams.get('chat')
self.delay = 0 if self.chat_stream.encryption.key_fingerprint > self.chat_stream.encryption.peer_fingerprint else 1
self.tries = 5
@property
def audio_stream(self):
return self.blink_session.streams.get('audio', Null)
def start(self):
call_later(self.delay, self._do_smp)
def stop(self):
if self.blink_session.info.streams.chat.smp_status is SMPVerification.InProgress:
self.blink_session.info.streams.chat.smp_status = SMPVerification.Unavailable
notification_center = NotificationCenter()
notification_center.post_notification('BlinkSessionInfoUpdated', sender=self.blink_session, data=NotificationData(elements={'media'}))
self.chat_stream = Null
def _do_smp(self):
if self.blink_session.info.streams.chat.smp_status in (SMPVerification.InProgress, SMPVerification.Succeeded, SMPVerification.Failed):
return
audio_stream = self.audio_stream
if audio_stream.encryption.active and audio_stream.encryption.type == 'ZRTP' and audio_stream.encryption.zrtp.verified:
self.chat_stream.encryption.smp_verify(audio_stream.encryption.zrtp.sas, question=self.question)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_ChatStreamSMPVerificationDidNotStart(self, notification):
if notification.data.reason == 'in progress' and self.blink_session.info.streams.chat.smp_status is not SMPVerification.InProgress:
# another SMP exchange prevented us from starting, but it was not accepted. reschedule ours as to not lose our attempt.
call_later(0, self._do_smp)
def _NH_ChatStreamSMPVerificationDidStart(self, notification):
if notification.data.originator == 'local':
self.blink_session.info.streams.chat.smp_status = SMPVerification.InProgress
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self.blink_session, data=NotificationData(elements={'media'}))
return
if self.blink_session.info.streams.chat.smp_status is SMPVerification.Failed or notification.data.question != self.question:
self.chat_stream.encryption.smp_abort()
return
audio_stream = self.audio_stream
if audio_stream.encryption.active and audio_stream.encryption.type == 'ZRTP':
self.chat_stream.encryption.smp_answer(audio_stream.encryption.zrtp.sas)
if self.blink_session.info.streams.chat.smp_status not in (SMPVerification.Succeeded, SMPVerification.Failed):
self.blink_session.info.streams.chat.smp_status = SMPVerification.InProgress
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self.blink_session, data=NotificationData(elements={'media'}))
else:
self.chat_stream.encryption.smp_abort()
def _NH_ChatStreamSMPVerificationDidEnd(self, notification):
if self.blink_session.info.streams.chat.smp_status in (SMPVerification.Succeeded, SMPVerification.Failed):
return
if notification.data.status == SMPStatus.Success:
if notification.data.same_secrets:
self.blink_session.info.streams.chat.smp_status = SMPVerification.Succeeded if self.blink_session.info.streams.audio.zrtp_verified else SMPVerification.Unavailable
else:
self.blink_session.info.streams.chat.smp_status = SMPVerification.Failed
else:
self.blink_session.info.streams.chat.smp_status = SMPVerification.Unavailable
if notification.data.status is SMPStatus.ProtocolError and notification.data.reason == 'startup collision':
self.tries -= 1
self.delay *= 2
if self.tries > 0:
call_later(self.delay, self._do_smp)
elif notification.data.status is SMPStatus.ProtocolError:
log.warning("SMP exchange got protocol error: {}".format(notification.data.reason))
notification.center.post_notification('BlinkSessionInfoUpdated', sender=self.blink_session, data=NotificationData(elements={'media'}))
def _NH_RTPStreamZRTPReceivedSAS(self, notification):
call_later(self.delay, self._do_smp)
def _NH_RTPStreamZRTPVerifiedStateChanged(self, notification):
call_later(self.delay, self._do_smp)
class ClientConference(object):
def __init__(self):
self.sessions = []
......
import re
from PyQt4 import uic
from PyQt4.QtCore import Qt, pyqtSignal
from PyQt4.QtGui import QStyle, QStyleOption, QStylePainter
from blink.resources import Resources
from blink.sessions import SMPVerification
__all__ = ['OTRWidget']
ui_class, base_class = uic.loadUiType(Resources.get('otr_widget.ui'))
class OTRWidget(base_class, ui_class):
closed = pyqtSignal()
nameChanged = pyqtSignal()
statusChanged = pyqtSignal()
color_table = {'green': 'hsv(100, 85%, 100%)', 'orange': 'hsv(20, 85%, 100%)'}
def __init__(self, parent=None):
super(OTRWidget, self).__init__(parent)
with Resources.directory:
self.setupUi(self)
self.__dict__.update(peer_verified=False, smp_status=SMPVerification.Unavailable) # interdependent properties (they need to preexist as their setters read each other)
self.peer_name = ''
self.peer_verified = False
self.peer_fingerprint = ''
self.my_fingerprint = ''
self.smp_status = SMPVerification.Unavailable
self.verification_stack.wrap = True
self.verification_stack.animationDuration = 200
self.close_button.clicked.connect(self.hide)
self.switch_button.clicked.connect(self.verification_stack.slideInNext)
self.peer_name_value.editingFinished.connect(self._check_name_changes)
self.validate_button.clicked.connect(self._SH_ValidateButtonClicked)
self.verification_stack.currentChanged.connect(self._SH_VerificationStackPanelChanged)
@property
def peer_name(self):
return self.peer_name_value.text()
@peer_name.setter
def peer_name(self, name):
self.__dict__['peer_name'] = name
self.peer_name_value.setText(name)
@property
def peer_verified(self):
return self.__dict__['peer_verified']
@peer_verified.setter
def peer_verified(self, verified):
self.__dict__['peer_verified'] = verified
self.validate_button.setText(u'Invalidate' if verified else u'Validate')
self.validate_button.setChecked(verified)
self.validate_button.setEnabled(verified or self.verification_stack.currentWidget() is not self.smp_panel or self.smp_status is SMPVerification.Succeeded)
self.peer_fingerprint_value.setStyleSheet(u'QLabel {{ color: {}; }}'.format(self.color_table['green'] if verified else self.color_table['orange']))
self.smp_status_value.setText(self.smp_status_text)
@property
def peer_fingerprint(self):
return self.__dict__['peer_fingerprint']
@peer_fingerprint.setter
def peer_fingerprint(self, fingerprint):
self.__dict__['peer_fingerprint'] = fingerprint
self.peer_fingerprint_value.setText(self._encode_fingerprint(fingerprint))
@property
def my_fingerprint(self):
return self.__dict__['my_fingerprint']
@my_fingerprint.setter
def my_fingerprint(self, fingerprint):
self.__dict__['my_fingerprint'] = fingerprint
self.my_fingerprint_value.setText(self._encode_fingerprint(fingerprint))
@property
def smp_status(self):
return self.__dict__['smp_status']
@smp_status.setter
def smp_status(self, status):
self.__dict__['smp_status'] = status
self.validate_button.setEnabled(self.peer_verified or self.verification_stack.currentWidget() is not self.smp_panel or self.smp_status is SMPVerification.Succeeded)
self.smp_status_value.setText(self.smp_status_text)
@property
def smp_status_text(self):
if self.peer_verified:
return u'<span style="color: {[green]};">Verified</span>'.format(self.color_table)
elif self.smp_status is SMPVerification.Succeeded:
return u'<span style="color: {[green]};">Succeeded</span>'.format(self.color_table)
elif self.smp_status is SMPVerification.Failed:
return u'<span style="color: {[orange]};">Failed</span>'.format(self.color_table)
else:
return u'{}'.format(self.smp_status.value)
def hideEvent(self, event):
if not event.spontaneous():
self.closed.emit()
self._check_name_changes()
def paintEvent(self, event):
option = QStyleOption()
option.initFrom(self)
painter = QStylePainter(self)
painter.setRenderHint(QStylePainter.Antialiasing, True)
painter.drawPrimitive(QStyle.PE_Widget if self.testAttribute(Qt.WA_NoSystemBackground) else QStyle.PE_Frame, option)
@staticmethod
def _encode_fingerprint(fingerprint):
return re.sub('....', lambda match: match.group(0) + {match.endpos: '', match.endpos//2: '<br/>'}.get(match.end(), ' '), fingerprint.encode('hex').upper())
def _check_name_changes(self):
peer_name = self.peer_name_value.text()
if peer_name != self.__dict__['peer_name']:
self.__dict__['peer_name'] = peer_name
self.nameChanged.emit()
def _SH_ValidateButtonClicked(self, checked):
self.hide()
self.peer_verified = checked
self.statusChanged.emit()
def _SH_VerificationStackPanelChanged(self, index):
self.validate_button.setEnabled(self.peer_verified or self.verification_stack.currentWidget() is not self.smp_panel or self.smp_status is SMPVerification.Succeeded)
del ui_class, base_class
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>lock_widget</class>
<widget class="QWidget" name="lock_widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>655</width>
<height>22</height>
</rect>
</property>
<property name="windowTitle">
<string>Lock</string>
</property>
<property name="styleSheet">
<string notr="true">#lock_widget {
background: hsva(0, 0, 100%, 80%);
border: 0px;
border-radius: 3px;
}
#note_label {
margin: 0px;
padding-left: 4px;
}
#confirm_button {
background: hsv(20, 67%, 100%);
background-origin: border;
border: 0px;
border-left: 1px solid palette(dark);
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
margin: 0px;
padding: 0px 5px 0px 5px;
}
#confirm_button:hover {
background: hsv(20, 60%, 100%);
}
#confirm_button:pressed {
background: hsv(20, 60%, 90%);
}
</string>
</property>
<layout class="QHBoxLayout" name="lock_layout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="ElidedLabel" name="note_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Encryption has been terminated by the other party</string>
</property>
<property name="indent">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="confirm_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string>Confirm</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ElidedLabel</class>
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>otr_widget</class>
<widget class="QFrame" name="otr_widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>220</width>
<height>300</height>
</rect>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="ButtonText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="ButtonText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="ButtonText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="windowTitle">
<string>OTR</string>
</property>
<property name="styleSheet">
<string notr="true">QFrame, QLabel, QLineEdit, QPushButton, QToolButton {
color: white;
}
#otr_widget {
background-color: rgba(51, 51, 51, 230);
background-color: #444444;
border: 1px solid #222222;
border-radius: 4px;
}
#verification_stack &gt; QWidget {
background-color: transparent;
}
#title {
background-color: rgba(0, 0, 0, 100);
border-bottom: 0px solid #222222;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
#switch_button {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.7 transparent, stop:0.71 #aaaaaa, stop:1 #aaaaaa);
background-origin: border;
border: 1px solid #aaaaaa;
border-radius: 1px;
margin: 2px 4px 3px 3px;
padding: 0px;
}
#switch_button:focus {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.7 transparent, stop:0.71 #0093dd, stop:1 #0093dd);
border: 1px solid #0093dd;
}
#switch_button:hover {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.7 transparent, stop:0.71 #c0c0c0, stop:1 #c0c0c0);
border: 1px solid #c0c0c0;
}
#switch_button:hover:focus {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 transparent, stop:0.7 transparent, stop:0.71 #00aaff, stop:1 #00aaff);
border: 1px solid #00aaff;
}
#switch_button:pressed {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #c0c0c0, stop:0.3 #c0c0c0, stop:0.31 transparent, stop:1 transparent);
border: 1px solid #c0c0c0;
}
#switch_button:pressed:focus {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #00aaff, stop:0.3 #00aaff, stop:0.31 transparent, stop:1 transparent);
border: 1px solid #00aaff;
}
#close_button {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 transparent, stop:0.6 transparent, stop:0.7 #aaaaaa, stop:0.85 transparent, stop:1 transparent);
background-origin: border;
border: 0px;
margin: 0px;
padding: 0px;
}
#close_button:focus {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 transparent, stop:0.6 transparent, stop:0.7 #0093dd, stop:0.85 transparent, stop:1 transparent);
}
#close_button:hover {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 transparent, stop:0.6 transparent, stop:0.7 #dddddd, stop:0.85 transparent, stop:1 transparent);
}
#close_button:hover:focus {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 transparent, stop:0.6 transparent, stop:0.7 #00aaff, stop:0.85 transparent, stop:1 transparent);
}
#close_button:pressed {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 #808080, stop:0.6 #606060, stop:0.7 #dddddd, stop:0.85 transparent, stop:1 transparent);
}
#close_button:pressed:focus {
background: qradialgradient(cx: 0.465, cy: 0.465, radius: 0.5, fx:0.5, fy:0.5, stop:0 #808080, stop:0.6 #606060, stop:0.7 #00aaff, stop:0.85 transparent, stop:1 transparent);
}
[role=&quot;value&quot;] {
background-color: rbga(0, 0, 0, 20);
border: 1px solid #202020;
border-radius: 2px;
margin-bottom: 5px;
padding: 1px;
}
[role=&quot;selectable-value&quot;] {
background-color: rbga(0, 0, 0, 20);
border: 1px solid #202020;
border-radius: 2px;
margin-bottom: 5px;
padding: 0px 0px;
selection-background-color: #0066aa;
}
[role=&quot;editable-value&quot;] {
background-color: rgba(0, 0, 0, 40);
border: 2px solid #202020;
border-radius: 4px;
margin-bottom: 5px;
padding: 0px 0px;
selection-background-color: #0066aa;
}
[role=&quot;fingerprint&quot;] {
background-color: rbga(0, 0, 0, 20);
border: 1px solid #202020;
border-radius: 2px;
margin-bottom: 5px;
padding: 3px 1px 1px 1px;
font-family: &quot;Lucida Console&quot;, mono;
}
[role=&quot;note&quot;] {
color: #aaaaaa;
}
[role=&quot;action-button&quot;] {
background: transparent;
background-origin: border;
border: 2px solid #808080;
border-radius: 4px;
margin: 0px;
padding: 0px;
}
[role=&quot;action-button&quot;]:focus {
border: 2px solid #0080c0;
border: 2px solid #0093dd;
}
[role=&quot;action-button&quot;]:hover {
border: 2px solid #c0c0c0;
}
[role=&quot;action-button&quot;]:hover:focus {
border: 2px solid #00aaff;
}
[role=&quot;action-button&quot;]:pressed {
background: qradialgradient(cx: 0.5, cy: 0.5, radius: 1, fx:0.5, fy:0.5, stop:0 #909090, stop:1 transparent);
}
[role=&quot;action-button&quot;]:disabled {
border: 2px solid #555555;
color: #808080;
}
</string>
</property>
<layout class="QVBoxLayout" name="otr_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="title">
<layout class="QHBoxLayout" name="title_layout">
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="title_label">
<property name="text">
<string>OTR Encryption</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="indent">
<number>3</number>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="switch_button">
<property name="minimumSize">
<size>
<width>19</width>
<height>19</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>19</width>
<height>19</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="close_button">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>icons/cross-white.svg</normaloff>icons/cross-white.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="body">
<layout class="QVBoxLayout" name="body_layout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="peer_name_title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Peer Device Name</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="peer_name_value">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="role" stdset="0">
<string notr="true">editable-value</string>
</property>
</widget>
</item>
<item>
<widget class="SlidingStackedWidget" name="verification_stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QFrame" name="fingerprint_panel">
<layout class="QVBoxLayout" name="fingerprint_layout">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="my_fingerprint_title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>My fingerprint</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="my_fingerprint_value">
<property name="text">
<string>C54E 0DD0 178F 2596 C3C9&lt;br/&gt;AE0E 4AD5 F8AC DD05 F7F7</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="role" stdset="0">
<string notr="true">fingerprint</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="peer_fingerprint_title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Peer fingerprint</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="peer_fingerprint_value">
<property name="styleSheet">
<string notr="true">QLabel { color: hsv(20, 85%, 100%); }</string>
</property>
<property name="text">
<string>&lt;span&gt;C54E 0DD0 178F 2596 C3C9&lt;br/&gt;AE0E 4AD5 F8AC DD05 F7F7&lt;/span&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="role" stdset="0">
<string notr="true">fingerprint</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="fingerprint_note">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Verify the peer fingerprint by reading it over a voice call. If it matches, the peer can be validated as trusted (only needs to be done once).</string>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="role" stdset="0">
<string notr="true">note</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QFrame" name="smp_panel">
<layout class="QVBoxLayout" name="smp_layout">
<property name="spacing">
<number>6</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="smp_status_title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>SMP Verification</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="smp_status_value">
<property name="text">
<string>&lt;span style=&quot;color: hsv(20, 85%, 100%);&quot;&gt;Failed&lt;/span&gt;</string>
</property>
<property name="role" stdset="0">
<string notr="true">value</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="smp_note">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>The peer's identity is verified using SMP with the ZRTP authentication string as the shared secret. If the verification is successful, the peer can be validated as trusted (only needs to be done once).
Audio with verified ZRTP is needed for the SMP verification to be available.</string>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="role" stdset="0">
<string notr="true">note</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QToolButton" name="validate_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Validate</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
<property name="role" stdset="0">
<string notr="true">action-button</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SlidingStackedWidget</class>
<extends>QStackedWidget</extends>
<header>blink.widgets.containers</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>peer_name_value</tabstop>
<tabstop>validate_button</tabstop>
<tabstop>close_button</tabstop>
<tabstop>switch_button</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment