Merge branch 'gtk3' into 'gtk3'

Port recent changes to OMEMO

See merge request !16
This commit is contained in:
Philipp Hörist
2017-01-24 14:55:54 +01:00
21 changed files with 2758 additions and 2392 deletions

View File

@@ -1,379 +0,0 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,input
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,e,Run,_,log,ui,iq,db
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set). This supports can work
# with qualified names.
ignored-classes=
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,_show_lock_image
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@@ -1,4 +0,0 @@
[style]
based_on_style = pep8
align_closing_bracket_with_visual_indent = true
join_multiple_lines = true

View File

@@ -1,8 +1,33 @@
1.0.1 / 2017-01-14
- Better XEP Compliance
- Bugfixes
1.0.0 / 2016-12-04
- Bugfixes
0.9.9 / 2016-12-01
- Bugfixes
0.9.8 / 2016-11-28
- Fix a Problem where OMEMO wouldnt activate after the plugin is updated
- Add QR Verification Code to Plugin Config
0.9.7 / 2016-11-12
- Bugfixes
0.9.6 / 2016-11-01
- Bugfixes
0.9.5 / 2016-10-10
- Add GroupChat BETA
- Add Option to delete Fingerprints
- Add Option to deactivate Accounts for OMEMO
0.9.0 / 2016-08-28
- Send INFO message to resources who dont support OMEMO
- Check dependencys and give correct error message
- Dont process PreKeyWhisperMessages without PreKey
- Dont process PGP messages
- Send INFO message to resources who dont support OMEMO
- Check dependencys and give correct error message
- Dont process PreKeyWhisperMessages without PreKey
- Dont process PGP messages
0.8.1 / 2016-08-05
- Query own Device Bundles on send button press

View File

@@ -1,90 +0,0 @@
# OMEMO Plugin for Gajim
This Plugin adds support for the [OMEMO Encryption](http://conversations.im/omemo) to [Gajim](https://gajim.org/). This
plugin is [free software](http://www.gnu.org/philosophy/free-sw.en.html)
distributed under the GNU General Public License version 3 or any later version.
## Installation
Before you open any issues please read our [Wiki](https://github.com/omemo/gajim-omemo/wiki) which addresses some problems that can occur during an install
### Linux
See [Linux Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Linux)
### Windows
See [Windows Wiki](https://github.com/omemo/gajim-omemo/wiki/Installing-on-Windows)
### Via Package Manager
#### Arch
See [Arch Wiki](https://wiki.archlinux.org/index.php/Gajim#OMEMO_Support)
#### Gentoo
`layman -a flow && emerge gajim-omemo`
### Via PluginInstallerPlugin
Install the current stable version via the Gajim PluginManager. You *need* Gajim
version *0.16.5*. If your package manager does not provide an up to date version
you can install it from the official Mercurial repository. *DO NOT USE* gajim
0.16.4 it contains a vulnerability, which is fixed in 0.16.5.
```shell
hg clone https://hg.gajim.org/gajim
cd gajim
hg update gajim-0.16.5 --clean
```
**NOTE:** You *have* to install `python-axolotl` via `pip`. Depending on your setup you might
want to use `pip2` as Gajim is using python2.7. If you are using the official repository,
do not forget to install the `nbxmpp` dependency via pip or you package manager.
if you still have problems, we have written down the most common problems [here](https://github.com/omemo/gajim-omemo/wiki/It-doesnt-work,-what-should-i-do%3F-(Linux))
## Running
Enable *OMEMO Multi-End Message and Object Encryption* in the Plugin-Manager.
If your contact supports OMEMO you should see a new orange fish icon in the chat window.
Encryption will be enabled by default for contacts that support OMEMO.
If you open the chat window, the Plugin will tell you with a green status message if its *enabled* or *disabled*.
If you see no status message, your contact doesnt support OMEMO.
(**Beware**, every status message is green. A green message does not mean encryption is active. Read the message !)
You can also check if encryption is enabled/disabled, when you click on the OMEMO icon.
When you send your first message the Plugin will query your contacts encryption keys and you will
see them in a readable fingerprint format in the fingerprint window which pops up.
you have to trust at least **one** fingerprint to send messages.
you can receive messages from fingerprints where you didnt made a trust decision, but you cant
receive Messages from *not trusted* fingerprints
## Debugging
To see OMEMO related debug output start Gajim with the parameter `-l
gajim.plugin_system.omemo=DEBUG`.
## Hacking
This repository contains the current development version. If you want to
contribute clone the git repository into your Gajim's plugin directory.
```shell
mkdir ~/.local/share/gajim/plugins -p
cd ~/.local/share/gajim/plugins
git clone https://github.com/omemo/gajim-omemo
```
## Support this project
I develop this project in my free time. Your donation allows me to spend more
time working on it and on free software generally.
My Bitcoin Address is: `1CnNM3Mree9hU8eRjCXrfCWVmX6oBnEfV1`
[![Support Me via Flattr](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/thing/5038679)
## I found a bug
Please report it to the [issue
tracker](https://github.com/omemo/gajim-omemo/issues). If you are experiencing
misbehaviour please provide detailed steps to reproduce and debugging output.
Always mention the exact Gajim version.
## Contact
You can contact me via email at `bahtiar@gadimov.de` or follow me on
[Twitter](https://twitter.com/_kalkin)

View File

@@ -1,883 +1 @@
# -*- coding: utf-8 -*-
#
# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
# Copyright 2015 Daniel Gultsch <daniel@cgultsch.de>
#
# This file is part of Gajim-OMEMO plugin.
#
# The Gajim-OMEMO plugin is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# Gajim-OMEMO is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# the Gajim-OMEMO plugin. If not, see <http://www.gnu.org/licenses/>.
#
import logging
import os
import sqlite3
from common import caps_cache, gajim, ged
from common.pep import SUPPORTED_PERSONAL_USER_EVENTS
from plugins import GajimPlugin
from plugins.helpers import log_calls
from nbxmpp.simplexml import Node
from nbxmpp import NS_CORRECT
from . import ui
from .ui import Ui
from .xmpp import (
NS_NOTIFY, NS_OMEMO, NS_EME, BundleInformationAnnouncement,
BundleInformationQuery, DeviceListAnnouncement, DevicelistQuery,
DevicelistPEP, OmemoMessage, successful, unpack_device_bundle,
unpack_device_list_update, unpack_encrypted)
# from common import demandimport
# demandimport.enable()
# demandimport.ignore += ['_imp']
IQ_CALLBACK = {}
AXOLOTL_MISSING = 'You are missing Python-Axolotl or use an outdated version'
PROTOBUF_MISSING = 'OMEMO cant import Google Protobuf, you can find help in ' \
'the GitHub Wiki'
GAJIM_VERSION = 'OMEMO only works with the latest Gajim version, get the ' \
'latest version from gajim.org'
ERROR_MSG = ''
NS_HINTS = 'urn:xmpp:hints'
NS_PGP = 'urn:xmpp:openpgp:0'
DB_DIR = gajim.gajimpaths.data_root
log = logging.getLogger('gajim.plugin_system.omemo')
try:
from .omemo.state import OmemoState
except Exception as e:
log.error(e)
ERROR_MSG = 'Error: {}'.format(e)
try:
import google.protobuf
except Exception as e:
log.error(e)
ERROR_MSG = PROTOBUF_MISSING
try:
SETUPTOOLS_MISSING = False
from pkg_resources import parse_version
except Exception as e:
SETUPTOOLS_MISSING = True
ERROR_MSG = 'You are missing the Setuptools package.'
if not SETUPTOOLS_MISSING:
try:
import axolotl
if parse_version(axolotl.__version__) < parse_version('0.1.35'):
ERROR_MSG = AXOLOTL_MISSING
except Exception as e:
log.error(e)
ERROR_MSG = AXOLOTL_MISSING
# pylint: disable=no-init
# pylint: disable=attribute-defined-outside-init
class OmemoPlugin(GajimPlugin):
omemo_states = {}
ui_list = {}
@log_calls('OmemoPlugin')
def init(self):
""" Init """
if ERROR_MSG:
self.activatable = False
self.available_text = ERROR_MSG
return
self.events_handlers = {
'mam-message-received': (ged.PRECORE, self.mam_message_received),
'message-received': (ged.PRECORE, self.message_received),
'pep-received': (ged.PRECORE, self.handle_device_list_update),
'raw-iq-received': (ged.PRECORE, self.handle_iq_received),
'signed-in': (ged.PRECORE, self.signed_in),
'stanza-message-outgoing':
(ged.PRECORE, self.handle_outgoing_stanza),
'message-outgoing':
(ged.PRECORE, self.handle_outgoing_event),
}
self.config_dialog = ui.OMEMOConfigDialog(self)
self.gui_extension_points = {'chat_control': (self.connect_ui,
self.disconnect_ui)}
SUPPORTED_PERSONAL_USER_EVENTS.append(DevicelistPEP)
self.plugin = self
self.announced = []
self.query_for_bundles = []
@log_calls('OmemoPlugin')
def get_omemo_state(self, account):
""" Returns the the OmemoState for the specified account.
Creates the OmemoState if it does not exist yet.
Parameters
----------
account : str
the account name
Returns
-------
OmemoState
"""
if account not in self.omemo_states:
self.deactivate_gajim_e2e(account)
db_path = os.path.join(DB_DIR, 'omemo_' + account + '.db')
conn = sqlite3.connect(db_path, check_same_thread=False)
my_jid = gajim.get_jid_from_account(account)
self.omemo_states[account] = OmemoState(my_jid, conn, account,
self.plugin)
return self.omemo_states[account]
@staticmethod
def deactivate_gajim_e2e(account):
""" Deativates E2E encryption in Gajim """
gajim.config.set_per('accounts', account,
'autonegotiate_esessions', False)
gajim.config.set_per('accounts', account,
'enable_esessions', False)
log.info(str(account) + " => Gajim E2E encryption disabled")
@log_calls('OmemoPlugin')
def signed_in(self, event):
""" Method called on SignIn
Parameters
----------
event : SignedInEvent
"""
account = event.conn.name
log.debug(account +
' => Announce Support after Sign In')
self.query_for_bundles = []
self.announced = []
self.announced.append(account)
self.publish_bundle(account)
self.query_own_devicelist(account)
@log_calls('OmemoPlugin')
def activate(self):
""" Method called when the Plugin is activated in the PluginManager
"""
self.query_for_bundles = []
if NS_NOTIFY not in gajim.gajim_common_features:
gajim.gajim_common_features.append(NS_NOTIFY)
self._compute_caps_hash()
# Publish bundle information
for account in gajim.connections:
if account not in self.announced:
if gajim.account_is_connected(account):
log.debug(account +
' => Announce Support after Plugin Activation')
self.announced.append(account)
self.publish_bundle(account)
self.query_own_devicelist(account)
@log_calls('OmemoPlugin')
def deactivate(self):
""" Method called when the Plugin is deactivated in the PluginManager
Removes OMEMO from the Entity Capabilities list
"""
if NS_NOTIFY in gajim.gajim_common_features:
gajim.gajim_common_features.remove(NS_NOTIFY)
self._compute_caps_hash()
@staticmethod
def _compute_caps_hash():
""" Computes the hash for Entity Capabilities and publishes it """
for acc in gajim.connections:
gajim.caps_hash[acc] = caps_cache.compute_caps_hash(
[gajim.gajim_identity],
gajim.gajim_common_features +
gajim.gajim_optional_features[acc])
# re-send presence with new hash
connected = gajim.connections[acc].connected
if connected > 1 and gajim.SHOW_LIST[connected] != 'invisible':
gajim.connections[acc].change_status(
gajim.SHOW_LIST[connected], gajim.connections[acc].status)
@log_calls('OmemoPlugin')
def mam_message_received(self, msg):
""" Handles an incoming MAM message
Payload is decrypted and the plaintext is written into the
event object. Afterwards the event is passed on further to Gajim.
Parameters
----------
msg : MamMessageReceivedEvent
Returns
-------
Return means that the Event is passed on to Gajim
"""
if msg.msg_.getTag('openpgp', namespace=NS_PGP):
return
omemo_encrypted_tag = msg.msg_.getTag('encrypted', namespace=NS_OMEMO)
if omemo_encrypted_tag:
account = msg.conn.name
log.debug(account + ' => OMEMO MAM msg received')
state = self.get_omemo_state(account)
from_jid = str(msg.msg_.getAttr('from'))
from_jid = gajim.get_jid_without_resource(from_jid)
msg_dict = unpack_encrypted(omemo_encrypted_tag)
msg_dict['sender_jid'] = from_jid
plaintext = state.decrypt_msg(msg_dict)
if not plaintext:
return
self.print_msg_to_log(msg.msg_)
msg.msgtxt = plaintext
contact_jid = msg.with_
if account in self.ui_list and \
contact_jid in self.ui_list[account]:
self.ui_list[account][contact_jid].activate_omemo()
return False
elif msg.msg_.getTag('body'):
account = msg.conn.name
jid = msg.with_
state = self.get_omemo_state(account)
omemo_enabled = state.encryption.is_active(jid)
if omemo_enabled:
msg.msgtxt = '**Unencrypted** ' + msg.msgtxt
@log_calls('OmemoPlugin')
def message_received(self, msg):
""" Handles an incoming message
Payload is decrypted and the plaintext is written into the
event object. Afterwards the event is passed on further to Gajim.
Parameters
----------
msg : MessageReceivedEvent
Returns
-------
Return means that the Event is passed on to Gajim
"""
if msg.stanza.getTag('openpgp', namespace=NS_PGP):
return
if msg.stanza.getTag('encrypted', namespace=NS_OMEMO) and \
msg.mtype == 'chat':
account = msg.conn.name
log.debug(account + ' => OMEMO msg received')
state = self.get_omemo_state(account)
if msg.forwarded and msg.sent:
from_jid = str(msg.stanza.getTo()) # why gajim? why?
log.debug('message was forwarded doing magic')
else:
from_jid = str(msg.stanza.getFrom())
self.print_msg_to_log(msg.stanza)
msg_dict = unpack_encrypted(msg.stanza.getTag
('encrypted', namespace=NS_OMEMO))
msg_dict['sender_jid'] = gajim.get_jid_without_resource(from_jid)
plaintext = state.decrypt_msg(msg_dict)
if not plaintext:
return
msg.msgtxt = plaintext
# Gajim bug: there must be a body or the message
# gets dropped from history
msg.stanza.setBody(plaintext)
contact_jid = gajim.get_jid_without_resource(from_jid)
if account in self.ui_list and \
contact_jid in self.ui_list[account]:
self.ui_list[account][contact_jid].activate_omemo()
return False
elif msg.stanza.getTag('body') and msg.mtype == 'chat':
account = msg.conn.name
from_jid = str(msg.stanza.getFrom())
jid = gajim.get_jid_without_resource(from_jid)
state = self.get_omemo_state(account)
omemo_enabled = state.encryption.is_active(jid)
if omemo_enabled:
msg.msgtxt = '**Unencrypted** ' + msg.msgtxt
# msg.stanza.setBody(msg.msgtxt)
try:
gui = self.ui_list[account].get(jid, None)
if gui and gui.encryption_active():
gui.plain_warning()
except KeyError:
log.debug('No Ui present for ' + jid +
', Ui Warning not shown')
@log_calls('OmemoPlugin')
def handle_outgoing_event(self, event):
""" Handles a message outgoing event
In this event we have no stanza. XHTML is set to None
so that it doesnt make its way into the stanza
Parameters
----------
event : MessageOutgoingEvent
Returns
-------
Return if encryption is not activated
"""
account = event.account
state = self.get_omemo_state(account)
if not state.encryption.is_active(event.jid):
return False
event.xhtml = None
@log_calls('OmemoPlugin')
def handle_outgoing_stanza(self, event):
""" Manipulates the outgoing stanza
The body is getting encrypted
Parameters
----------
event : StanzaMessageOutgoingEvent
Returns
-------
Return if encryption is not activated or any other
exception or error occurs
"""
try:
if not event.msg_iq.getTag('body'):
return
account = event.conn.name
state = self.get_omemo_state(account)
full_jid = str(event.msg_iq.getAttr('to'))
to_jid = gajim.get_jid_without_resource(full_jid)
if not state.encryption.is_active(to_jid):
return
# Delete previous Message out of Correction Message Stanza
if event.msg_iq.getTag('replace', namespace=NS_CORRECT):
event.msg_iq.delChild('encrypted', attrs={'xmlns': NS_OMEMO})
plaintext = event.msg_iq.getBody().encode('utf-8')
msg_dict = state.create_msg(
gajim.get_jid_from_account(account), to_jid, plaintext)
if not msg_dict:
return True
encrypted_node = OmemoMessage(msg_dict)
# Check if non-OMEMO resource is online
contacts = gajim.contacts.get_contacts(account, to_jid)
non_omemo_resource_online = False
for contact in contacts:
if contact.show == 'offline':
continue
if not contact.supports(NS_NOTIFY):
log.debug(contact.get_full_jid() +
' => Contact doesnt support OMEMO, '
'adding Info Message to Body')
support_msg = 'You received a message encrypted with ' \
'OMEMO but your client doesnt support OMEMO.'
event.msg_iq.setBody(support_msg)
non_omemo_resource_online = True
if not non_omemo_resource_online:
event.msg_iq.delChild('body')
event.msg_iq.addChild(node=encrypted_node)
# XEP-xxxx: Explicit Message Encryption
if not event.msg_iq.getTag('encrypted', attrs={'xmlns': NS_EME}):
eme_node = Node('encrypted', attrs={'xmlns': NS_EME,
'name': 'OMEMO',
'namespace': NS_OMEMO})
event.msg_iq.addChild(node=eme_node)
# Store Hint for MAM
store = Node('store', attrs={'xmlns': NS_HINTS})
event.msg_iq.addChild(node=store)
self.print_msg_to_log(event.msg_iq)
except Exception as e:
log.debug(e)
return True
@log_calls('OmemoPlugin')
def handle_device_list_update(self, event):
""" Check if the passed event is a device list update and store the new
device ids.
Parameters
----------
event : PEPReceivedEvent
Returns
-------
bool
True if the given event was a valid device list update event
See also
--------
4.2 Discovering peer support
http://conversations.im/xeps/multi-end.html#usecases-discovering
"""
if event.pep_type != 'headline':
return False
devices_list = list(set(unpack_device_list_update(event.stanza,
event.conn.name)))
if len(devices_list) == 0:
return False
account = event.conn.name
contact_jid = gajim.get_jid_without_resource(event.fjid)
state = self.get_omemo_state(account)
my_jid = gajim.get_jid_from_account(account)
if contact_jid == my_jid:
log.info(account + ' => Received own device list:' + str(
devices_list))
state.set_own_devices(devices_list)
state.store.sessionStore.setActiveState(devices_list, my_jid)
# remove contact from list, so on send button pressed
# we query for bundle and build a session
if contact_jid in self.query_for_bundles:
self.query_for_bundles.remove(contact_jid)
if not state.own_device_id_published():
# Our own device_id is not in the list, it could be
# overwritten by some other client
self.publish_own_devices_list(account)
else:
log.info(account + ' => Received device list for ' +
contact_jid + ':' + str(devices_list))
state.set_devices(contact_jid, devices_list)
state.store.sessionStore.setActiveState(devices_list, contact_jid)
# remove contact from list, so on send button pressed
# we query for bundle and build a session
if contact_jid in self.query_for_bundles:
self.query_for_bundles.remove(contact_jid)
# Enable Encryption on receiving first Device List
if not state.encryption.exist(contact_jid):
if account in self.ui_list and \
contact_jid in self.ui_list[account]:
log.debug(account +
' => Switch encryption ON automatically ...')
self.ui_list[account][contact_jid].activate_omemo()
else:
log.debug(account +
' => Switch encryption ON automatically ...')
self.omemo_enable_for(contact_jid, account)
if account in self.ui_list and \
contact_jid not in self.ui_list[account]:
chat_control = gajim.interface.msg_win_mgr.get_control(
contact_jid, account)
if chat_control:
self.connect_ui(chat_control)
return True
@log_calls('OmemoPlugin')
def publish_own_devices_list(self, account):
""" Check if the passed event is a device list update and store the new
device ids.
Parameters
----------
account : str
the account name
"""
state = self.get_omemo_state(account)
devices_list = state.own_devices
devices_list.append(state.own_device_id)
devices_list = list(set(devices_list))
state.set_own_devices(devices_list)
log.debug(account + ' => Publishing own Devices: ' + str(
devices_list))
iq = DeviceListAnnouncement(devices_list)
gajim.connections[account].connection.send(iq)
id_ = str(iq.getAttr('id'))
IQ_CALLBACK[id_] = lambda event: log.debug(event)
@log_calls('OmemoPlugin')
def connect_ui(self, chat_control):
""" Method called from Gajim when a Chat Window is opened
Parameters
----------
chat_control : ChatControl
Gajim ChatControl object
"""
account = chat_control.contact.account.name
contact_jid = chat_control.contact.jid
if account not in self.ui_list:
self.ui_list[account] = {}
state = self.get_omemo_state(account)
my_jid = gajim.get_jid_from_account(account)
omemo_enabled = state.encryption.is_active(contact_jid)
if omemo_enabled:
log.debug(account + " => Adding OMEMO ui for " + contact_jid)
self.ui_list[account][contact_jid] = Ui(self, chat_control,
omemo_enabled, state)
self.ui_list[account][contact_jid].new_fingerprints_available()
return
if contact_jid in state.device_ids or contact_jid == my_jid:
log.debug(account + " => Adding OMEMO ui for " + contact_jid)
self.ui_list[account][contact_jid] = Ui(self, chat_control,
omemo_enabled, state)
self.ui_list[account][contact_jid].new_fingerprints_available()
else:
log.warning(account + " => No devices for " + contact_jid)
@log_calls('OmemoPlugin')
def disconnect_ui(self, chat_control):
""" Calls the removeUi method to remove all relatad UI objects.
Parameters
----------
chat_control : ChatControl
Gajim ChatControl object
"""
contact_jid = chat_control.contact.jid
account = chat_control.contact.account.name
self.ui_list[account][contact_jid].removeUi()
def are_keys_missing(self, account, contact_jid):
""" Checks if devicekeys are missing and querys the
bundles
Parameters
----------
account : str
the account name
contact_jid : str
bare jid of the contact
Returns
-------
bool
Returns True if there are no trusted Fingerprints
"""
state = self.get_omemo_state(account)
my_jid = gajim.get_jid_from_account(account)
# Fetch Bundles of own other Devices
if my_jid not in self.query_for_bundles:
devices_without_session = state \
.devices_without_sessions(my_jid)
self.query_for_bundles.append(my_jid)
if devices_without_session:
for device_id in devices_without_session:
self.fetch_device_bundle_information(account, my_jid,
device_id)
# Fetch Bundles of contacts devices
if contact_jid not in self.query_for_bundles:
devices_without_session = state \
.devices_without_sessions(contact_jid)
self.query_for_bundles.append(contact_jid)
if devices_without_session:
for device_id in devices_without_session:
self.fetch_device_bundle_information(account, contact_jid,
device_id)
if state.getTrustedFingerprints(contact_jid):
return False
else:
return True
@staticmethod
def handle_iq_received(event):
""" Method called when an IQ is received
Parameters
----------
event : RawIqReceived
"""
id_ = str(event.stanza.getAttr("id"))
if id_ in IQ_CALLBACK:
try:
IQ_CALLBACK[id_](event.stanza)
except:
raise
finally:
del IQ_CALLBACK[id_]
@log_calls('OmemoPlugin')
def fetch_device_bundle_information(self, account, jid, device_id):
""" Fetch bundle information for specified jid, key, and create axolotl
session on success.
Parameters
----------
account : str
The account name
jid : str
The jid to query for bundle information
device_id : int
The device_id for which we are missing an axolotl session
"""
log.info(account + ' => Fetch bundle device ' + str(device_id) +
'#' + jid)
iq = BundleInformationQuery(jid, device_id)
iq_id = str(iq.getAttr('id'))
IQ_CALLBACK[iq_id] = \
lambda stanza: self.session_from_prekey_bundle(account,
stanza, jid,
device_id)
gajim.connections[account].connection.send(iq)
@log_calls('OmemoPlugin')
def session_from_prekey_bundle(self, account, stanza,
recipient_id, device_id):
""" Starts a session from a PreKey bundle.
This method tries to build an axolotl session when a PreKey bundle
is fetched.
If a session can not be build it will fail silently but log the a
warning.
See also
--------
4.4 Building a session:
http://conversations.im/xeps/multi-end.html#usecases-building
Parameters:
-----------
account : str
The account name
stanza
The stanza object received from callback
recipient_id : str
The recipient jid
device_id : int
The device_id for which the bundle was queried
"""
state = self.get_omemo_state(account)
bundle_dict = unpack_device_bundle(stanza, device_id)
if not bundle_dict:
log.warning('Failed to build Session with ' + recipient_id)
return
if state.build_session(recipient_id, device_id, bundle_dict):
log.info(account + ' => session created for: ' + recipient_id)
# Trigger dialog to trust new Fingerprints if
# the Chat Window is Open
if account in self.ui_list and \
recipient_id in self.ui_list[account]:
self.ui_list[account][recipient_id]. \
new_fingerprints_available()
@log_calls('OmemoPlugin')
def query_own_devicelist(self, account):
""" Query own devicelist from the server.
Parameters
----------
account : str
the account name
"""
my_jid = gajim.get_jid_from_account(account)
iq = DevicelistQuery(my_jid)
gajim.connections[account].connection.send(iq)
log.info(account + ' => Querry own devicelist ...')
id_ = str(iq.getAttr("id"))
IQ_CALLBACK[id_] = lambda stanza: \
self.handle_devicelist_result(account, stanza)
@log_calls('OmemoPlugin')
def publish_bundle(self, account):
""" Publish our bundle information to the PEP node.
Parameters
----------
account : str
the account name
See also
--------
4.3 Announcing bundle information:
http://conversations.im/xeps/multi-end.html#usecases-announcing
"""
state = self.get_omemo_state(account)
iq = BundleInformationAnnouncement(state.bundle, state.own_device_id)
gajim.connections[account].connection.send(iq)
id_ = str(iq.getAttr("id"))
log.info(account + " => Publishing bundle ...")
IQ_CALLBACK[id_] = lambda stanza: \
self.handle_publish_result(account, stanza)
@staticmethod
def handle_publish_result(account, stanza):
""" Log if publishing our bundle was successful
Parameters
----------
account : str
the account name
stanza
The stanza object received from callback
"""
if successful(stanza):
log.info(account + ' => Publishing bundle was successful')
else:
log.error(account + ' => Publishing bundle was NOT successful')
@log_calls('OmemoPlugin')
def handle_devicelist_result(self, account, stanza):
""" If query was successful add own device to the list.
Parameters
----------
account : str
the account name
stanza
The stanza object received from callback
"""
my_jid = gajim.get_jid_from_account(account)
state = self.get_omemo_state(account)
if successful(stanza):
log.info(account + ' => Devicelistquery was successful')
devices_list = list(set(unpack_device_list_update(stanza, account)))
if len(devices_list) == 0:
return False
contact_jid = stanza.getAttr('from')
if contact_jid == my_jid:
state.set_own_devices(devices_list)
state.store.sessionStore.setActiveState(devices_list, my_jid)
# remove contact from list, so on send button pressed
# we query for bundle and build a session
if contact_jid in self.query_for_bundles:
self.query_for_bundles.remove(contact_jid)
if not state.own_device_id_published():
# Our own device_id is not in the list, it could be
# overwritten by some other client
self.publish_own_devices_list(account)
else:
log.error(account + ' => Devicelistquery was NOT successful')
self.publish_own_devices_list(account)
@log_calls('OmemoPlugin')
def clear_device_list(self, account):
""" Clears the local devicelist of our own devices and publishes
a new one including only the current ID of this device
Parameters
----------
account : str
the account name
"""
connection = gajim.connections[account].connection
if not connection:
return
state = self.get_omemo_state(account)
devices_list = [state.own_device_id]
state.set_own_devices(devices_list)
log.info(account + ' => Clearing devices_list ' + str(devices_list))
iq = DeviceListAnnouncement(devices_list)
connection.send(iq)
id_ = str(iq.getAttr('id'))
IQ_CALLBACK[id_] = lambda event: log.info(event)
@staticmethod
def print_msg_to_log(stanza):
""" Prints a stanza in a fancy way to the log """
log.debug('-'*15)
stanzastr = '\n' + stanza.__str__(fancy=True)
stanzastr = stanzastr[0:-1]
log.debug(stanzastr)
log.debug('-'*15)
@log_calls('OmemoPlugin')
def omemo_enable_for(self, jid, account):
""" Used by the UI to enable OMEMO for a specified contact.
To activate OMEMO check first if a Ui Object exists for the
Contact. If it exists use Ui.activate_omemo(). Only if there
is no Ui Object for the contact this method is to be used.
Parameters
----------
jid : str
bare jid
account : str
the account name
"""
state = self.get_omemo_state(account)
state.encryption.activate(jid)
@log_calls('OmemoPlugin')
def omemo_disable_for(self, jid, account):
""" Used by the UI to disable OMEMO for a specified contact.
WARNING - OMEMO should only be disabled through
User interaction with the UI.
Parameters
----------
jid : str
bare jid
account : str
the account name
"""
state = self.get_omemo_state(account)
state.encryption.deactivate(jid)
from .omemoplugin import OmemoPlugin

File diff suppressed because it is too large Load Diff

View File

@@ -1,298 +1,299 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="2.16"/>
<!-- interface-naming-policy toplevel-contextual -->
<object class="GtkListStore" id="account_store">
<columns>
<!-- column-name accountname -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="fingerprint_store">
<columns>
<!-- column-name id -->
<column type="gint"/>
<!-- column-name screenname -->
<column type="gchararray"/>
<!-- column-name trust -->
<column type="gchararray"/>
<!-- column-name fingerprint -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkNotebook" id="notebook1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<signal name="switch-page" handler="update_context_list" after="yes" swapped="no"/>
<child>
<object class="GtkVBox" id="vbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="spacing">10</property>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="height_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<object class="GtkTreeView" id="fingerprint_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">fingerprint_store</property>
<property name="search_column">0</property>
<property name="tooltip_column">3</property>
<signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/>
<child>
<object class="GtkTreeViewColumn" id="name_column">
<property name="resizable">True</property>
<property name="title">Name</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext2"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="trust_column">
<property name="resizable">True</property>
<property name="title">Trust</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertoggle1"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fingerprint_column">
<property name="resizable">True</property>
<property name="title">Fingerprint</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext4"/>
<attributes>
<attribute name="markup">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="trust_button">
<property name="label" translatable="yes" comments="button">Trust/Revoke Fingerprint</property>
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="trust_button_clicked_cb" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="tab label">Contact</property>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="spacing">10</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel" id="fingerprint_label_desc1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="Descriptive label">Own Fingerprint:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="fingerprint_label_own">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">&lt;tt&gt;-------- -------- -------- -------- -------- &lt;/tt&gt;</property>
<property name="use_markup">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow2">
<property name="height_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<object class="GtkTreeView" id="fingerprint_view_own">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="model">fingerprint_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<property name="tooltip_column">3</property>
<signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/>
<child>
<object class="GtkTreeViewColumn" id="name_column1">
<property name="resizable">True</property>
<property name="title">Name</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="trust_column1">
<property name="resizable">True</property>
<property name="title">Trust</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertoggle2"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fingerprint_column1">
<property name="resizable">True</property>
<property name="title">Fingerprint</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext3"/>
<attributes>
<attribute name="markup">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="trust_button1">
<property name="label" translatable="yes" comments="button">Trust/Revoke Fingerprint</property>
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="trust_button_clicked_cb" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Own Devices</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<object class="GtkMenu" id="fprclipboard_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="copyfprclipboard_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="Context menu item">Copy to clipboard</property>
<property name="use_underline">True</property>
<signal name="activate" handler="clipboard_button_cb" swapped="no"/>
</object>
</child>
</object>
</interface>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<object class="GtkListStore" id="fingerprint_store">
<columns>
<!-- column-name id -->
<column type="gint"/>
<!-- column-name screenname -->
<column type="gchararray"/>
<!-- column-name trust -->
<column type="gchararray"/>
<!-- column-name fingerprint -->
<column type="gchararray"/>
<!-- column-name deviceid -->
<column type="gint"/>
</columns>
</object>
<object class="GtkNotebook" id="notebook1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<signal name="switch-page" handler="update_context_list" after="yes" swapped="no"/>
<child>
<object class="GtkVBox" id="vbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="spacing">10</property>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="height_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkTreeView" id="fingerprint_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="model">fingerprint_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<property name="tooltip_column">3</property>
<signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection1"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="name_column">
<property name="resizable">True</property>
<property name="title">Name</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="NameCell"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="trust_column">
<property name="resizable">True</property>
<property name="title">Trust</property>
<child>
<object class="GtkCellRendererText" id="TrustCell"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fingerprint_column">
<property name="resizable">True</property>
<property name="title">Fingerprint</property>
<child>
<object class="GtkCellRendererText" id="FingerprintCell"/>
<attributes>
<attribute name="markup">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="trust_button">
<property name="label" translatable="yes" comments="button">Trust/Revoke Fingerprint</property>
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="trust_button_clicked_cb" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="tab label">Contact</property>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="spacing">10</property>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkLabel" id="fingerprint_label_desc1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes" comments="Descriptive label">Own Fingerprint:</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="fingerprint_label_own">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">&lt;tt&gt;-------- -------- -------- -------- -------- &lt;/tt&gt;</property>
<property name="use_markup">True</property>
<property name="selectable">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow2">
<property name="height_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkTreeView" id="fingerprint_view_own">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="model">fingerprint_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<property name="tooltip_column">3</property>
<signal name="button-press-event" handler="fpr_button_pressed_cb" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection2"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="name_column1">
<property name="resizable">True</property>
<property name="title">Name</property>
<child>
<object class="GtkCellRendererText" id="NameCell1"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="trust_column1">
<property name="resizable">True</property>
<property name="title">Trust</property>
<child>
<object class="GtkCellRendererText" id="TrustCell2"/>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="fingerprint_column1">
<property name="resizable">True</property>
<property name="title">Fingerprint</property>
<child>
<object class="GtkCellRendererText" id="FingerprintCell1"/>
<attributes>
<attribute name="markup">3</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkHBox" id="hbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="trust_button1">
<property name="label" translatable="yes" comments="button">Trust/Revoke Fingerprint</property>
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="trust_button_clicked_cb" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Own Devices</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<object class="GtkMenu" id="fprclipboard_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkMenuItem" id="copyfprclipboard_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="activate" handler="clipboard_button_cb" swapped="no"/>
</object>
</child>
</object>
</interface>

View File

@@ -1,11 +1,11 @@
[info]
name: OMEMO
short_name: omemo
version: 0.9.0
version: 1.0.1
description: OMEMO is an XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption based on Axolotl and PEP. You need to install some dependencys, you can find install instructions for your system in the Github Wiki.
authors: Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
Daniel Gultsch <daniel@gultsch.de>
Philipp Hörist <philipp@hoerist.com>
homepage: http://github.com/omemo/gajim-omemo.git
homepage: https://dev.gajim.org/gajim/gajim-plugins/wikis/OmemoGajimPlugin
min_gajim_version: 0.16.9
max_gajim_version: 0.16.11

View File

@@ -18,6 +18,7 @@
#
import sys
import logging
log = logging.getLogger('gajim.plugin_system.omemo')
try:
@@ -35,7 +36,11 @@ def encrypt(key, iv, plaintext):
def decrypt(key, iv, ciphertext):
return aes_decrypt(key, iv, ciphertext)
plaintext = aes_decrypt(key, iv, ciphertext).decode('utf-8')
if sys.version_info < (3, 0):
return unicode(plaintext)
else:
return plaintext
class NoValidSessions(Exception):

View File

@@ -29,11 +29,14 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
from struct import pack, unpack
from Crypto.Cipher import AES
from Crypto.Util import strxor
log = logging.getLogger('gajim.plugin_system.omemo')
def gcm_rightshift(vec):
for x in range(15, 0, -1):
@@ -140,13 +143,20 @@ def gcm_encrypt(k, iv, plaintext, auth_data):
def aes_encrypt(key, nonce, plaintext):
""" Use AES128 GCM with the given key and iv to encrypt the payload. """
c, t = gcm_encrypt(key, nonce, plaintext, '')
result = c + t
return result
return gcm_encrypt(key, nonce, plaintext, '')
def aes_decrypt(key, nonce, payload):
def aes_decrypt(_key, nonce, payload):
""" Use AES128 GCM with the given key and iv to decrypt the payload. """
ciphertext = payload[:-16]
mac = payload[-16:]
if len(_key) >= 32:
# XEP-0384
log.debug('XEP Compliant Key/Tag')
ciphertext = payload
key = _key[:16]
mac = _key[16:]
else:
# Legacy
log.debug('Legacy Key/Tag')
ciphertext = payload[:-16]
key = _key
mac = payload[-16:]
return gcm_decrypt(key, nonce, ciphertext, '', mac)

View File

@@ -19,6 +19,7 @@
import os
import logging
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers.modes import GCM
@@ -32,11 +33,22 @@ if os.name == 'nt':
else:
from cryptography.hazmat.backends import default_backend
log = logging.getLogger('gajim.plugin_system.omemo')
def aes_decrypt(key, iv, payload):
def aes_decrypt(_key, iv, payload):
""" Use AES128 GCM with the given key and iv to decrypt the payload. """
data = payload[:-16]
tag = payload[-16:]
if len(_key) >= 32:
# XEP-0384
log.debug('XEP Compliant Key/Tag')
data = payload
key = _key[:16]
tag = _key[16:]
else:
# Legacy
log.debug('Legacy Key/Tag')
data = payload[:-16]
key = _key
tag = payload[-16:]
if os.name == 'nt':
_backend = backend
else:
@@ -58,4 +70,4 @@ def aes_encrypt(key, iv, plaintext):
algorithms.AES(key),
GCM(iv),
backend=_backend).encryptor()
return encryptor.update(plaintext) + encryptor.finalize() + encryptor.tag
return encryptor.update(plaintext) + encryptor.finalize(), encryptor.tag

View File

@@ -83,10 +83,16 @@ class LiteAxolotlStore(AxolotlStore):
def saveIdentity(self, recepientId, identityKey):
self.identityKeyStore.saveIdentity(recepientId, identityKey)
def deleteIdentity(self, recipientId, identityKey):
self.identityKeyStore.deleteIdentity(recipientId, identityKey)
def isTrustedIdentity(self, recepientId, identityKey):
return self.identityKeyStore.isTrustedIdentity(recepientId,
identityKey)
def setTrust(self, identityKey, trust):
return self.identityKeyStore.setTrust(identityKey, trust)
def getTrustedFingerprints(self, jid):
return self.identityKeyStore.getTrustedFingerprints(jid)
@@ -127,6 +133,9 @@ class LiteAxolotlStore(AxolotlStore):
# TODO Reuse this
return self.sessionStore.getSubDeviceSessions(recepientId)
def getJidFromDevice(self, device_id):
return self.sessionStore.getJidFromDevice(device_id)
def storeSession(self, recepientId, deviceId, sessionRecord):
self.sessionStore.storeSession(recepientId, deviceId, sessionRecord)
@@ -139,6 +148,15 @@ class LiteAxolotlStore(AxolotlStore):
def deleteAllSessions(self, recepientId):
self.sessionStore.deleteAllSessions(recepientId)
def getSessionsFromJid(self, recipientId):
return self.sessionStore.getSessionsFromJid(recipientId)
def getSessionsFromJids(self, recipientId):
return self.sessionStore.getSessionsFromJids(recipientId)
def getAllSessions(self):
return self.sessionStore.getAllSessions()
def loadSignedPreKey(self, signedPreKeyId):
return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId)

View File

@@ -86,6 +86,13 @@ class LiteIdentityKeyStore(IdentityKeyStore):
return result is not None
def deleteIdentity(self, recipientId, identityKey):
q = "DELETE FROM identities WHERE recipient_id = ? AND public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (recipientId,
identityKey.getPublicKey().serialize()))
self.dbConn.commit()
def isTrustedIdentity(self, recipientId, identityKey):
q = "SELECT trust FROM identities WHERE recipient_id = ? " \
"AND public_key = ?"
@@ -160,8 +167,8 @@ class LiteIdentityKeyStore(IdentityKeyStore):
c.execute(q, fingerprints)
self.dbConn.commit()
def setTrust(self, _id, trust):
q = "UPDATE identities SET trust = ? WHERE _id = ?"
def setTrust(self, identityKey, trust):
q = "UPDATE identities SET trust = ? WHERE public_key = ?"
c = self.dbConn.cursor()
c.execute(q, (trust, _id))
c.execute(q, (trust, identityKey.getPublicKey().serialize()))
self.dbConn.commit()

View File

@@ -48,6 +48,14 @@ class LiteSessionStore(SessionStore):
deviceIds = [r[0] for r in result]
return deviceIds
def getJidFromDevice(self, device_id):
q = "SELECT recipient_id from sessions WHERE device_id = ?"
c = self.dbConn.cursor()
c.execute(q, (device_id, ))
result = c.fetchone()
return result[0].decode('utf-8')
def getActiveDeviceTuples(self):
q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1"
c = self.dbConn.cursor()
@@ -82,6 +90,33 @@ class LiteSessionStore(SessionStore):
self.dbConn.cursor().execute(q, (recipientId, ))
self.dbConn.commit()
def getAllSessions(self):
q = "SELECT _id, recipient_id, device_id, record, active from sessions"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def getSessionsFromJid(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id = ?"
c = self.dbConn.cursor()
result = []
for row in c.execute(q, (recipientId,)):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def getSessionsFromJids(self, recipientId):
q = "SELECT _id, recipient_id, device_id, record, active from sessions" \
" WHERE recipient_id IN ({})" \
.format(', '.join(['?'] * len(recipientId)))
c = self.dbConn.cursor()
result = []
for row in c.execute(q, recipientId):
result.append((row[0], row[1].decode('utf-8'), row[2], row[3], row[4]))
return result
def setActiveState(self, deviceList, jid):
c = self.dbConn.cursor()
@@ -96,28 +131,6 @@ class LiteSessionStore(SessionStore):
c.execute(q, deviceList)
self.dbConn.commit()
def getActiveSessionsKeys(self, recipientId):
q = "SELECT record FROM sessions WHERE active = 1 AND recipient_id = ?"
c = self.dbConn.cursor()
result = []
for row in c.execute(q, (recipientId,)):
public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey().
getPublicKey())
result.append(public_key.serialize())
return result
def getAllActiveSessionsKeys(self):
q = "SELECT record FROM sessions WHERE active = 1"
c = self.dbConn.cursor()
result = []
for row in c.execute(q):
public_key = (SessionRecord(serialized=row[0]).
getSessionState().getRemoteIdentityKey().
getPublicKey())
result.append(public_key.serialize())
return result
def getInactiveSessionsKeys(self, recipientId):
q = "SELECT record FROM sessions WHERE active = 0 AND recipient_id = ?"
c = self.dbConn.cursor()

View File

@@ -29,6 +29,14 @@ class SQLDatabase():
self.dbConn = dbConn
self.createDb()
self.migrateDb()
c = self.dbConn.cursor()
c.execute("PRAGMA synchronous=NORMAL;")
c.execute("PRAGMA journal_mode;")
mode = c.fetchone()[0]
# WAL is a persistent DB mode, dont override it if user has set it
if mode != 'wal':
c.execute("PRAGMA journal_mode=MEMORY;")
self.dbConn.commit()
def createDb(self):
if user_version(self.dbConn) == 0:

View File

@@ -200,8 +200,8 @@ class OmemoState:
key = self.handleWhisperMessage(sender_jid, sid, encrypted_key)
except (NoSessionException, InvalidMessageException) as e:
log.warning('No Session found ' + e.message)
log.warning('sender_jid => ' + str(sender_jid) +
' sid =>' + sid)
log.warning('sender_jid => ' + str(sender_jid) + ' sid =>' +
str(sid))
return
except (DuplicateMessageException) as e:
log.warning('Duplicate message found ' + str(e.args))
@@ -211,7 +211,7 @@ class OmemoState:
log.warning('Duplicate message found ' + str(e.args))
return
result = decrypt(key, iv, payload).decode('utf-8')
result = decrypt(key, iv, payload)
log.debug("Decrypted Message => " + result)
return result
@@ -226,43 +226,44 @@ class OmemoState:
log.error('No known devices')
return
for dev in devices_list:
self.get_session_cipher(jid, dev)
session_ciphers = self.session_ciphers[jid]
if not session_ciphers:
log.warning('No session ciphers for ' + jid)
return
payload, tag = encrypt(key, iv, plaintext)
# for XEP-384 Compliance uncomment
# key += tag
payload += tag
# Encrypt the message key with for each of receivers devices
for rid, cipher in session_ciphers.items():
for device in devices_list:
try:
if self.isTrusted(cipher) == TRUSTED:
encrypted_keys[rid] = cipher.encrypt(key).serialize()
if self.isTrusted(jid, device) == TRUSTED:
cipher = self.get_session_cipher(jid, device)
cipher_key = cipher.encrypt(key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(cipher)))
str(self.isTrusted(jid, device)))
except:
log.warning('Failed to find key for device ' + str(rid))
log.warning('Failed to find key for device ' + str(device))
if len(encrypted_keys) == 0:
log_msg = 'Encrypted keys empty'
log.error(log_msg)
raise NoValidSessions(log_msg)
log.error('Encrypted keys empty')
raise NoValidSessions('Encrypted keys empty')
my_other_devices = set(self.own_devices) - set({self.own_device_id})
# Encrypt the message key with for each of our own devices
for dev in my_other_devices:
for device in my_other_devices:
try:
cipher = self.get_session_cipher(from_jid, dev)
if self.isTrusted(cipher) == TRUSTED:
encrypted_keys[dev] = cipher.encrypt(key).serialize()
if self.isTrusted(from_jid, device) == TRUSTED:
cipher = self.get_session_cipher(from_jid, device)
cipher_key = cipher.encrypt(key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[device] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped own Device because Trust is: ' +
str(self.isTrusted(cipher)))
str(self.isTrusted(from_jid, device)))
except:
log.warning('Failed to find key for device ' + str(dev))
payload = encrypt(key, iv, plaintext)
log.warning('Failed to find key for device ' + str(device))
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
@@ -273,14 +274,109 @@ class OmemoState:
log.debug('Finished encrypting message')
return result
def isTrusted(self, cipher):
self.cipher = cipher
self.state = self.cipher.sessionStore. \
loadSession(self.cipher.recipientId, self.cipher.deviceId). \
getSessionState()
self.key = self.state.getRemoteIdentityKey()
return self.store.identityKeyStore. \
isTrustedIdentity(self.cipher.recipientId, self.key)
def create_gc_msg(self, from_jid, jid, plaintext):
key = get_random_bytes(16)
iv = get_random_bytes(16)
encrypted_keys = {}
room = jid
encrypted_jids = []
devices_list = self.device_list_for(jid, True)
if len(devices_list) == 0:
log.error('No known devices')
return
payload, tag = encrypt(key, iv, plaintext)
# for XEP-384 Compliance uncomment
# key += tag
payload += tag
for tup in devices_list:
self.get_session_cipher(tup[0], tup[1])
# Encrypt the message key with for each of receivers devices
for nick in self.plugin.groupchat[room]:
jid_to = self.plugin.groupchat[room][nick]
if jid_to == self.own_jid:
continue
if jid_to in encrypted_jids: # We already encrypted to this JID
continue
for rid, cipher in self.session_ciphers[jid_to].items():
try:
if self.isTrusted(jid_to, rid) == TRUSTED:
cipher_key = cipher.encrypt(key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[rid] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped Device because Trust is: ' +
str(self.isTrusted(jid_to, rid)))
except:
log.exception('ERROR:')
log.warning('Failed to find key for device ' +
str(rid))
encrypted_jids.append(jid_to)
if len(encrypted_keys) == 0:
log_msg = 'Encrypted keys empty'
log.error(log_msg)
raise NoValidSessions(log_msg)
my_other_devices = set(self.own_devices) - set({self.own_device_id})
# Encrypt the message key with for each of our own devices
for dev in my_other_devices:
try:
cipher = self.get_session_cipher(from_jid, dev)
if self.isTrusted(from_jid, dev) == TRUSTED:
cipher_key = cipher.encrypt(key)
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
encrypted_keys[dev] = (cipher_key.serialize(), prekey)
else:
log.debug('Skipped own Device because Trust is: ' +
str(self.isTrusted(from_jid, dev)))
except:
log.exception('ERROR:')
log.warning('Failed to find key for device ' + str(dev))
result = {'sid': self.own_device_id,
'keys': encrypted_keys,
'jid': jid,
'iv': iv,
'payload': payload}
log.debug('Finished encrypting message')
return result
def device_list_for(self, jid, gc=False):
""" Return a list of known device ids for the specified jid.
Parameters
----------
jid : string
The contacts jid
gc : bool
Groupchat Message
"""
if gc:
room = jid
devicelist = []
for nick in self.plugin.groupchat[room]:
jid_to = self.plugin.groupchat[room][nick]
if jid_to == self.own_jid:
continue
for device in self.device_ids[jid_to]:
devicelist.append((jid_to, device))
return devicelist
if jid == self.own_jid:
return set(self.own_devices) - set({self.own_device_id})
if jid not in self.device_ids:
return set()
return set(self.device_ids[jid])
def isTrusted(self, recipient_id, device_id):
record = self.store.loadSession(recipient_id, device_id)
identity_key = record.getSessionState().getRemoteIdentityKey()
return self.store.isTrustedIdentity(recipient_id, identity_key)
def getTrustedFingerprints(self, recipient_id):
inactive = self.store.getInactiveSessionsKeys(recipient_id)
@@ -296,20 +392,6 @@ class OmemoState:
return undecided
def device_list_for(self, jid):
""" Return a list of known device ids for the specified jid.
Parameters
----------
jid : string
The contacts jid
"""
if jid == self.own_jid:
return set(self.own_devices) - set({self.own_device_id})
if jid not in self.device_ids:
return set()
return set(self.device_ids[jid])
def devices_without_sessions(self, jid):
""" List device_ids for the given jid which have no axolotl session.
@@ -364,10 +446,10 @@ class OmemoState:
def handleWhisperMessage(self, recipient_id, device_id, key):
whisperMessage = WhisperMessage(serialized=key)
sessionCipher = self.get_session_cipher(recipient_id, device_id)
log.debug(self.account + " => Received WhisperMessage from " +
recipient_id)
if self.isTrusted(sessionCipher) >= TRUSTED:
if self.isTrusted(recipient_id, device_id):
sessionCipher = self.get_session_cipher(recipient_id, device_id)
key = sessionCipher.decryptMsg(whisperMessage, textMsg=False)
return key
else:

1135
omemo/omemoplugin.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +0,0 @@
# Maintainer: Tommaso Sardelli <lacapannadelloziotom AT gmail DOT com>
pkgname=gajim-plugin-omemo
_pkgname=gajim-omemo
pkgver=0.8.1
pkgrel=2
pkgdesc="Gajim plugin for OMEMO Multi-End Message and Object Encryption."
arch=(any)
url="https://github.com/omemo/${_pkgname}"
license=('GPL')
depends=("gajim" "python2-setuptools" "python2-cryptography" "python2-axolotl-git")
provides=('gajim-plugin-omemo')
conflicts=('gajim-plugin-omemo-git')
source=("https://github.com/omemo/${_pkgname}/archive/${pkgver}.tar.gz")
sha512sums=('e9280033fbe111f5010f2e9e8fa32c5b8c0abe308000f9a043a1c5e8215c96f8be434876b1d72cc8d68aed4ddaebe9655c70f9648a2db718cba71d90434fee2e')
package() {
cd $srcdir/gajim-omemo-${pkgver}
rm -r CHANGELOG COPYING doc pkgs README.md
install -d ${pkgdir}/usr/share/gajim/plugins/omemo
cp -r * ${pkgdir}/usr/share/gajim/plugins/omemo/
}
# vim:set ts=2 sw=2 et:

View File

@@ -1,2 +0,0 @@
[flake8]
max-line-length=80

View File

@@ -20,18 +20,30 @@
import binascii
import logging
import os
import message_control
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import GObject, Gtk, GdkPixbuf
# pylint: disable=import-error
import gtkgui_helpers
from common import gajim
from dialogs import YesNoDialog
from plugins.gui import GajimPluginConfigDialog
# pylint: enable=import-error
from axolotl.state.sessionrecord import SessionRecord
from common import configpaths
log = logging.getLogger('gajim.plugin_system.omemo')
PILLOW = False
try:
import qrcode
PILLOW = True
except Exception as e:
log.exception('Error:')
log.error('python-qrcode or dependencies of it, are not available')
# pylint: enable=import-error
UNDECIDED = 2
TRUSTED = 1
UNTRUSTED = 0
@@ -106,6 +118,12 @@ class Ui(object):
self.account = self.contact.account.name
self.windowinstances = {}
self.groupchat = False
if chat_control.type_id == message_control.TYPE_GC:
self.groupchat = True
self.omemo_capable = False
self.room = self.chat_control.room_jid
self.display_omemo_state()
self.refresh_auth_lock_icon()
@@ -133,6 +151,9 @@ class Ui(object):
item.set_image(Gtk.Image.new_from_file(icon_path))
item.set_submenu(submenu)
if self.groupchat:
item.set_sensitive(self.omemo_capable)
# at index 8 is the separator after the esession encryption entry
menu.insert(item, 8)
return menu
@@ -159,7 +180,38 @@ class Ui(object):
log.debug(self.account + ' => Sending Message to ' +
self.contact.jid)
self.chat_control.send_message = omemo_send_message
def omemo_send_gc_message(message, xhtml=None, process_commands=True):
self.new_fingerprints_available()
if self.encryption_active():
missing = True
own_jid = gajim.get_jid_from_account(self.account)
for nick in self.plugin.groupchat[self.room]:
real_jid = self.plugin.groupchat[self.room][nick]
if real_jid == own_jid:
continue
if not self.plugin.are_keys_missing(self.account,
real_jid):
missing = False
if missing:
log.debug(self.account +
' => No Trusted Fingerprints for ' +
self.room)
self.no_trusted_fingerprints_warning()
else:
self.chat_control.orig_send_message(message, xhtml,
process_commands)
log.debug(self.account + ' => Sending Message to ' +
self.room)
else:
self.chat_control.orig_send_message(message, xhtml,
process_commands)
log.debug(self.account + ' => Sending Message to ' +
self.room)
if self.groupchat:
self.chat_control.send_message = omemo_send_gc_message
else:
self.chat_control.send_message = omemo_send_message
def set_omemo_state(self, enabled):
"""
@@ -182,6 +234,12 @@ class Ui(object):
self.omemobutton.set_omemo_state(enabled)
self.display_omemo_state()
def sensitive(self, value):
self.omemobutton.set_sensitive(value)
self.omemo_capable = value
if value:
self.chat_control.prepare_context_menu
def encryption_active(self):
return self.state.encryption.is_active(self.contact.jid)
@@ -190,16 +248,31 @@ class Ui(object):
self.set_omemo_state(True)
def new_fingerprints_available(self):
fingerprints = self.state.store.getNewFingerprints(self.contact.jid)
if fingerprints:
self.show_fingerprint_window(fingerprints)
jid = self.contact.jid
if self.groupchat and self.room in self.plugin.groupchat:
for nick in self.plugin.groupchat[self.room]:
real_jid = self.plugin.groupchat[self.room][nick]
fingerprints = self.state.store. \
getNewFingerprints(real_jid)
if fingerprints:
self.show_fingerprint_window(fingerprints)
elif not self.groupchat:
fingerprints = self.state.store.getNewFingerprints(jid)
if fingerprints:
self.show_fingerprint_window(fingerprints)
def show_fingerprint_window(self, fingerprints=None):
if 'dialog' not in self.windowinstances:
self.windowinstances['dialog'] = \
FingerprintWindow(self.plugin, self.contact,
self.chat_control.parent_win.window,
self.windowinstances)
if self.groupchat:
self.windowinstances['dialog'] = \
FingerprintWindow(self.plugin, self.contact,
self.chat_control.parent_win.window,
self.windowinstances, groupchat=True)
else:
self.windowinstances['dialog'] = \
FingerprintWindow(self.plugin, self.contact,
self.chat_control.parent_win.window,
self.windowinstances)
self.windowinstances['dialog'].show_all()
if fingerprints:
log.debug(self.account +
@@ -225,10 +298,12 @@ class Ui(object):
def no_trusted_fingerprints_warning(self):
msg = "To send an encrypted message, you have to " \
"first trust the fingerprint of your contact!"
"first trust the fingerprint of your contact!"
self.chat_control.print_conversation_line(msg, 'status', '', None)
def refresh_auth_lock_icon(self):
if self.groupchat:
return
if self.encryption_active():
if self.state.getUndecidedFingerprints(self.contact.jid):
self.chat_control._show_lock_image(True, 'OMEMO', True, True,
@@ -242,6 +317,8 @@ class Ui(object):
def removeUi(self):
self.actions_hbox.remove(self.omemobutton)
self.chat_control._show_lock_image(False, 'OMEMO', False, True,
False)
self.chat_control.prepare_context_menu = \
self.chat_control.omemo_orig_prepare_context_menu
self.chat_control.send_message = self.chat_control.orig_send_message
@@ -256,40 +333,144 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
self.B.set_translation_domain('gajim_plugins')
self.B.add_from_file(self.GTK_BUILDER_FILE_PATH)
self.fpr_model = Gtk.ListStore(GObject.TYPE_INT,
GObject.TYPE_STRING,
GObject.TYPE_STRING,
GObject.TYPE_STRING)
try:
self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS']
except KeyError:
self.plugin.config['DISABLED_ACCOUNTS'] = []
self.disabled_accounts = self.plugin.config['DISABLED_ACCOUNTS']
self.device_model = Gtk.ListStore(GObject.TYPE_INT)
log.debug('Disabled Accounts:')
log.debug(self.disabled_accounts)
self.account_store = self.B.get_object('account_store')
for account in sorted(gajim.contacts.get_accounts()):
self.account_store.append(row=(account,))
self.fpr_model = self.B.get_object('fingerprint_store')
self.device_model = self.B.get_object('deviceid_store')
self.fpr_view = self.B.get_object('fingerprint_view')
self.fpr_view.set_model(self.fpr_model)
self.fpr_view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
self.device_view = self.B.get_object('deviceid_view')
self.device_view.set_model(self.device_model)
self.disabled_acc_store = self.B.get_object('disabled_account_store')
self.account_store = self.B.get_object('account_store')
if len(self.account_store) > 0:
self.B.get_object('account_combobox').set_active(0)
self.active_acc_view = self.B.get_object('active_accounts_view')
self.disabled_acc_view = self.B.get_object('disabled_accounts_view')
vbox = self.get_content_area()
vbox.pack_start(self.B.get_object('notebook1'), True, True, 0)
self.B.connect_signals(self)
self.plugin_active = False
def on_run(self):
self.update_context_list()
self.account_combobox_changed_cb(self.B.get_object('account_combobox'))
for plugin in gajim.plugin_manager.active_plugins:
log.debug(type(plugin))
if type(plugin).__name__ == 'OmemoPlugin':
self.plugin_active = True
break
self.update_account_store()
self.update_account_combobox()
self.update_disabled_account_view()
def is_in_accountstore(self, account):
for row in self.account_store:
if row[0] == account:
return True
return False
def update_account_store(self):
for account in sorted(gajim.contacts.get_accounts()):
if account not in self.disabled_accounts and \
not self.is_in_accountstore(account):
self.account_store.append(row=(account,))
def update_account_combobox(self):
if self.plugin_active is False:
return
if len(self.account_store) > 0:
self.B.get_object('account_combobox').set_active(0)
else:
self.account_combobox_changed_cb(
self.B.get_object('account_combobox'))
def account_combobox_changed_cb(self, box, *args):
self.update_context_list()
def get_qrcode(self, jid, sid, fingerprint):
file_name = 'omemo_{}.png'.format(jid)
path = os.path.join(
configpaths.gajimpaths['MY_DATA'], file_name)
ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint)
log.debug('Verification String: ' + ver_string)
if os.path.exists(path):
return path
qr = qrcode.QRCode(version=None, error_correction=2, box_size=4, border=1)
qr.add_data(ver_string)
qr.make(fit=True)
img = qr.make_image()
img.save(path)
return path
def update_disabled_account_view(self):
self.disabled_acc_store.clear()
for account in self.disabled_accounts:
self.disabled_acc_store.append(row=(account,))
def activate_accounts_btn_clicked(self, button, *args):
mod, paths = self.disabled_acc_view.get_selection().get_selected_rows()
for path in paths:
it = mod.get_iter(path)
account = mod.get(it, 0)
if account[0] in self.disabled_accounts and \
not self.is_in_accountstore(account[0]):
self.account_store.append(row=(account[0],))
self.disabled_accounts.remove(account[0])
self.update_disabled_account_view()
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
self.update_account_combobox()
def disable_accounts_btn_clicked(self, button, *args):
mod, paths = self.active_acc_view.get_selection().get_selected_rows()
for path in paths:
it = mod.get_iter(path)
account = mod.get(it, 0)
if account[0] not in self.disabled_accounts and \
self.is_in_accountstore(account[0]):
self.disabled_accounts.append(account[0])
self.account_store.remove(it)
self.update_disabled_account_view()
self.plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
self.update_account_combobox()
def delfpr_button_clicked(self, button, *args):
active = self.B.get_object('account_combobox').get_active()
account = self.account_store[active][0]
state = self.plugin.get_omemo_state(account)
mod, paths = self.fpr_view.get_selection().get_selected_rows()
def on_yes(checked):
record = state.store.loadSession(jid, deviceid)
identity_key = record.getSessionState().getRemoteIdentityKey()
state.store.deleteSession(jid, deviceid)
state.store.deleteIdentity(jid, identity_key)
self.update_context_list()
for path in paths:
it = mod.get_iter(path)
jid, fpr, deviceid = mod.get(it, 1, 3, 4)
fpr = fpr[31:-12]
YesNoDialog(
'Delete Fingerprint?',
'Do you want to delete the '
'fingerprint of <b>{}</b> on your account <b>{}</b>?'
'\n\n<tt>{}</tt>'.format(jid, account, fpr),
on_response_yes=on_yes, transient_for=self)
def trust_button_clicked_cb(self, button, *args):
active = self.B.get_object('account_combobox').get_active()
account = self.account_store[active][0]
@@ -298,43 +479,42 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
mod, paths = self.fpr_view.get_selection().get_selected_rows()
def on_yes(checked, identity_key):
state.store.setTrust(identity_key, TRUSTED)
try:
if self.plugin.ui_list[account]:
self.plugin.ui_list[account][jid]. \
refresh_auth_lock_icon()
except:
log.debug('UI not available')
self.update_context_list()
def on_no(identity_key):
state.store.setTrust(identity_key, UNTRUSTED)
try:
if jid in self.plugin.ui_list[account]:
self.plugin.ui_list[account][jid]. \
refresh_auth_lock_icon()
except:
log.debug('UI not available')
self.update_context_list()
for path in paths:
it = mod.get_iter(path)
_id, user, fpr = mod.get(it, 0, 1, 3)
jid, fpr, deviceid = mod.get(it, 1, 3, 4)
fpr = fpr[31:-12]
dlg = Gtk.Dialog('Trust / Revoke Fingerprint', self,
Gtk.DialogFlags.MODAL |
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(Gtk.STOCK_YES, Gtk.ResponseType.YES,
Gtk.STOCK_NO, Gtk.ResponseType.NO))
l = Gtk.Label()
l.set_markup('Do you want to trust the '
'fingerprint of <b>%s</b> on your account <b>%s</b>?'
'\n\n<tt>%s</tt>' % (user, account, fpr))
l.set_line_wrap(True)
l.set_padding(12, 12)
vbox = dlg.get_content_area()
vbox.add(l)
dlg.show_all()
response = dlg.run()
if response == Gtk.ResponseType.YES:
state.store.identityKeyStore.setTrust(_id, TRUSTED)
try:
if self.plugin.ui_list[account]:
self.plugin.ui_list[account][user].refresh_auth_lock_icon()
except:
dlg.destroy()
else:
if response == Gtk.ResponseType.NO:
state.store.identityKeyStore.setTrust(_id, UNTRUSTED)
try:
if user in self.plugin.ui_list[account]:
self.plugin.ui_list[account][user].refresh_auth_lock_icon()
except:
dlg.destroy()
record = state.store.loadSession(jid, deviceid)
identity_key = record.getSessionState().getRemoteIdentityKey()
self.update_context_list()
YesNoDialog(
'Trust / Revoke Fingerprint?',
'Do you want to trust the fingerprint of <b>{}</b> '
'on your account <b>{}</b>?\n\n'
'<tt>{}</tt>'.format(jid, account, fpr),
on_response_yes=(on_yes, identity_key),
on_response_no=(on_no, identity_key),
transient_for=self)
def cleardevice_button_clicked_cb(self, button, *args):
active = self.B.get_object('account_combobox').get_active()
@@ -380,67 +560,94 @@ class OMEMOConfigDialog(GajimPluginConfigDialog):
def update_context_list(self):
self.fpr_model.clear()
self.device_model.clear()
self.qrcode = self.B.get_object('qrcode')
self.qrinfo = self.B.get_object('qrinfo')
if len(self.account_store) == 0:
self.B.get_object('ID').set_markup('')
self.B.get_object('fingerprint_label').set_markup('')
self.B.get_object('trust_button').set_sensitive(False)
self.B.get_object('delfprbutton').set_sensitive(False)
self.B.get_object('refresh').set_sensitive(False)
self.B.get_object('cleardevice_button').set_sensitive(False)
self.B.get_object('qrcode').clear()
return
active = self.B.get_object('account_combobox').get_active()
account = self.account_store[active][0]
state = self.plugin.get_omemo_state(account)
# Set buttons active
self.B.get_object('trust_button').set_sensitive(True)
self.B.get_object('delfprbutton').set_sensitive(True)
self.B.get_object('refresh').set_sensitive(True)
if account == 'Local':
self.B.get_object('cleardevice_button').set_sensitive(False)
else:
self.B.get_object('cleardevice_button').set_sensitive(True)
# Set FPR Label and DeviceID
state = self.plugin.get_omemo_state(account)
deviceid = state.own_device_id
self.B.get_object('ID').set_markup('<tt>%s</tt>' % deviceid)
ownfpr = binascii.hexlify(state.store.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8')
ownfpr = self.human_hash(ownfpr[2:])
human_ownfpr = human_hash(ownfpr[2:])
self.B.get_object('fingerprint_label').set_markup('<tt>%s</tt>'
% ownfpr)
% human_ownfpr)
fprDB = state.store.identityKeyStore.getAllFingerprints()
activeSessions = state.store.sessionStore. \
getAllActiveSessionsKeys()
for item in fprDB:
_id, jid, fpr, tr = item
active = fpr in activeSessions
fpr = binascii.hexlify(fpr).decode('utf-8')
fpr = self.human_hash(fpr[2:])
jid = jid.decode('utf-8')
if tr == UNTRUSTED:
if active:
self.fpr_model.append((_id, jid, 'False',
'<tt><span foreground="#FF0040">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'False',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
elif tr == TRUSTED:
if active:
self.fpr_model.append((_id, jid, 'True',
'<tt><span foreground="#2EFE2E">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'True',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
else:
if active:
self.fpr_model.append((_id, jid, 'Undecided',
'<tt><span foreground="#FF8000">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'Undecided',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
# Set Fingerprint List
trust_str = {0: 'False', 1: 'True', 2: 'Undecided'}
session_db = state.store.getAllSessions()
for item in session_db:
color = {0: '#FF0040', # red
1: '#2EFE2E', # green
2: '#FF8000'} # orange
_id, jid, deviceid, record, active = item
active = bool(active)
identity_key = SessionRecord(serialized=record). \
getSessionState().getRemoteIdentityKey()
fpr = binascii.hexlify(
identity_key.getPublicKey().serialize()).decode('utf-8')
fpr = human_hash(fpr[2:])
trust = state.store.isTrustedIdentity(jid, identity_key)
if not active:
color[trust] = '#585858' # grey
self.fpr_model.append(
(_id, jid, trust_str[trust],
'<tt><span foreground="{}">{}</span></tt>'.
format(color[trust], fpr),
deviceid))
# Set Device ID List
for item in state.own_devices:
self.device_model.append([item])
def human_hash(self, fpr):
fpr = fpr.upper()
fplen = len(fpr)
wordsize = fplen // 8
buf = ''
for w in range(0, fplen, wordsize):
buf += '{0} '.format(fpr[w:w + wordsize])
return buf.rstrip()
# Set QR Verification Code
if PILLOW:
path = self.get_qrcode(
gajim.get_jid_from_account(account), deviceid, ownfpr[2:])
self.qrcode.set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(path))
self.qrinfo.hide()
else:
self.qrinfo.show()
class FingerprintWindow(Gtk.Dialog):
def __init__(self, plugin, contact, parent, windowinstances):
def __init__(self, plugin, contact, parent, windowinstances,
groupchat=False):
self.groupchat = groupchat
self.contact = contact
self.windowinstances = windowinstances
self.account = self.contact.account.name
self.plugin = plugin
self.omemostate = self.plugin.get_omemo_state(self.account)
self.own_jid = gajim.get_jid_from_account(self.account)
Gtk.Dialog.__init__(self,
title=('Fingerprints for %s') % contact.jid,
parent=parent,
@@ -448,45 +655,34 @@ class FingerprintWindow(Gtk.Dialog):
close_button = self.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)
close_button.connect('clicked', self.on_close_button_clicked)
self.connect('delete-event', self.on_window_delete)
self.plugin = plugin
self.GTK_BUILDER_FILE_PATH = \
self.plugin.local_file_path('fpr_dialog.ui')
self.B = Gtk.Builder()
self.B.set_translation_domain('gajim_plugins')
self.B.add_from_file(self.GTK_BUILDER_FILE_PATH)
self.xml = Gtk.Builder()
self.xml.add_from_file(self.GTK_BUILDER_FILE_PATH)
self.xml.set_translation_domain('gajim_plugins')
self.fpr_model = Gtk.ListStore(GObject.TYPE_INT,
GObject.TYPE_STRING,
GObject.TYPE_STRING,
GObject.TYPE_STRING)
self.fpr_model = self.xml.get_object('fingerprint_store')
self.fpr_view = self.B.get_object('fingerprint_view')
self.fpr_view.set_model(self.fpr_model)
self.fpr_view.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
self.fpr_view_own = self.B.get_object('fingerprint_view_own')
self.fpr_view_own.set_model(self.fpr_model)
self.fpr_view_own.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE)
self.notebook = self.B.get_object('notebook1')
self.fpr_view = self.xml.get_object('fingerprint_view')
self.fpr_view_own = self.xml.get_object('fingerprint_view_own')
self.notebook = self.xml.get_object('notebook1')
vbox = self.get_content_area()
vbox.pack_start(self.notebook, True, True, 0)
self.B.connect_signals(self)
self.account = self.contact.account.name
self.omemostate = self.plugin.get_omemo_state(self.account)
self.xml.connect_signals(self)
# Set own Fingerprint Label
ownfpr = binascii.hexlify(self.omemostate.store.getIdentityKeyPair()
.getPublicKey().serialize()).decode('utf-8')
ownfpr = self.human_hash(ownfpr[2:])
self.B.get_object('fingerprint_label_own').set_markup('<tt>%s</tt>'
% ownfpr)
ownfpr = human_hash(ownfpr[2:])
self.xml.get_object('fingerprint_label_own').set_markup('<tt>%s</tt>'
% ownfpr)
self.update_context_list()
self.show_all()
def on_close_button_clicked(self, widget):
del self.windowinstances['dialog']
self.hide()
@@ -496,43 +692,43 @@ class FingerprintWindow(Gtk.Dialog):
self.hide()
def trust_button_clicked_cb(self, button, *args):
state = self.omemostate
if self.notebook.get_current_page() == 1:
mod, paths = self.fpr_view_own.get_selection().get_selected_rows()
else:
mod, paths = self.fpr_view.get_selection().get_selected_rows()
for path in paths:
it = mod.get_iter(path)
_id, user, fpr = mod.get(it, 0, 1, 3)
fpr = fpr[31:-12]
dlg = Gtk.Dialog('Trust / Revoke Fingerprint', self,
Gtk.DialogFlags.MODAL |
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(Gtk.STOCK_YES, Gtk.ResponseType.YES,
Gtk.STOCK_NO, Gtk.ResponseType.NO))
l = Gtk.Label()
l.set_markup('Do you want to trust the '
'fingerprint of <b>%s</b> on your account <b>%s</b>?'
'\n\n<tt>%s</tt>' % (user, self.account, fpr))
l.set_line_wrap(True)
l.set_padding(12, 12)
vbox = dlg.get_content_area()
vbox.add(l)
dlg.show_all()
response = dlg.run()
if response == Gtk.ResponseType.YES:
self.omemostate.store.identityKeyStore.setTrust(_id, TRUSTED)
def on_yes(checked, identity_key):
state.store.setTrust(identity_key, TRUSTED)
if not self.groupchat:
self.plugin.ui_list[self.account][self.contact.jid]. \
refresh_auth_lock_icon()
dlg.destroy()
else:
if response == Gtk.ResponseType.NO:
self.omemostate.store.identityKeyStore.setTrust(_id, UNTRUSTED)
self.plugin.ui_list[self.account][self.contact.jid]. \
refresh_auth_lock_icon()
dlg.destroy()
self.update_context_list()
self.update_context_list()
def on_no(identity_key):
state.store.setTrust(identity_key, UNTRUSTED)
if not self.groupchat:
self.plugin.ui_list[self.account][self.contact.jid]. \
refresh_auth_lock_icon()
self.update_context_list()
for path in paths:
it = mod.get_iter(path)
jid, fpr, deviceid = mod.get(it, 1, 3, 4)
fpr = fpr[31:-12]
record = state.store.loadSession(jid, deviceid)
identity_key = record.getSessionState().getRemoteIdentityKey()
YesNoDialog(
'Trust / Revoke Fingerprint?',
'Do you want to trust the fingerprint of <b>{}</b> '
'on your account <b>{}</b>?\n\n'
'<tt>{}</tt>'.format(jid, self.account, fpr),
on_response_yes=(on_yes, identity_key),
on_response_no=(on_no, identity_key),
transient_for=self)
def fpr_button_pressed_cb(self, tw, event):
if event.button == 3:
@@ -547,7 +743,7 @@ class FingerprintWindow(Gtk.Dialog):
# selection, otherwise we only select the new item
keep_selection = tw.get_selection().path_is_selected(pthinfo[0])
pop = self.B.get_object('fprclipboard_menu')
pop = self.xml.get_object('fprclipboard_menu')
pop.popup(None, None, None, event.button, event.time)
# keep_selection=True -> no further processing of click event
@@ -571,49 +767,56 @@ class FingerprintWindow(Gtk.Dialog):
def update_context_list(self, *args):
self.fpr_model.clear()
state = self.omemostate
if self.notebook.get_current_page() == 1:
jid = gajim.get_jid_from_account(self.account)
contact_jid = self.own_jid
else:
jid = self.contact.jid
contact_jid = self.contact.jid
fprDB = self.omemostate.store.identityKeyStore.getFingerprints(jid)
activeSessions = self.omemostate.store.sessionStore. \
getActiveSessionsKeys(jid)
trust_str = {0: 'False', 1: 'True', 2: 'Undecided'}
if self.groupchat and self.notebook.get_current_page() == 0:
contact_jids = []
for nick in self.plugin.groupchat[contact_jid]:
real_jid = self.plugin.groupchat[contact_jid][nick]
if real_jid == self.own_jid:
continue
contact_jids.append(real_jid)
session_db = state.store.getSessionsFromJids(contact_jids)
else:
session_db = state.store.getSessionsFromJid(contact_jid)
for item in fprDB:
_id, jid, fpr, tr = item
active = fpr in activeSessions
fpr = binascii.hexlify(fpr).decode('utf-8')
fpr = self.human_hash(fpr[2:])
jid = jid.decode('utf-8')
if tr == UNTRUSTED:
if active:
self.fpr_model.append((_id, jid, 'False',
'<tt><span foreground="#FF0040">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'False',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
elif tr == TRUSTED:
if active:
self.fpr_model.append((_id, jid, 'True',
'<tt><span foreground="#2EFE2E">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'True',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
else:
if active:
self.fpr_model.append((_id, jid, 'Undecided',
'<tt><span foreground="#FF8000">%s</span></tt>' % fpr))
else:
self.fpr_model.append((_id, jid, 'Undecided',
'<tt><span foreground="#585858">%s</span></tt>' % fpr))
for item in session_db:
color = {0: '#FF0040', # red
1: '#2EFE2E', # green
2: '#FF8000'} # orange
def human_hash(self, fpr):
fpr = fpr.upper()
fplen = len(fpr)
wordsize = fplen // 8
buf = ''
for w in range(0, fplen, wordsize):
buf += '{0} '.format(fpr[w:w + wordsize])
return buf.rstrip()
_id, jid, deviceid, record, active = item
active = bool(active)
identity_key = SessionRecord(serialized=record). \
getSessionState().getRemoteIdentityKey()
fpr = binascii.hexlify(identity_key.getPublicKey().serialize()).decode('utf-8')
fpr = human_hash(fpr[2:])
trust = state.store.isTrustedIdentity(jid, identity_key)
if not active:
color[trust] = '#585858' # grey
self.fpr_model.append(
(_id, jid, trust_str[trust],
'<tt><span foreground="{}">{}</span></tt>'.
format(color[trust], fpr),
deviceid))
def human_hash(fpr):
fpr = fpr.upper()
fplen = len(fpr)
wordsize = fplen // 8
buf = ''
for w in range(0, fplen, wordsize):
buf += '{0} '.format(fpr[w:w + wordsize])
return buf.rstrip()

View File

@@ -79,10 +79,14 @@ class OmemoMessage(Node):
# , contact_jid, key, iv, payload, dev_id, my_dev_id):
Node.__init__(self, 'encrypted', attrs={'xmlns': NS_OMEMO})
header = Node('header', attrs={'sid': msg_dict['sid']})
for rid, key in msg_dict['keys'].items():
header.addChild('key', attrs={'rid': rid}).addData(b64encode(key)
.decode('utf-8'))
for rid, (key, prekey) in msg_dict['keys'].items():
if prekey:
child = header.addChild('key',
attrs={'prekey': 'true', 'rid': rid})
else:
child = header.addChild('key',
attrs={'rid': rid})
child.addData(b64encode(key).decode('utf-8'))
header.addChild('iv').addData(b64encode(msg_dict['iv']).decode('utf-8'))
self.addChild(node=header)
self.addChild('payload').addData(b64encode(msg_dict['payload'])