Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/gajim-plugins.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/omemo
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2016-08-29 00:26:56 +0300
committerPhilipp Hörist <philipp@hoerist.com>2016-08-29 00:26:56 +0300
commitdabfbbf826a091972eaac83c40ada5498f8d8933 (patch)
tree7d35b7ccff7c71415b4e7236acffee535e9ca10b /omemo
parent9457a18016146215924381de98a4e01adbcae75e (diff)
OMEMO GTK3 inital
Diffstat (limited to 'omemo')
-rw-r--r--omemo/.pylintrc379
-rw-r--r--omemo/.style.yapf4
-rw-r--r--omemo/CHANGELOG80
-rw-r--r--omemo/COPYING674
-rw-r--r--omemo/README.md90
-rw-r--r--omemo/__init__.py883
-rw-r--r--omemo/config_dialog.ui417
-rw-r--r--omemo/fpr_dialog.ui298
-rw-r--r--omemo/manifest.ini11
-rw-r--r--omemo/omemo.pngbin0 -> 5759 bytes
-rw-r--r--omemo/omemo/__init__.py1
-rw-r--r--omemo/omemo/aes_gcm.py42
-rw-r--r--omemo/omemo/aes_gcm_fallback.py152
-rw-r--r--omemo/omemo/aes_gcm_native.py61
-rw-r--r--omemo/omemo/db_helpers.py15
-rw-r--r--omemo/omemo/encryption.py64
-rw-r--r--omemo/omemo/liteaxolotlstore.py168
-rw-r--r--omemo/omemo/liteidentitykeystore.py167
-rw-r--r--omemo/omemo/liteprekeystore.py87
-rw-r--r--omemo/omemo/litesessionstore.py130
-rw-r--r--omemo/omemo/litesignedprekeystore.py113
-rw-r--r--omemo/omemo/sql.py147
-rw-r--r--omemo/omemo/state.py412
-rw-r--r--omemo/omemo16x16.pngbin0 -> 816 bytes
-rw-r--r--omemo/pkgs/PKGBUILD24
-rw-r--r--omemo/setup.cfg2
-rw-r--r--omemo/ui.py619
-rw-r--r--omemo/xmpp.py346
28 files changed, 5386 insertions, 0 deletions
diff --git a/omemo/.pylintrc b/omemo/.pylintrc
new file mode 100644
index 0000000..7222fa7
--- /dev/null
+++ b/omemo/.pylintrc
@@ -0,0 +1,379 @@
+[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
diff --git a/omemo/.style.yapf b/omemo/.style.yapf
new file mode 100644
index 0000000..9277d6a
--- /dev/null
+++ b/omemo/.style.yapf
@@ -0,0 +1,4 @@
+[style]
+based_on_style = pep8
+align_closing_bracket_with_visual_indent = true
+join_multiple_lines = true
diff --git a/omemo/CHANGELOG b/omemo/CHANGELOG
new file mode 100644
index 0000000..20bd7d1
--- /dev/null
+++ b/omemo/CHANGELOG
@@ -0,0 +1,80 @@
+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
+
+0.8.1 / 2016-08-05
+- Query own Device Bundles on send button press
+- Make Fingerprint Window higher and rename Buttons for something more appropriate
+- Bugfixes
+
+0.8.0 / 2016-08-03
+- Encryption improvements:
+-- SignedPreKey renews every 24 hours
+-- New PreKeys are generated and published if less then 80 are available
+-- If the Python Cryptography package is installed native encryption is now used (faster on old devices)
+-- Bundle Information is only pulled right before sending a Message (see Business rules of the OMEMO XEP)
+-- If Contact supports OMEMO, encryption is activated automatically
+
+- Other Stuff:
+-- The Fingerprint Window pops up if the Send Button is pressed and there are new Fingerprints in the DB
+-- Message Correction now works with OMEMO (Press STRG + UP Arrow to correct the last send message)
+-- SQL Refactoring, so new users dont have to go through DB Migration
+-- Small bugfixes
+
+0.7.5 / 2016-07-20
+================
+- Announcing of Support right after Plugin activation
+- New Context Menu for Gajim Compact View
+- Own Device Fingerprints are now available in the Fingerprint Window
+- Small bugfixes
+
+0.7 / 2016-07-16
+================
+- Reworked publishing Devicelist
+- Deactivate Gajim E2E on startup
+- Added new OMEMO popup menu
+- UI & handling of inactive Devices
+- various refactoring
+
+0.6 / 2016-06-30
+================
+- Add MAM support
+- Added Fingerprint Trustmanagment UI
+- Added Plugin Config Menu
+
+0.5 / 2016-05-02
+================
+- Add Windows support
+- Fix bugs
+
+0.4 / 2016-01-21
+==================
+
+ * Update README.md
+ * Fix #32: Add own devices as possible OMEMO partners.
+ * Fix one of the errors in #26
+ * Fix sqlite db intialization
+ * Use the standalone python-omemo library
+ * FIx LOG_DB errors / lost messages
+ * Move all OMEMO related parts to own dir
+ * Rename all links from kalkin/.. to omemo/...
+ * Update archlinux PKGBUILD to 0.3
+
+0.3 / 2016-01-10
+==================
+ * Save if OMEMO is enabled between restarts - #17
+ * Disable OMEMO if dependencies are missing - #9
+ * Make logging less verbose
+ * Add Arch Linux PKGBUILD file (Thanks Tommaso Sardelli)
+ * Extend README
+ * Fix hiding OMEMO controls in muc
+ * Fix "'ChatControl' object has no attribute 'lock_image'" bug - #16
+ * Ui clearly displays which message is encrypted (and how) - #15
+ * Plaintext messages are now always marked - #15
+
+# 2015-12-27
+- Fix crash, if jid is not in list (Thanks Mic92)
+- Fix clear_device_list, if account is not connected (Thanks Mic92)
+- Provide python-axolotl installation instructions in README and manifest.ini
diff --git a/omemo/COPYING b/omemo/COPYING
new file mode 100644
index 0000000..818433e
--- /dev/null
+++ b/omemo/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program 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.
+
+ This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/omemo/README.md b/omemo/README.md
new file mode 100644
index 0000000..e72cceb
--- /dev/null
+++ b/omemo/README.md
@@ -0,0 +1,90 @@
+# 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)
diff --git a/omemo/__init__.py b/omemo/__init__.py
new file mode 100644
index 0000000..0220169
--- /dev/null
+++ b/omemo/__init__.py
@@ -0,0 +1,883 @@
+# -*- 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)
diff --git a/omemo/config_dialog.ui b/omemo/config_dialog.ui
new file mode 100644
index 0000000..df0b849
--- /dev/null
+++ b/omemo/config_dialog.ui
@@ -0,0 +1,417 @@
+<?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="deviceid_store">
+ <columns>
+ <!-- column-name Device -->
+ <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>
+ <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="hbox2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="label4">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes" comments="label for account selector">&lt;b&gt;Account:&lt;/b&gt;</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBox" id="account_combobox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">account_store</property>
+ <signal name="changed" handler="account_combobox_changed_cb" swapped="no"/>
+ <child>
+ <object class="GtkCellRendererText" id="cellrenderertext1"/>
+ <attributes>
+ <attribute name="text">0</attribute>
+ </attributes>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="fingerprint_label_desc">
+ <property name="width_request">110</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</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">
+ <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">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox5">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="OwnIDLabel">
+ <property name="width_request">110</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Own Device ID:</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="ID">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">0</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ </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" comments="tab label">Own Fingerprints</property>
+ </object>
+ <packing>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <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="height_request">300</property>
+ <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">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel" id="label2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes" comments="tab label">Known Fingerprints</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab_fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkVBox" id="vbox3">
+ <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="GtkLabel" id="label5">
+ <property name="height_request">25</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Published Devices</property>
+ <attributes>
+ <attribute name="style" value="normal"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">7</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow2">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTreeView" id="deviceid_view">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="model">deviceid_store</property>
+ <property name="search_column">0</property>
+ <child>
+ <object class="GtkTreeViewColumn" id="deviceid_column">
+ <property name="title" translatable="yes">Device ID</property>
+ <child>
+ <object class="GtkCellRendererText" id="cellrenderertext3"/>
+ <attributes>
+ <attribute name="text">0</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="cleardevice_button">
+ <property name="label" translatable="yes">Clear Devices</property>
+ <property name="width_request">160</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="clicked" handler="cleardevice_button_clicked_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="refresh">
+ <property name="label">gtk-refresh</property>
+ <property name="width_request">160</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_stock">True</property>
+ <signal name="clicked" handler="refresh_button_clicked_cb" swapped="no"/>
+ </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">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </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">Clear Devices</property>
+ </object>
+ <packing>
+ <property name="position">2</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>
diff --git a/omemo/fpr_dialog.ui b/omemo/fpr_dialog.ui
new file mode 100644
index 0000000..76fbcc5
--- /dev/null
+++ b/omemo/fpr_dialog.ui
@@ -0,0 +1,298 @@
+<?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>
diff --git a/omemo/manifest.ini b/omemo/manifest.ini
new file mode 100644
index 0000000..ee82b11
--- /dev/null
+++ b/omemo/manifest.ini
@@ -0,0 +1,11 @@
+[info]
+name: OMEMO
+short_name: omemo
+version: 0.9.0
+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
+min_gajim_version: 0.16.9
+max_gajim_version: 0.16.11
diff --git a/omemo/omemo.png b/omemo/omemo.png
new file mode 100644
index 0000000..8d1c0fa
--- /dev/null
+++ b/omemo/omemo.png
Binary files differ
diff --git a/omemo/omemo/__init__.py b/omemo/omemo/__init__.py
new file mode 100644
index 0000000..3f5c4a7
--- /dev/null
+++ b/omemo/omemo/__init__.py
@@ -0,0 +1 @@
+__version__ = "0.1.0"
diff --git a/omemo/omemo/aes_gcm.py b/omemo/omemo/aes_gcm.py
new file mode 100644
index 0000000..67e7328
--- /dev/null
+++ b/omemo/omemo/aes_gcm.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
+#
+# This file is part of python-omemo library.
+#
+# The python-omemo library 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.
+#
+# python-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 python-omemo library. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import logging
+log = logging.getLogger('gajim.plugin_system.omemo')
+try:
+ from .aes_gcm_native import aes_decrypt
+ from .aes_gcm_native import aes_encrypt
+ log.debug('Using fast cryptography')
+except ImportError:
+ from .aes_gcm_fallback import aes_decrypt
+ from .aes_gcm_fallback import aes_encrypt
+ log.debug('Using slow cryptography')
+
+
+def encrypt(key, iv, plaintext):
+ return aes_encrypt(key, iv, plaintext)
+
+
+def decrypt(key, iv, ciphertext):
+ return aes_decrypt(key, iv, ciphertext)
+
+
+class NoValidSessions(Exception):
+ pass
diff --git a/omemo/omemo/aes_gcm_fallback.py b/omemo/omemo/aes_gcm_fallback.py
new file mode 100644
index 0000000..f157a22
--- /dev/null
+++ b/omemo/omemo/aes_gcm_fallback.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2014 Jonathan Zdziarski <jonathan@zdziarski.com>
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# 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.
+
+from struct import pack, unpack
+
+from Crypto.Cipher import AES
+from Crypto.Util import strxor
+
+
+def gcm_rightshift(vec):
+ for x in range(15, 0, -1):
+ c = vec[x] >> 1
+ c |= (vec[x - 1] << 7) & 0x80
+ vec[x] = c
+ vec[0] >>= 1
+ return vec
+
+
+def gcm_gf_mult(a, b):
+ mask = [0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01]
+ poly = [0x00, 0xe1]
+
+ Z = [0] * 16
+ V = [c for c in a]
+
+ for x in range(128):
+ if b[x >> 3] & mask[x & 7]:
+ Z = [V[y] ^ Z[y] for y in range(16)]
+ bit = V[15] & 1
+ V = gcm_rightshift(V)
+ V[0] ^= poly[bit]
+ return Z
+
+
+def ghash(h, auth_data, data):
+ u = (16 - len(data)) % 16
+ v = (16 - len(auth_data)) % 16
+
+ x = auth_data + chr(0) * v + data + chr(0) * u
+ x += pack('>QQ', len(auth_data) * 8, len(data) * 8)
+
+ y = [0] * 16
+ vec_h = [ord(c) for c in h]
+
+ for i in range(0, len(x), 16):
+ block = [ord(c) for c in x[i:i + 16]]
+ y = [y[j] ^ block[j] for j in range(16)]
+ y = gcm_gf_mult(y, vec_h)
+
+ return ''.join(chr(c) for c in y)
+
+
+def inc32(block):
+ counter, = unpack('>L', block[12:])
+ counter += 1
+ return block[:12] + pack('>L', counter)
+
+
+def gctr(k, icb, plaintext):
+ y = ''
+ if len(plaintext) == 0:
+ return y
+
+ aes = AES.new(k)
+ cb = icb
+
+ for i in range(0, len(plaintext), aes.block_size):
+ cb = inc32(cb)
+ encrypted = aes.encrypt(cb)
+ plaintext_block = plaintext[i:i + aes.block_size]
+ y += strxor.strxor(plaintext_block, encrypted[:len(plaintext_block)])
+
+ return y
+
+
+def gcm_decrypt(k, iv, encrypted, auth_data, tag):
+ aes = AES.new(k)
+ h = aes.encrypt(chr(0) * aes.block_size)
+
+ if len(iv) == 12:
+ y0 = iv + "\x00\x00\x00\x01"
+ else:
+ y0 = ghash(h, '', iv)
+
+ decrypted = gctr(k, y0, encrypted)
+ s = ghash(h, auth_data, encrypted)
+
+ t = aes.encrypt(y0)
+ T = strxor.strxor(s, t)
+ if T != tag:
+ raise ValueError('Decrypted data is invalid')
+ else:
+ return decrypted
+
+
+def gcm_encrypt(k, iv, plaintext, auth_data):
+ aes = AES.new(k)
+ h = aes.encrypt(chr(0) * aes.block_size)
+
+ if len(iv) == 12:
+ y0 = iv + "\x00\x00\x00\x01"
+ else:
+ y0 = ghash(h, '', iv)
+
+ encrypted = gctr(k, y0, plaintext)
+ s = ghash(h, auth_data, encrypted)
+
+ t = aes.encrypt(y0)
+ T = strxor.strxor(s, t)
+ return (encrypted, T)
+
+
+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
+
+
+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:]
+ return gcm_decrypt(key, nonce, ciphertext, '', mac)
diff --git a/omemo/omemo/aes_gcm_native.py b/omemo/omemo/aes_gcm_native.py
new file mode 100644
index 0000000..7781573
--- /dev/null
+++ b/omemo/omemo/aes_gcm_native.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
+#
+# This file is part of python-omemo library.
+#
+# The python-omemo library 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.
+#
+# python-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 python-omemo library. If not, see <http://www.gnu.org/licenses/>.
+#
+
+
+import os
+from cryptography.hazmat.primitives.ciphers import Cipher
+from cryptography.hazmat.primitives.ciphers import algorithms
+from cryptography.hazmat.primitives.ciphers.modes import GCM
+
+# On Windows we have to import a specific backend because the
+# default_backend() mechanism doesnt work in Gajim for Windows.
+# Its because of how Gajim is build with cx_freeze
+
+if os.name == 'nt':
+ from cryptography.hazmat.backends.openssl import backend
+else:
+ from cryptography.hazmat.backends import default_backend
+
+
+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 os.name == 'nt':
+ _backend = backend
+ else:
+ _backend = default_backend()
+ decryptor = Cipher(
+ algorithms.AES(key),
+ GCM(iv, tag=tag),
+ backend=_backend).decryptor()
+ return decryptor.update(data) + decryptor.finalize()
+
+
+def aes_encrypt(key, iv, plaintext):
+ """ Use AES128 GCM with the given key and iv to encrypt the plaintext. """
+ if os.name == 'nt':
+ _backend = backend
+ else:
+ _backend = default_backend()
+ encryptor = Cipher(
+ algorithms.AES(key),
+ GCM(iv),
+ backend=_backend).encryptor()
+ return encryptor.update(plaintext) + encryptor.finalize() + encryptor.tag
diff --git a/omemo/omemo/db_helpers.py b/omemo/omemo/db_helpers.py
new file mode 100644
index 0000000..dc95d6c
--- /dev/null
+++ b/omemo/omemo/db_helpers.py
@@ -0,0 +1,15 @@
+''' Database helper functions '''
+
+
+def table_exists(db, name):
+ """ Check if the specified table exists in the db. """
+
+ query = """ SELECT name FROM sqlite_master
+ WHERE type='table' AND name=?;
+ """
+ return db.execute(query, (name, )).fetchone() is not None
+
+
+def user_version(db):
+ """ Return the value of PRAGMA user_version. """
+ return db.execute('PRAGMA user_version').fetchone()[0]
diff --git a/omemo/omemo/encryption.py b/omemo/omemo/encryption.py
new file mode 100644
index 0000000..e4d4fd8
--- /dev/null
+++ b/omemo/omemo/encryption.py
@@ -0,0 +1,64 @@
+# -*- 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/>.
+#
+
+
+class EncryptionState():
+ """ Used to store if OMEMO is enabled or not between gajim restarts """
+
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+
+ def activate(self, jid):
+ q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
+ VALUES (?, 1) """
+
+ c = self.dbConn.cursor()
+ c.execute(q, (jid, ))
+ self.dbConn.commit()
+
+ def deactivate(self, jid):
+ q = """INSERT OR REPLACE INTO encryption_state (jid, encryption)
+ VALUES (?, 0)"""
+
+ c = self.dbConn.cursor()
+ c.execute(q, (jid, ))
+ self.dbConn.commit()
+
+ def is_active(self, jid):
+ q = 'SELECT encryption FROM encryption_state where jid = ?;'
+ c = self.dbConn.cursor()
+ c.execute(q, (jid, ))
+ result = c.fetchone()
+ if result is None:
+ return False
+ return result[0]
+
+ def exist(self, jid):
+ q = 'SELECT encryption FROM encryption_state where jid = ?;'
+ c = self.dbConn.cursor()
+ c.execute(q, (jid, ))
+ result = c.fetchone()
+ if result is None:
+ return False
+ else:
+ return True
diff --git a/omemo/omemo/liteaxolotlstore.py b/omemo/omemo/liteaxolotlstore.py
new file mode 100644
index 0000000..64f14b3
--- /dev/null
+++ b/omemo/omemo/liteaxolotlstore.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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
+
+from axolotl.state.axolotlstore import AxolotlStore
+from axolotl.util.keyhelper import KeyHelper
+
+from .liteidentitykeystore import LiteIdentityKeyStore
+from .liteprekeystore import LitePreKeyStore
+from .litesessionstore import LiteSessionStore
+from .litesignedprekeystore import LiteSignedPreKeyStore
+from .encryption import EncryptionState
+from .sql import SQLDatabase
+
+log = logging.getLogger('gajim.plugin_system.omemo')
+
+DEFAULT_PREKEY_AMOUNT = 100
+MIN_PREKEY_AMOUNT = 80
+SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
+SPK_CYCLE_TIME = 86400 # 24 Hours
+
+
+class LiteAxolotlStore(AxolotlStore):
+ def __init__(self, connection):
+ try:
+ connection.text_factory = bytes
+ except(AttributeError):
+ raise AssertionError('Expected a sqlite3.Connection got ' +
+ str(connection))
+
+ self.sql = SQLDatabase(connection)
+ self.identityKeyStore = LiteIdentityKeyStore(connection)
+ self.preKeyStore = LitePreKeyStore(connection)
+ self.signedPreKeyStore = LiteSignedPreKeyStore(connection)
+ self.sessionStore = LiteSessionStore(connection)
+ self.encryptionStore = EncryptionState(connection)
+
+ if not self.getLocalRegistrationId():
+ log.info("Generating Axolotl keys")
+ self._generate_axolotl_keys()
+
+ def _generate_axolotl_keys(self):
+ identityKeyPair = KeyHelper.generateIdentityKeyPair()
+ registrationId = KeyHelper.generateRegistrationId()
+ preKeys = KeyHelper.generatePreKeys(KeyHelper.getRandomSequence(),
+ DEFAULT_PREKEY_AMOUNT)
+ self.storeLocalData(registrationId, identityKeyPair)
+
+ signedPreKey = KeyHelper.generateSignedPreKey(
+ identityKeyPair, KeyHelper.getRandomSequence(65536))
+
+ self.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
+
+ for preKey in preKeys:
+ self.storePreKey(preKey.getId(), preKey)
+
+ def getIdentityKeyPair(self):
+ return self.identityKeyStore.getIdentityKeyPair()
+
+ def storeLocalData(self, registrationId, identityKeyPair):
+ self.identityKeyStore.storeLocalData(registrationId, identityKeyPair)
+
+ def getLocalRegistrationId(self):
+ return self.identityKeyStore.getLocalRegistrationId()
+
+ def saveIdentity(self, recepientId, identityKey):
+ self.identityKeyStore.saveIdentity(recepientId, identityKey)
+
+ def isTrustedIdentity(self, recepientId, identityKey):
+ return self.identityKeyStore.isTrustedIdentity(recepientId,
+ identityKey)
+
+ def getTrustedFingerprints(self, jid):
+ return self.identityKeyStore.getTrustedFingerprints(jid)
+
+ def getUndecidedFingerprints(self, jid):
+ return self.identityKeyStore.getUndecidedFingerprints(jid)
+
+ def setShownFingerprints(self, jid):
+ return self.identityKeyStore.setShownFingerprints(jid)
+
+ def getNewFingerprints(self, jid):
+ return self.identityKeyStore.getNewFingerprints(jid)
+
+ def loadPreKey(self, preKeyId):
+ return self.preKeyStore.loadPreKey(preKeyId)
+
+ def loadPreKeys(self):
+ return self.preKeyStore.loadPendingPreKeys()
+
+ def storePreKey(self, preKeyId, preKeyRecord):
+ self.preKeyStore.storePreKey(preKeyId, preKeyRecord)
+
+ def containsPreKey(self, preKeyId):
+ return self.preKeyStore.containsPreKey(preKeyId)
+
+ def removePreKey(self, preKeyId):
+ self.preKeyStore.removePreKey(preKeyId)
+
+ def loadSession(self, recepientId, deviceId):
+ return self.sessionStore.loadSession(recepientId, deviceId)
+
+ def getActiveDeviceTuples(self):
+ return self.sessionStore.getActiveDeviceTuples()
+
+ def getInactiveSessionsKeys(self, recipientId):
+ return self.sessionStore.getInactiveSessionsKeys(recipientId)
+
+ def getSubDeviceSessions(self, recepientId):
+ # TODO Reuse this
+ return self.sessionStore.getSubDeviceSessions(recepientId)
+
+ def storeSession(self, recepientId, deviceId, sessionRecord):
+ self.sessionStore.storeSession(recepientId, deviceId, sessionRecord)
+
+ def containsSession(self, recepientId, deviceId):
+ return self.sessionStore.containsSession(recepientId, deviceId)
+
+ def deleteSession(self, recepientId, deviceId):
+ self.sessionStore.deleteSession(recepientId, deviceId)
+
+ def deleteAllSessions(self, recepientId):
+ self.sessionStore.deleteAllSessions(recepientId)
+
+ def loadSignedPreKey(self, signedPreKeyId):
+ return self.signedPreKeyStore.loadSignedPreKey(signedPreKeyId)
+
+ def loadSignedPreKeys(self):
+ return self.signedPreKeyStore.loadSignedPreKeys()
+
+ def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
+ self.signedPreKeyStore.storeSignedPreKey(signedPreKeyId,
+ signedPreKeyRecord)
+
+ def containsSignedPreKey(self, signedPreKeyId):
+ return self.signedPreKeyStore.containsSignedPreKey(signedPreKeyId)
+
+ def removeSignedPreKey(self, signedPreKeyId):
+ self.signedPreKeyStore.removeSignedPreKey(signedPreKeyId)
+
+ def getNextSignedPreKeyId(self):
+ return self.signedPreKeyStore.getNextSignedPreKeyId()
+
+ def getCurrentSignedPreKeyId(self):
+ return self.signedPreKeyStore.getCurrentSignedPreKeyId()
+
+ def getSignedPreKeyTimestamp(self, signedPreKeyId):
+ return self.signedPreKeyStore.getSignedPreKeyTimestamp(signedPreKeyId)
+
+ def removeOldSignedPreKeys(self, timestamp):
+ self.signedPreKeyStore.removeOldSignedPreKeys(timestamp)
diff --git a/omemo/omemo/liteidentitykeystore.py b/omemo/omemo/liteidentitykeystore.py
new file mode 100644
index 0000000..29974bc
--- /dev/null
+++ b/omemo/omemo/liteidentitykeystore.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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/>.
+#
+
+from axolotl.ecc.djbec import DjbECPrivateKey, DjbECPublicKey
+from axolotl.identitykey import IdentityKey
+from axolotl.identitykeypair import IdentityKeyPair
+from axolotl.state.identitykeystore import IdentityKeyStore
+
+UNDECIDED = 2
+TRUSTED = 1
+UNTRUSTED = 0
+
+
+class LiteIdentityKeyStore(IdentityKeyStore):
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+
+ def getIdentityKeyPair(self):
+ q = "SELECT public_key, private_key FROM identities " + \
+ "WHERE recipient_id = -1"
+ c = self.dbConn.cursor()
+ c.execute(q)
+ result = c.fetchone()
+
+ publicKey, privateKey = result
+ return IdentityKeyPair(
+ IdentityKey(DjbECPublicKey(publicKey[1:])),
+ DjbECPrivateKey(privateKey))
+
+ def getLocalRegistrationId(self):
+ q = "SELECT registration_id FROM identities WHERE recipient_id = -1"
+ c = self.dbConn.cursor()
+ c.execute(q)
+ result = c.fetchone()
+ return result[0] if result else None
+
+ def storeLocalData(self, registrationId, identityKeyPair):
+ q = "INSERT INTO identities( " + \
+ "recipient_id, registration_id, public_key, private_key) " + \
+ "VALUES(-1, ?, ?, ?)"
+ c = self.dbConn.cursor()
+ c.execute(q,
+ (registrationId,
+ identityKeyPair.getPublicKey().getPublicKey().serialize(),
+ identityKeyPair.getPrivateKey().serialize()))
+
+ self.dbConn.commit()
+
+ def saveIdentity(self, recipientId, identityKey):
+ q = "INSERT INTO identities (recipient_id, public_key, trust) " \
+ "VALUES(?, ?, ?)"
+ c = self.dbConn.cursor()
+
+ if not self.getIdentity(recipientId, identityKey):
+ c.execute(q, (recipientId,
+ identityKey.getPublicKey().serialize(),
+ UNDECIDED))
+ self.dbConn.commit()
+
+ def getIdentity(self, recipientId, identityKey):
+ q = "SELECT * FROM identities WHERE recipient_id = ? " \
+ "AND public_key = ?"
+ c = self.dbConn.cursor()
+
+ c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
+ result = c.fetchone()
+
+ return result is not None
+
+ def isTrustedIdentity(self, recipientId, identityKey):
+ q = "SELECT trust FROM identities WHERE recipient_id = ? " \
+ "AND public_key = ?"
+ c = self.dbConn.cursor()
+
+ c.execute(q, (recipientId, identityKey.getPublicKey().serialize()))
+ result = c.fetchone()
+
+ states = [UNTRUSTED, TRUSTED, UNDECIDED]
+
+ if result and result[0] in states:
+ return result[0]
+ else:
+ return True
+
+ def getAllFingerprints(self):
+ q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
+ "WHERE recipient_id != -1 ORDER BY recipient_id ASC"
+ c = self.dbConn.cursor()
+
+ result = []
+ for row in c.execute(q):
+ result.append((row[0], row[1], row[2], row[3]))
+ return result
+
+ def getFingerprints(self, jid):
+ q = "SELECT _id, recipient_id, public_key, trust FROM identities " \
+ "WHERE recipient_id =? ORDER BY trust ASC"
+ c = self.dbConn.cursor()
+
+ result = []
+ c.execute(q, (jid,))
+ rows = c.fetchall()
+ for row in rows:
+ result.append((row[0], row[1], row[2], row[3]))
+ return result
+
+ def getTrustedFingerprints(self, jid):
+ q = "SELECT public_key FROM identities WHERE recipient_id = ? AND trust = ?"
+ c = self.dbConn.cursor()
+
+ result = []
+ c.execute(q, (jid, TRUSTED))
+ rows = c.fetchall()
+ for row in rows:
+ result.append(row[0])
+ return result
+
+ def getUndecidedFingerprints(self, jid):
+ q = "SELECT trust FROM identities WHERE recipient_id = ? AND trust = ?"
+ c = self.dbConn.cursor()
+
+ result = []
+ c.execute(q, (jid, UNDECIDED))
+ result = c.fetchall()
+
+ return result
+
+ def getNewFingerprints(self, jid):
+ q = "SELECT _id FROM identities WHERE shown = 0 AND " \
+ "recipient_id = ?"
+ c = self.dbConn.cursor()
+ result = []
+ for row in c.execute(q, (jid,)):
+ result.append(row[0])
+ return result
+
+ def setShownFingerprints(self, fingerprints):
+ q = "UPDATE identities SET shown = 1 WHERE _id IN ({})" \
+ .format(', '.join(['?'] * len(fingerprints)))
+ c = self.dbConn.cursor()
+ c.execute(q, fingerprints)
+ self.dbConn.commit()
+
+ def setTrust(self, _id, trust):
+ q = "UPDATE identities SET trust = ? WHERE _id = ?"
+ c = self.dbConn.cursor()
+ c.execute(q, (trust, _id))
+ self.dbConn.commit()
diff --git a/omemo/omemo/liteprekeystore.py b/omemo/omemo/liteprekeystore.py
new file mode 100644
index 0000000..78ffc7a
--- /dev/null
+++ b/omemo/omemo/liteprekeystore.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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/>.
+#
+
+from axolotl.state.prekeyrecord import PreKeyRecord
+from axolotl.state.prekeystore import PreKeyStore
+from axolotl.util.keyhelper import KeyHelper
+
+
+class LitePreKeyStore(PreKeyStore):
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+
+ def loadPreKey(self, preKeyId):
+ q = "SELECT record FROM prekeys WHERE prekey_id = ?"
+
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (preKeyId, ))
+
+ result = cursor.fetchone()
+ if not result:
+ raise Exception("No such prekeyRecord!")
+
+ return PreKeyRecord(serialized=result[0])
+
+ def loadPendingPreKeys(self):
+ q = "SELECT record FROM prekeys"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q)
+ result = cursor.fetchall()
+
+ return [PreKeyRecord(serialized=r[0]) for r in result]
+
+ def storePreKey(self, preKeyId, preKeyRecord):
+ q = "INSERT INTO prekeys (prekey_id, record) VALUES(?,?)"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (preKeyId, preKeyRecord.serialize()))
+ self.dbConn.commit()
+
+ def containsPreKey(self, preKeyId):
+ q = "SELECT record FROM prekeys WHERE prekey_id = ?"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (preKeyId, ))
+ return cursor.fetchone() is not None
+
+ def removePreKey(self, preKeyId):
+ q = "DELETE FROM prekeys WHERE prekey_id = ?"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (preKeyId, ))
+ self.dbConn.commit()
+
+ def getCurrentPreKeyId(self):
+ q = "SELECT MAX(prekey_id) FROM prekeys"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q)
+ return cursor.fetchone()[0]
+
+ def getPreKeyCount(self):
+ q = "SELECT COUNT(prekey_id) FROM prekeys"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q)
+ return cursor.fetchone()[0]
+
+ def generateNewPreKeys(self, count):
+ startId = self.getCurrentPreKeyId() + 1
+ preKeys = KeyHelper.generatePreKeys(startId, count)
+
+ for preKey in preKeys:
+ self.storePreKey(preKey.getId(), preKey)
diff --git a/omemo/omemo/litesessionstore.py b/omemo/omemo/litesessionstore.py
new file mode 100644
index 0000000..d8ef66c
--- /dev/null
+++ b/omemo/omemo/litesessionstore.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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/>.
+#
+
+from axolotl.state.sessionrecord import SessionRecord
+from axolotl.state.sessionstore import SessionStore
+
+
+class LiteSessionStore(SessionStore):
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+
+ def loadSession(self, recipientId, deviceId):
+ q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
+ c = self.dbConn.cursor()
+ c.execute(q, (recipientId, deviceId))
+ result = c.fetchone()
+
+ if result:
+ return SessionRecord(serialized=result[0])
+ else:
+ return SessionRecord()
+
+ def getSubDeviceSessions(self, recipientId):
+ q = "SELECT device_id from sessions WHERE recipient_id = ?"
+ c = self.dbConn.cursor()
+ c.execute(q, (recipientId, ))
+ result = c.fetchall()
+
+ deviceIds = [r[0] for r in result]
+ return deviceIds
+
+ def getActiveDeviceTuples(self):
+ q = "SELECT recipient_id, device_id FROM sessions WHERE active = 1"
+ c = self.dbConn.cursor()
+ result = []
+ for row in c.execute(q):
+ result.append((row[0].decode('utf-8'), row[1]))
+ return result
+
+ def storeSession(self, recipientId, deviceId, sessionRecord):
+ self.deleteSession(recipientId, deviceId)
+
+ q = "INSERT INTO sessions(recipient_id, device_id, record) VALUES(?,?,?)"
+ c = self.dbConn.cursor()
+ c.execute(q, (recipientId, deviceId, sessionRecord.serialize()))
+ self.dbConn.commit()
+
+ def containsSession(self, recipientId, deviceId):
+ q = "SELECT record FROM sessions WHERE recipient_id = ? AND device_id = ?"
+ c = self.dbConn.cursor()
+ c.execute(q, (recipientId, deviceId))
+ result = c.fetchone()
+
+ return result is not None
+
+ def deleteSession(self, recipientId, deviceId):
+ q = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?"
+ self.dbConn.cursor().execute(q, (recipientId, deviceId))
+ self.dbConn.commit()
+
+ def deleteAllSessions(self, recipientId):
+ q = "DELETE FROM sessions WHERE recipient_id = ?"
+ self.dbConn.cursor().execute(q, (recipientId, ))
+ self.dbConn.commit()
+
+ def setActiveState(self, deviceList, jid):
+ c = self.dbConn.cursor()
+
+ q = "UPDATE sessions SET active = {} " \
+ "WHERE recipient_id = '{}' AND device_id IN ({})" \
+ .format(1, jid, ', '.join(['?'] * len(deviceList)))
+ c.execute(q, deviceList)
+
+ q = "UPDATE sessions SET active = {} " \
+ "WHERE recipient_id = '{}' AND device_id NOT IN ({})" \
+ .format(0, jid, ', '.join(['?'] * len(deviceList)))
+ 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()
+ result = []
+ for row in c.execute(q, (recipientId,)):
+ public_key = (SessionRecord(serialized=row[0]).
+ getSessionState().getRemoteIdentityKey().
+ getPublicKey())
+ result.append(public_key.serialize())
+ return result
diff --git a/omemo/omemo/litesignedprekeystore.py b/omemo/omemo/litesignedprekeystore.py
new file mode 100644
index 0000000..d6e4a90
--- /dev/null
+++ b/omemo/omemo/litesignedprekeystore.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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/>.
+#
+
+from axolotl.invalidkeyidexception import InvalidKeyIdException
+from axolotl.state.signedprekeyrecord import SignedPreKeyRecord
+from axolotl.state.signedprekeystore import SignedPreKeyStore
+from axolotl.util.medium import Medium
+
+
+class LiteSignedPreKeyStore(SignedPreKeyStore):
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+
+ def loadSignedPreKey(self, signedPreKeyId):
+ q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
+
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (signedPreKeyId, ))
+
+ result = cursor.fetchone()
+ if not result:
+ raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
+ signedPreKeyId)
+
+ return SignedPreKeyRecord(serialized=result[0])
+
+ def loadSignedPreKeys(self):
+ q = "SELECT record FROM signed_prekeys"
+
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, )
+ result = cursor.fetchall()
+ results = []
+ for row in result:
+ results.append(SignedPreKeyRecord(serialized=row[0]))
+
+ return results
+
+ def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
+ q = "INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (signedPreKeyId, signedPreKeyRecord.serialize()))
+ self.dbConn.commit()
+
+ def containsSignedPreKey(self, signedPreKeyId):
+ q = "SELECT record FROM signed_prekeys WHERE prekey_id = ?"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (signedPreKeyId, ))
+ return cursor.fetchone() is not None
+
+ def removeSignedPreKey(self, signedPreKeyId):
+ q = "DELETE FROM signed_prekeys WHERE prekey_id = ?"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (signedPreKeyId, ))
+ self.dbConn.commit()
+
+ def getNextSignedPreKeyId(self):
+ result = self.getCurrentSignedPreKeyId()
+ if not result:
+ return 1 # StartId if no SignedPreKeys exist
+ else:
+ return (result % (Medium.MAX_VALUE - 1)) + 1
+
+ def getCurrentSignedPreKeyId(self):
+ q = "SELECT MAX(prekey_id) FROM signed_prekeys"
+
+ cursor = self.dbConn.cursor()
+ cursor.execute(q)
+ result = cursor.fetchone()
+ if not result:
+ return None
+ else:
+ return result[0]
+
+ def getSignedPreKeyTimestamp(self, signedPreKeyId):
+ q = "SELECT strftime('%s', timestamp) FROM " \
+ "signed_prekeys WHERE prekey_id = ?"
+
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (signedPreKeyId, ))
+
+ result = cursor.fetchone()
+ if not result:
+ raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
+ signedPreKeyId)
+
+ return result[0]
+
+ def removeOldSignedPreKeys(self, timestamp):
+ q = "DELETE FROM signed_prekeys " \
+ "WHERE timestamp < datetime(?, 'unixepoch')"
+ cursor = self.dbConn.cursor()
+ cursor.execute(q, (timestamp, ))
+ self.dbConn.commit()
diff --git a/omemo/omemo/sql.py b/omemo/omemo/sql.py
new file mode 100644
index 0000000..25571f8
--- /dev/null
+++ b/omemo/omemo/sql.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Tarek Galal <tare2.galal@gmail.com>
+#
+# 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/>.
+#
+from .db_helpers import user_version
+
+
+class SQLDatabase():
+ """ SQL Database """
+
+ def __init__(self, dbConn):
+ """
+ :type dbConn: Connection
+ """
+ self.dbConn = dbConn
+ self.createDb()
+ self.migrateDb()
+
+ def createDb(self):
+ if user_version(self.dbConn) == 0:
+
+ # Creates
+ # IdentityKeyStore
+ # PreKeyStore
+ # SignedPreKeyStore
+ # SessionStore
+ # EncryptionStore
+
+ create_tables = '''
+ CREATE TABLE IF NOT EXISTS identities (
+ _id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT,
+ registration_id INTEGER, public_key BLOB, private_key BLOB,
+ next_prekey_id INTEGER, timestamp INTEGER, trust INTEGER,
+ shown INTEGER DEFAULT 0);
+
+ CREATE UNIQUE INDEX IF NOT EXISTS
+ public_key_index ON identities (public_key, recipient_id);
+
+ CREATE TABLE IF NOT EXISTS prekeys(
+ _id INTEGER PRIMARY KEY AUTOINCREMENT,
+ prekey_id INTEGER UNIQUE, sent_to_server BOOLEAN,
+ record BLOB);
+
+ CREATE TABLE IF NOT EXISTS signed_prekeys (
+ _id INTEGER PRIMARY KEY AUTOINCREMENT,
+ prekey_id INTEGER UNIQUE,
+ timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
+
+ CREATE TABLE IF NOT EXISTS sessions (
+ _id INTEGER PRIMARY KEY AUTOINCREMENT,
+ recipient_id TEXT, device_id INTEGER,
+ record BLOB, timestamp INTEGER, active INTEGER DEFAULT 1,
+ UNIQUE(recipient_id, device_id));
+
+ CREATE TABLE IF NOT EXISTS encryption_state (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ jid TEXT UNIQUE,
+ encryption INTEGER,
+ timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP
+ );
+ '''
+
+ create_db_sql = """
+ BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=5;
+ END TRANSACTION;
+ """ % (create_tables)
+ self.dbConn.executescript(create_db_sql)
+
+ def migrateDb(self):
+ """ Migrates the DB
+ """
+
+ # Find all double entrys and delete them
+ if user_version(self.dbConn) < 2:
+ delete_dupes = """ DELETE FROM identities WHERE _id not in (
+ SELECT MIN(_id)
+ FROM identities
+ GROUP BY
+ recipient_id, public_key
+ );
+ """
+
+ self.dbConn.executescript(""" BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=2;
+ END TRANSACTION;
+ """ % (delete_dupes))
+
+ if user_version(self.dbConn) < 3:
+ # Create a UNIQUE INDEX so every public key/recipient_id tuple
+ # can only be once in the db
+ add_index = """ CREATE UNIQUE INDEX IF NOT EXISTS
+ public_key_index
+ ON identities (public_key, recipient_id);
+ """
+
+ self.dbConn.executescript(""" BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=3;
+ END TRANSACTION;
+ """ % (add_index))
+
+ if user_version(self.dbConn) < 4:
+ # Adds column "active" to the sessions table
+ add_active = """ ALTER TABLE sessions
+ ADD COLUMN active INTEGER DEFAULT 1;
+ """
+
+ self.dbConn.executescript(""" BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=4;
+ END TRANSACTION;
+ """ % (add_active))
+
+ if user_version(self.dbConn) < 5:
+ # Adds DEFAULT Timestamp
+ add_timestamp = """
+ DROP TABLE signed_prekeys;
+ CREATE TABLE IF NOT EXISTS signed_prekeys (
+ _id INTEGER PRIMARY KEY AUTOINCREMENT,
+ prekey_id INTEGER UNIQUE,
+ timestamp NUMERIC DEFAULT CURRENT_TIMESTAMP, record BLOB);
+ ALTER TABLE identities ADD COLUMN shown INTEGER DEFAULT 0;
+ UPDATE identities SET shown = 1;
+ """
+
+ self.dbConn.executescript(""" BEGIN TRANSACTION;
+ %s
+ PRAGMA user_version=5;
+ END TRANSACTION;
+ """ % (add_timestamp))
diff --git a/omemo/omemo/state.py b/omemo/omemo/state.py
new file mode 100644
index 0000000..dd06e05
--- /dev/null
+++ b/omemo/omemo/state.py
@@ -0,0 +1,412 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.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 time
+from base64 import b64encode
+
+from axolotl.ecc.djbec import DjbECPublicKey
+from axolotl.identitykey import IdentityKey
+from axolotl.duplicatemessagexception import DuplicateMessageException
+from axolotl.invalidmessageexception import InvalidMessageException
+from axolotl.invalidversionexception import InvalidVersionException
+from axolotl.untrustedidentityexception import UntrustedIdentityException
+from axolotl.nosessionexception import NoSessionException
+from axolotl.protocol.prekeywhispermessage import PreKeyWhisperMessage
+from axolotl.protocol.whispermessage import WhisperMessage
+from axolotl.sessionbuilder import SessionBuilder
+from axolotl.sessioncipher import SessionCipher
+from axolotl.state.prekeybundle import PreKeyBundle
+from axolotl.util.keyhelper import KeyHelper
+from Crypto.Random import get_random_bytes
+
+from .aes_gcm import NoValidSessions, decrypt, encrypt
+from .liteaxolotlstore import (LiteAxolotlStore, DEFAULT_PREKEY_AMOUNT,
+ MIN_PREKEY_AMOUNT, SPK_CYCLE_TIME,
+ SPK_ARCHIVE_TIME)
+
+log = logging.getLogger('gajim.plugin_system.omemo')
+logAxolotl = logging.getLogger('axolotl')
+
+
+UNTRUSTED = 0
+TRUSTED = 1
+UNDECIDED = 2
+
+
+class OmemoState:
+ def __init__(self, own_jid, connection, account, plugin):
+ """ Instantiates an OmemoState object.
+
+ :param connection: an :py:class:`sqlite3.Connection`
+ """
+ self.account = account
+ self.plugin = plugin
+ self.session_ciphers = {}
+ self.own_jid = own_jid
+ self.device_ids = {}
+ self.own_devices = []
+ self.store = LiteAxolotlStore(connection)
+ self.encryption = self.store.encryptionStore
+ for jid, device_id in self.store.getActiveDeviceTuples():
+ if jid != own_jid:
+ self.add_device(jid, device_id)
+ else:
+ self.add_own_device(device_id)
+
+ log.info(self.account + ' => Roster devices after boot:' +
+ str(self.device_ids))
+ log.info(self.account + ' => Own devices after boot:' +
+ str(self.own_devices))
+ log.debug(self.account + ' => ' +
+ str(self.store.preKeyStore.getPreKeyCount()) +
+ ' PreKeys available')
+
+ def build_session(self, recipient_id, device_id, bundle_dict):
+ sessionBuilder = SessionBuilder(self.store, self.store, self.store,
+ self.store, recipient_id, device_id)
+
+ registration_id = self.store.getLocalRegistrationId()
+
+ preKeyPublic = DjbECPublicKey(bundle_dict['preKeyPublic'][1:])
+
+ signedPreKeyPublic = DjbECPublicKey(bundle_dict['signedPreKeyPublic'][
+ 1:])
+ identityKey = IdentityKey(DjbECPublicKey(bundle_dict['identityKey'][
+ 1:]))
+
+ prekey_bundle = PreKeyBundle(
+ registration_id, device_id, bundle_dict['preKeyId'], preKeyPublic,
+ bundle_dict['signedPreKeyId'], signedPreKeyPublic,
+ bundle_dict['signedPreKeySignature'], identityKey)
+
+ sessionBuilder.processPreKeyBundle(prekey_bundle)
+ return self.get_session_cipher(recipient_id, device_id)
+
+ def set_devices(self, name, devices):
+ """ Return a an.
+
+ Parameters
+ ----------
+ jid : string
+ The contacts jid
+
+ devices: [int]
+ A list of devices
+ """
+
+ self.device_ids[name] = devices
+ log.info(self.account + ' => Saved devices for ' + name)
+
+ def add_device(self, name, device_id):
+ if name not in self.device_ids:
+ self.device_ids[name] = [device_id]
+ elif device_id not in self.device_ids[name]:
+ self.device_ids[name].append(device_id)
+
+ def set_own_devices(self, devices):
+ """ Overwrite the current :py:attribute:`OmemoState.own_devices` with
+ the given devices.
+
+ Parameters
+ ----------
+ devices : [int]
+ A list of device_ids
+ """
+ self.own_devices = devices
+ log.info(self.account + ' => Saved own devices')
+
+ def add_own_device(self, device_id):
+ if device_id not in self.own_devices:
+ self.own_devices.append(device_id)
+
+ @property
+ def own_device_id(self):
+ reg_id = self.store.getLocalRegistrationId()
+ assert reg_id is not None, \
+ "Requested device_id but there is no generated"
+
+ return ((reg_id % 2147483646) + 1)
+
+ def own_device_id_published(self):
+ """ Return `True` only if own device id was added via
+ :py:method:`OmemoState.set_own_devices()`.
+ """
+ return self.own_device_id in self.own_devices
+
+ @property
+ def bundle(self):
+ self.checkPreKeyAmount()
+ prekeys = [
+ (k.getId(), b64encode(k.getKeyPair().getPublicKey().serialize()))
+ for k in self.store.loadPreKeys()
+ ]
+
+ identityKeyPair = self.store.getIdentityKeyPair()
+
+ self.cycleSignedPreKey(identityKeyPair)
+
+ signedPreKey = self.store.loadSignedPreKey(
+ self.store.getCurrentSignedPreKeyId())
+
+ result = {
+ 'signedPreKeyId': signedPreKey.getId(),
+ 'signedPreKeyPublic':
+ b64encode(signedPreKey.getKeyPair().getPublicKey().serialize()),
+ 'signedPreKeySignature': b64encode(signedPreKey.getSignature()),
+ 'identityKey':
+ b64encode(identityKeyPair.getPublicKey().serialize()),
+ 'prekeys': prekeys
+ }
+ return result
+
+ def decrypt_msg(self, msg_dict):
+ own_id = self.own_device_id
+ if msg_dict['sid'] == own_id:
+ log.info('Received previously sent message by us')
+ return
+ if own_id not in msg_dict['keys']:
+ log.warning('OMEMO message does not contain our device key')
+ return
+
+ iv = msg_dict['iv']
+ sid = msg_dict['sid']
+ sender_jid = msg_dict['sender_jid']
+ payload = msg_dict['payload']
+
+ encrypted_key = msg_dict['keys'][own_id]
+
+ try:
+ key = self.handlePreKeyWhisperMessage(sender_jid, sid,
+ encrypted_key)
+ except (InvalidVersionException, InvalidMessageException):
+ try:
+ 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)
+ return
+ except (DuplicateMessageException) as e:
+ log.warning('Duplicate message found ' + str(e.args))
+ return
+
+ except (DuplicateMessageException) as e:
+ log.warning('Duplicate message found ' + str(e.args))
+ return
+
+ result = decrypt(key, iv, payload).decode('utf-8')
+
+ log.debug("Decrypted Message => " + result)
+ return result
+
+ def create_msg(self, from_jid, jid, plaintext):
+ key = get_random_bytes(16)
+ iv = get_random_bytes(16)
+ encrypted_keys = {}
+
+ devices_list = self.device_list_for(jid)
+ if len(devices_list) == 0:
+ 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
+
+ # Encrypt the message key with for each of receivers devices
+ for rid, cipher in session_ciphers.items():
+ try:
+ if self.isTrusted(cipher) == TRUSTED:
+ encrypted_keys[rid] = cipher.encrypt(key).serialize()
+ else:
+ log.debug('Skipped Device because Trust is: ' +
+ str(self.isTrusted(cipher)))
+ except:
+ log.warning('Failed to find key for device ' + str(rid))
+
+ 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(cipher) == TRUSTED:
+ encrypted_keys[dev] = cipher.encrypt(key).serialize()
+ else:
+ log.debug('Skipped own Device because Trust is: ' +
+ str(self.isTrusted(cipher)))
+ except:
+ log.warning('Failed to find key for device ' + str(dev))
+
+ payload = encrypt(key, iv, plaintext)
+
+ result = {'sid': self.own_device_id,
+ 'keys': encrypted_keys,
+ 'jid': jid,
+ 'iv': iv,
+ 'payload': payload}
+
+ 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 getTrustedFingerprints(self, recipient_id):
+ inactive = self.store.getInactiveSessionsKeys(recipient_id)
+ trusted = self.store.getTrustedFingerprints(recipient_id)
+ trusted = set(trusted) - set(inactive)
+
+ return trusted
+
+ def getUndecidedFingerprints(self, recipient_id):
+ inactive = self.store.getInactiveSessionsKeys(recipient_id)
+ undecided = self.store.getUndecidedFingerprints(recipient_id)
+ undecided = set(undecided) - set(inactive)
+
+ 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.
+
+ Parameters
+ ----------
+ jid : string
+ The contacts jid
+
+ Returns
+ -------
+ [int]
+ A list of device_ids
+ """
+ known_devices = self.device_list_for(jid)
+ missing_devices = [dev
+ for dev in known_devices
+ if not self.store.containsSession(jid, dev)]
+ if missing_devices:
+ log.info(self.account + ' => Missing device sessions for ' +
+ jid + ': ' + str(missing_devices))
+ return missing_devices
+
+ def get_session_cipher(self, jid, device_id):
+ if jid not in self.session_ciphers:
+ self.session_ciphers[jid] = {}
+
+ if device_id not in self.session_ciphers[jid]:
+ cipher = SessionCipher(self.store, self.store, self.store,
+ self.store, jid, device_id)
+ self.session_ciphers[jid][device_id] = cipher
+
+ return self.session_ciphers[jid][device_id]
+
+ def handlePreKeyWhisperMessage(self, recipient_id, device_id, key):
+ preKeyWhisperMessage = PreKeyWhisperMessage(serialized=key)
+ if not preKeyWhisperMessage.getPreKeyId():
+ raise Exception("Received PreKeyWhisperMessage without PreKey =>" +
+ recipient_id)
+ sessionCipher = self.get_session_cipher(recipient_id, device_id)
+ try:
+ log.debug(self.account +
+ " => Received PreKeyWhisperMessage from " +
+ recipient_id)
+ key = sessionCipher.decryptPkmsg(preKeyWhisperMessage)
+ # Publish new bundle after PreKey has been used
+ # for building a new Session
+ self.plugin.publish_bundle(self.account)
+ return key
+ except UntrustedIdentityException as e:
+ log.info(self.account + " => Received WhisperMessage " +
+ "from Untrusted Fingerprint! => " + e.getName())
+
+ 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:
+ key = sessionCipher.decryptMsg(whisperMessage, textMsg=False)
+ return key
+ else:
+ raise Exception("Received WhisperMessage "
+ "from Untrusted Fingerprint! => " + recipient_id)
+
+ def checkPreKeyAmount(self):
+ # Check if enough PreKeys are available
+ preKeyCount = self.store.preKeyStore.getPreKeyCount()
+ if preKeyCount < MIN_PREKEY_AMOUNT:
+ newKeys = DEFAULT_PREKEY_AMOUNT - preKeyCount
+ self.store.preKeyStore.generateNewPreKeys(newKeys)
+ log.info(self.account + ' => ' + str(newKeys) +
+ ' PreKeys created')
+
+ def cycleSignedPreKey(self, identityKeyPair):
+ # Publish every SPK_CYCLE_TIME a new SignedPreKey
+ # Delete all exsiting SignedPreKeys that are older
+ # then SPK_ARCHIVE_TIME
+
+ # Check if SignedPreKey exist and create if not
+ if not self.store.getCurrentSignedPreKeyId():
+ signedPreKey = KeyHelper.generateSignedPreKey(
+ identityKeyPair, self.store.getNextSignedPreKeyId())
+ self.store.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
+ log.debug(self.account +
+ ' => New SignedPreKey created, because none existed')
+
+ # if SPK_CYCLE_TIME is reached, generate a new SignedPreKey
+ now = int(time.time())
+ timestamp = self.store.getSignedPreKeyTimestamp(
+ self.store.getCurrentSignedPreKeyId())
+
+ if int(timestamp) < now - SPK_CYCLE_TIME:
+ signedPreKey = KeyHelper.generateSignedPreKey(
+ identityKeyPair, self.store.getNextSignedPreKeyId())
+ self.store.storeSignedPreKey(signedPreKey.getId(), signedPreKey)
+ log.debug(self.account + ' => Cycled SignedPreKey')
+
+ # Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
+ timestamp = now - SPK_ARCHIVE_TIME
+ self.store.removeOldSignedPreKeys(timestamp)
diff --git a/omemo/omemo16x16.png b/omemo/omemo16x16.png
new file mode 100644
index 0000000..aa97d6e
--- /dev/null
+++ b/omemo/omemo16x16.png
Binary files differ
diff --git a/omemo/pkgs/PKGBUILD b/omemo/pkgs/PKGBUILD
new file mode 100644
index 0000000..21ce483
--- /dev/null
+++ b/omemo/pkgs/PKGBUILD
@@ -0,0 +1,24 @@
+# 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:
diff --git a/omemo/setup.cfg b/omemo/setup.cfg
new file mode 100644
index 0000000..2cd96cc
--- /dev/null
+++ b/omemo/setup.cfg
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length=80
diff --git a/omemo/ui.py b/omemo/ui.py
new file mode 100644
index 0000000..e81813a
--- /dev/null
+++ b/omemo/ui.py
@@ -0,0 +1,619 @@
+# -*- 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 binascii
+import logging
+
+from gi.repository import GObject
+from gi.repository import Gtk
+
+# pylint: disable=import-error
+import gtkgui_helpers
+from common import gajim
+from plugins.gui import GajimPluginConfigDialog
+# pylint: enable=import-error
+
+log = logging.getLogger('gajim.plugin_system.omemo')
+
+UNDECIDED = 2
+TRUSTED = 1
+UNTRUSTED = 0
+
+
+class OmemoButton(Gtk.Button):
+ def __init__(self, plugin, chat_control, ui, enabled):
+ super(OmemoButton, self).__init__(label=None, stock=None)
+ self.chat_control = chat_control
+
+ self.set_property('relief', Gtk.ReliefStyle.NONE)
+ self.set_property('can-focus', False)
+ self.set_sensitive(True)
+
+ icon = Gtk.Image.new_from_file(
+ plugin.local_file_path('omemo16x16.png'))
+ self.set_image(icon)
+ self.set_tooltip_text('OMEMO Encryption')
+
+ self.connect('clicked', self.on_click)
+
+ self.menu = OmemoMenu(ui, enabled)
+
+ def on_click(self, widget):
+ """
+ Popup omemo menu
+ """
+ gtkgui_helpers.popup_emoticons_under_button(
+ self.menu, widget, self.chat_control.parent_win)
+
+ def set_omemo_state(self, state):
+ self.menu.set_omemo_state(state)
+
+
+class OmemoMenu(Gtk.Menu):
+ def __init__(self, ui, enabled):
+ super(OmemoMenu, self).__init__()
+ self.ui = ui
+
+ self.item_omemo_state = Gtk.CheckMenuItem('Activate OMEMO')
+ self.item_omemo_state.set_active(enabled)
+ self.item_omemo_state.connect('activate', self.on_toggle_omemo)
+ self.append(self.item_omemo_state)
+
+ item = Gtk.ImageMenuItem('Fingerprints')
+ icon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION,
+ Gtk.IconSize.MENU)
+ item.set_image(icon)
+ item.connect('activate', self.on_open_fingerprint_window)
+ self.append(item)
+
+ self.show_all()
+
+ def on_toggle_omemo(self, widget):
+ self.ui.set_omemo_state(widget.get_active())
+
+ def on_open_fingerprint_window(self, widget):
+ self.ui.show_fingerprint_window()
+
+ def set_omemo_state(self, state):
+ self.item_omemo_state.handler_block_by_func(self.on_toggle_omemo)
+ self.item_omemo_state.set_active(state)
+ self.item_omemo_state.handler_unblock_by_func(self.on_toggle_omemo)
+
+
+class Ui(object):
+ def __init__(self, plugin, chat_control, enabled, state):
+ self.contact = chat_control.contact
+ self.chat_control = chat_control
+ self.plugin = plugin
+ self.state = state
+ self.account = self.contact.account.name
+ self.windowinstances = {}
+
+ self.display_omemo_state()
+ self.refresh_auth_lock_icon()
+
+ self.omemobutton = OmemoButton(plugin, chat_control, self, enabled)
+
+ self.actions_hbox = chat_control.xml.get_object('actions_hbox')
+ send_button = chat_control.xml.get_object('send_button')
+ send_button_pos = self.actions_hbox.child_get_property(send_button,
+ 'position')
+ self.actions_hbox.add(self.omemobutton)
+ self.actions_hbox.reorder_child(self.omemobutton, send_button_pos - 1)
+ self.omemobutton.show_all()
+
+ # add a OMEMO entry to the context/advanced menu
+ self.chat_control.omemo_orig_prepare_context_menu = \
+ self.chat_control.prepare_context_menu
+
+ def omemo_prepare_context_menu(hide_buttonbar_items=False):
+ menu = self.chat_control. \
+ omemo_orig_prepare_context_menu(hide_buttonbar_items)
+ submenu = OmemoMenu(self, self.encryption_active())
+
+ item = Gtk.ImageMenuItem('OMEMO Encryption')
+ icon_path = plugin.local_file_path('omemo16x16.png')
+ item.set_image(Gtk.Image.new_from_file(icon_path))
+ item.set_submenu(submenu)
+
+ # at index 8 is the separator after the esession encryption entry
+ menu.insert(item, 8)
+ return menu
+ self.chat_control.prepare_context_menu = omemo_prepare_context_menu
+
+ # Hook into Send Button so we can check Stuff before sending
+ self.chat_control.orig_send_message = \
+ self.chat_control.send_message
+
+ def omemo_send_message(message, keyID='', chatstate=None, xhtml=None,
+ process_commands=True, attention=False):
+ self.new_fingerprints_available()
+ if self.encryption_active() and \
+ self.plugin.are_keys_missing(self.account,
+ self.contact.jid):
+
+ log.debug(self.account + ' => No Trusted Fingerprints for ' +
+ self.contact.jid)
+ self.no_trusted_fingerprints_warning()
+ else:
+ self.chat_control.orig_send_message(message, keyID, chatstate,
+ xhtml, process_commands,
+ attention)
+ log.debug(self.account + ' => Sending Message to ' +
+ self.contact.jid)
+
+ self.chat_control.send_message = omemo_send_message
+
+ def set_omemo_state(self, enabled):
+ """
+ Enable or disable OMEMO for this window's contact and update the
+ window ui accordingly
+ """
+ if enabled:
+ log.debug(self.contact.account.name + ' => Enable OMEMO for ' +
+ self.contact.jid)
+ self.plugin.omemo_enable_for(self.contact.jid,
+ self.contact.account.name)
+ self.refresh_auth_lock_icon()
+ else:
+ log.debug(self.contact.account.name + ' => Disable OMEMO for ' +
+ self.contact.jid)
+ self.plugin.omemo_disable_for(self.contact.jid,
+ self.contact.account.name)
+ self.refresh_auth_lock_icon()
+
+ self.omemobutton.set_omemo_state(enabled)
+ self.display_omemo_state()
+
+ def encryption_active(self):
+ return self.state.encryption.is_active(self.contact.jid)
+
+ def activate_omemo(self):
+ if not self.encryption_active():
+ 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)
+
+ 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)
+ self.windowinstances['dialog'].show_all()
+ if fingerprints:
+ log.debug(self.account +
+ ' => Showing Fingerprint Prompt for ' +
+ self.contact.jid)
+ self.state.store.setShownFingerprints(fingerprints)
+ else:
+ self.windowinstances['dialog'].update_context_list()
+ if fingerprints:
+ self.state.store.setShownFingerprints(fingerprints)
+
+ def plain_warning(self):
+ self.chat_control.print_conversation_line(
+ 'Received plaintext message! ' +
+ 'Your next message will still be encrypted!', 'status', '', None)
+
+ def display_omemo_state(self):
+ if self.encryption_active():
+ msg = u'OMEMO encryption enabled'
+ else:
+ msg = u'OMEMO encryption disabled'
+ self.chat_control.print_conversation_line(msg, 'status', '', None)
+
+ def no_trusted_fingerprints_warning(self):
+ msg = "To send an encrypted message, you have to " \
+ "first trust the fingerprint of your contact!"
+ self.chat_control.print_conversation_line(msg, 'status', '', None)
+
+ def refresh_auth_lock_icon(self):
+ if self.encryption_active():
+ if self.state.getUndecidedFingerprints(self.contact.jid):
+ self.chat_control._show_lock_image(True, 'OMEMO', True, True,
+ False)
+ else:
+ self.chat_control._show_lock_image(True, 'OMEMO', True, True,
+ True)
+ else:
+ self.chat_control._show_lock_image(False, 'OMEMO', False, True,
+ False)
+
+ def removeUi(self):
+ self.actions_hbox.remove(self.omemobutton)
+ 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
+
+
+class OMEMOConfigDialog(GajimPluginConfigDialog):
+ def init(self):
+ # pylint: disable=attribute-defined-outside-init
+ self.GTK_BUILDER_FILE_PATH = \
+ self.plugin.local_file_path('config_dialog.ui')
+ self.B = Gtk.Builder()
+ 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)
+
+ self.device_model = Gtk.ListStore(GObject.TYPE_INT)
+
+ 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_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)
+
+ if len(self.account_store) > 0:
+ self.B.get_object('account_combobox').set_active(0)
+
+ vbox = self.get_content_area()
+ vbox.pack_start(self.B.get_object('notebook1'), True, True, 0)
+
+ self.B.connect_signals(self)
+
+ def on_run(self):
+ self.update_context_list()
+ self.account_combobox_changed_cb(self.B.get_object('account_combobox'))
+
+ def account_combobox_changed_cb(self, box, *args):
+ self.update_context_list()
+
+ def trust_button_clicked_cb(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()
+
+ 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, 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()
+
+ self.update_context_list()
+
+ def cleardevice_button_clicked_cb(self, button, *args):
+ active = self.B.get_object('account_combobox').get_active()
+ account = self.account_store[active][0]
+ self.plugin.clear_device_list(account)
+ self.update_context_list()
+
+ def refresh_button_clicked_cb(self, button, *args):
+ self.update_context_list()
+
+ def fpr_button_pressed_cb(self, tw, event):
+ if event.button == 3:
+ pthinfo = tw.get_path_at_pos(int(event.x), int(event.y))
+
+ if pthinfo is None:
+ # only show the popup when we right clicked on list content
+ # ie. don't show it when we click at empty rows
+ return False
+
+ # if the row under the mouse is already selected, we keep the
+ # 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.popup(None, None, None, event.button, event.time)
+
+ # keep_selection=True -> no further processing of click event
+ # keep_selection=False-> further processing -> GTK usually selects
+ # the item below the cursor
+ return keep_selection
+
+ def clipboard_button_cb(self, menuitem):
+ mod, paths = self.fpr_view.get_selection().get_selected_rows()
+
+ fprs = []
+ for path in paths:
+ it = mod.get_iter(path)
+ jid, fpr = mod.get(it, 1, 3)
+ fprs.append('%s: %s' % (jid, fpr[4:-5]))
+ Gtk.Clipboard().set_text('\n'.join(fprs))
+ Gtk.Clipboard(selection='PRIMARY').set_text('\n'.join(fprs))
+
+ def update_context_list(self):
+ self.fpr_model.clear()
+ self.device_model.clear()
+ active = self.B.get_object('account_combobox').get_active()
+ account = self.account_store[active][0]
+ 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:])
+ self.B.get_object('fingerprint_label').set_markup('<tt>%s</tt>'
+ % 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))
+
+ 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()
+
+
+class FingerprintWindow(Gtk.Dialog):
+ def __init__(self, plugin, contact, parent, windowinstances):
+ self.contact = contact
+ self.windowinstances = windowinstances
+ Gtk.Dialog.__init__(self,
+ title=('Fingerprints for %s') % contact.jid,
+ parent=parent,
+ flags=Gtk.DialogFlags.DESTROY_WITH_PARENT)
+ 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.fpr_model = Gtk.ListStore(GObject.TYPE_INT,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING)
+
+ 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')
+
+ 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)
+
+ 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)
+
+ self.update_context_list()
+
+ def on_close_button_clicked(self, widget):
+ del self.windowinstances['dialog']
+ self.hide()
+
+ def on_window_delete(self, widget, event):
+ del self.windowinstances['dialog']
+ self.hide()
+
+ def trust_button_clicked_cb(self, button, *args):
+ 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)
+ 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()
+
+ def fpr_button_pressed_cb(self, tw, event):
+ if event.button == 3:
+ pthinfo = tw.get_path_at_pos(int(event.x), int(event.y))
+
+ if pthinfo is None:
+ # only show the popup when we right clicked on list content
+ # ie. don't show it when we click at empty rows
+ return False
+
+ # if the row under the mouse is already selected, we keep the
+ # 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.popup(None, None, None, event.button, event.time)
+
+ # keep_selection=True -> no further processing of click event
+ # keep_selection=False-> further processing -> GTK usually selects
+ # the item below the cursor
+ return keep_selection
+
+ def clipboard_button_cb(self, menuitem):
+ 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()
+
+ fprs = []
+ for path in paths:
+ it = mod.get_iter(path)
+ jid, fpr = mod.get(it, 1, 3)
+ fprs.append('%s: %s' % (jid, fpr[31:-12]))
+ Gtk.Clipboard().set_text('\n'.join(fprs))
+ Gtk.Clipboard(selection='PRIMARY').set_text('\n'.join(fprs))
+
+ def update_context_list(self, *args):
+ self.fpr_model.clear()
+
+ if self.notebook.get_current_page() == 1:
+ jid = gajim.get_jid_from_account(self.account)
+ else:
+ jid = self.contact.jid
+
+ fprDB = self.omemostate.store.identityKeyStore.getFingerprints(jid)
+ activeSessions = self.omemostate.store.sessionStore. \
+ getActiveSessionsKeys(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))
+
+ 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()
diff --git a/omemo/xmpp.py b/omemo/xmpp.py
new file mode 100644
index 0000000..c4097a5
--- /dev/null
+++ b/omemo/xmpp.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.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/>.
+#
+
+""" This module handles all the XMPP logic like creating different kind of
+stanza nodes and geting data from stanzas.
+"""
+
+import logging
+import random
+from base64 import b64decode, b64encode
+
+from nbxmpp.protocol import NS_PUBSUB, Iq
+from nbxmpp.simplexml import Node
+
+from common import gajim # pylint: disable=import-error
+from common.pep import AbstractPEP # pylint: disable=import-error
+from plugins.helpers import log_calls # pylint: disable=import-error
+
+NS_PUBSUB_EVENT = NS_PUBSUB + '#event'
+
+NS_EME = 'urn:xmpp:eme:0'
+NS_OMEMO = 'eu.siacs.conversations.axolotl'
+NS_DEVICE_LIST = NS_OMEMO + '.devicelist'
+NS_NOTIFY = NS_DEVICE_LIST + '+notify'
+NS_BUNDLES = NS_OMEMO + '.bundles:'
+log = logging.getLogger('gajim.plugin_system.omemo')
+
+
+class PublishNode(Node):
+ def __init__(self, node_str, data):
+ assert node_str is not None and isinstance(data, Node)
+ Node.__init__(self, tag='publish', attrs={'node': node_str})
+ self.addChild('item').addChild(node=data)
+
+
+class PubsubNode(Node):
+ def __init__(self, data):
+ assert isinstance(data, Node)
+ Node.__init__(self, tag='pubsub', attrs={'xmlns': NS_PUBSUB})
+ self.addChild(node=data)
+
+
+class DeviceListAnnouncement(Iq):
+ def __init__(self, device_list):
+ assert isinstance(device_list, list)
+ assert len(device_list) > 0
+ id_ = gajim.get_an_id()
+ attrs = {'id': id_}
+ Iq.__init__(self, typ='set', attrs=attrs)
+
+ list_node = Node('list', attrs={'xmlns': NS_OMEMO})
+ for device in device_list:
+ list_node.addChild('device').setAttr('id', device)
+
+ publish = PublishNode(NS_DEVICE_LIST, list_node)
+ pubsub = PubsubNode(publish)
+
+ self.addChild(node=pubsub)
+
+
+class OmemoMessage(Node):
+ def __init__(self, msg_dict):
+ # , 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'))
+
+ header.addChild('iv').addData(b64encode(msg_dict['iv']).decode('utf-8'))
+ self.addChild(node=header)
+ self.addChild('payload').addData(b64encode(msg_dict['payload'])
+ .decode('utf-8'))
+
+
+class BundleInformationQuery(Iq):
+ def __init__(self, contact_jid, device_id):
+ assert isinstance(device_id, int)
+ id_ = gajim.get_an_id()
+ attrs = {'id': id_}
+ Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid)
+ items = Node('items', attrs={'node': NS_BUNDLES + str(device_id)})
+ pubsub = PubsubNode(items)
+ self.addChild(node=pubsub)
+
+
+class BundleInformationAnnouncement(Iq):
+ def __init__(self, state_bundle, device_id):
+ id_ = gajim.get_an_id()
+ attrs = {'id': id_}
+ Iq.__init__(self, typ='set', attrs=attrs)
+ bundle_node = self.make_bundle_node(state_bundle)
+ publish = PublishNode(NS_BUNDLES + str(device_id), bundle_node)
+ pubsub = PubsubNode(publish)
+ self.addChild(node=pubsub)
+
+ def make_bundle_node(self, state_bundle):
+ result = Node('bundle', attrs={'xmlns': NS_OMEMO})
+ prekey_pub_node = result.addChild(
+ 'signedPreKeyPublic',
+ attrs={'signedPreKeyId': state_bundle['signedPreKeyId']})
+ prekey_pub_node.addData(state_bundle['signedPreKeyPublic']
+ .decode('utf-8'))
+
+ prekey_sig_node = result.addChild('signedPreKeySignature')
+ prekey_sig_node.addData(state_bundle['signedPreKeySignature']
+ .decode('utf-8'))
+
+ identity_key_node = result.addChild('identityKey')
+ identity_key_node.addData(state_bundle['identityKey'].decode('utf-8'))
+ prekeys = result.addChild('prekeys')
+
+ for key in state_bundle['prekeys']:
+ prekeys.addChild('preKeyPublic',
+ attrs={'preKeyId': key[0]}) \
+ .addData(key[1].decode('utf-8'))
+ return result
+
+
+class DevicelistQuery(Iq):
+ def __init__(self, contact_jid,):
+ id_ = gajim.get_an_id()
+ attrs = {'id': id_}
+ Iq.__init__(self, typ='get', attrs=attrs, to=contact_jid)
+ items = Node('items', attrs={'node': NS_DEVICE_LIST})
+ pubsub = PubsubNode(items)
+ self.addChild(node=pubsub)
+
+
+class DevicelistPEP(AbstractPEP):
+ type_ = 'headline'
+ namespace = NS_DEVICE_LIST
+
+ def _extract_info(self, items):
+ return ({}, [])
+
+
+@log_calls('OmemoPlugin')
+def unpack_device_bundle(bundle, device_id):
+ pubsub = bundle.getTag('pubsub', namespace=NS_PUBSUB)
+ if not pubsub:
+ log.warning('OMEMO device bundle has no pubsub node')
+ return
+ items = pubsub.getTag('items', attrs={'node': NS_BUNDLES + str(device_id)})
+ if not items:
+ log.warning('OMEMO device bundle has no items node')
+ return
+
+ item = items.getTag('item', namespace=NS_PUBSUB)
+ if not item:
+ log.warning('OMEMO device bundle has no item node')
+ return
+
+ bundle = item.getTag('bundle', namespace=NS_OMEMO)
+ if not bundle:
+ log.warning('OMEMO device bundle has no bundle node')
+ return
+
+ signed_prekey_node = bundle.getTag('signedPreKeyPublic',
+ namespace=NS_OMEMO)
+ if not signed_prekey_node:
+ log.warning('OMEMO device bundle has no signedPreKeyPublic node')
+ return
+
+ result = {}
+ result['signedPreKeyPublic'] = decode_data(signed_prekey_node)
+ if not result['signedPreKeyPublic']:
+ log.warning('OMEMO device bundle has no signedPreKeyPublic data')
+ return
+
+ if not signed_prekey_node.getAttr('signedPreKeyId'):
+ log.warning('OMEMO device bundle has no signedPreKeyId')
+ return
+ result['signedPreKeyId'] = int(signed_prekey_node.getAttr(
+ 'signedPreKeyId'))
+
+ signed_signature_node = bundle.getTag('signedPreKeySignature',
+ namespace=NS_OMEMO)
+ if not signed_signature_node:
+ log.warning('OMEMO device bundle has no signedPreKeySignature node')
+ return
+
+ result['signedPreKeySignature'] = decode_data(signed_signature_node)
+ if not result['signedPreKeySignature']:
+ log.warning('OMEMO device bundle has no signedPreKeySignature data')
+ return
+
+ identity_key_node = bundle.getTag('identityKey', namespace=NS_OMEMO)
+ if not identity_key_node:
+ log.warning('OMEMO device bundle has no identityKey node')
+ return
+
+ result['identityKey'] = decode_data(identity_key_node)
+ if not result['identityKey']:
+ log.warning('OMEMO device bundle has no identityKey data')
+ return
+
+ prekeys = bundle.getTag('prekeys', namespace=NS_OMEMO)
+ if not prekeys or len(prekeys.getChildren()) == 0:
+ log.warning('OMEMO device bundle has no prekys')
+ return
+
+ picked_key_node = random.SystemRandom().choice(prekeys.getChildren())
+
+ if not picked_key_node.getAttr('preKeyId'):
+ log.warning('OMEMO PreKey has no id set')
+ return
+ result['preKeyId'] = int(picked_key_node.getAttr('preKeyId'))
+
+ result['preKeyPublic'] = decode_data(picked_key_node)
+ if not result['preKeyPublic']:
+ return
+ return result
+
+
+@log_calls('OmemoPlugin')
+def unpack_encrypted(encrypted_node):
+ """ Unpacks the encrypted node, decodes the data and returns a msg_dict.
+ """
+ if not encrypted_node.getNamespace() == NS_OMEMO:
+ log.warning("Encrypted node with wrong NS")
+ return
+
+ header_node = encrypted_node.getTag('header', namespace=NS_OMEMO)
+ if not header_node:
+ log.warning("OMEMO message without header")
+ return
+
+ if not header_node.getAttr('sid'):
+ log.warning("OMEMO message without sid in header")
+ return
+
+ sid = int(header_node.getAttr('sid'))
+
+ iv_node = header_node.getTag('iv', namespace=NS_OMEMO)
+ if not iv_node:
+ log.warning("OMEMO message without iv")
+ return
+
+ iv = decode_data(iv_node)
+ if not iv:
+ log.warning("OMEMO message without iv data")
+
+ payload_node = encrypted_node.getTag('payload', namespace=NS_OMEMO)
+ payload = None
+ if payload_node:
+ payload = decode_data(payload_node)
+
+ key_nodes = header_node.getTags('key')
+ if len(key_nodes) < 1:
+ log.warning("OMEMO message without keys")
+ return
+
+ keys = {}
+ for kn in key_nodes:
+ rid = kn.getAttr('rid')
+ if not rid:
+ log.warning('Omemo key without rid')
+ continue
+
+ if not kn.getData():
+ log.warning('Omemo key without data')
+ continue
+
+ keys[int(rid)] = decode_data(kn)
+
+ result = {'sid': sid, 'iv': iv, 'keys': keys, 'payload': payload}
+ return result
+
+
+def unpack_device_list_update(stanza, account):
+ """ Unpacks the device list from a stanza
+
+ Parameters
+ ----------
+ stanza
+
+ Returns
+ -------
+ [int]
+ List of device ids or empty list if nothing found
+ """
+ event_node = stanza.getTag('event', namespace=NS_PUBSUB_EVENT)
+ if not event_node:
+ event_node = stanza.getTag('pubsub', namespace=NS_PUBSUB)
+ result = []
+
+ if not event_node:
+ log.warning(account + ' => Device list update event node empty!')
+ return result
+
+ items = event_node.getTag('items', {'node': NS_DEVICE_LIST})
+ if not items or len(items.getChildren()) != 1:
+ log.debug(
+ account +
+ ' => Device list update items node empty or not omemo device update')
+ return result
+
+ list_node = items.getChildren()[0].getTag('list')
+ if not list_node or len(list_node.getChildren()) == 0:
+ log.warning(account + ' => Device list update list node empty!')
+ return result
+
+ devices_nodes = list_node.getChildren()
+
+ for dn in devices_nodes:
+ _id = dn.getAttr('id')
+ if _id:
+ result.append(int(_id))
+
+ return result
+
+
+def decode_data(node):
+ """ Fetch the data from specified node and b64decode it. """
+ data = node.getData()
+
+ if not data:
+ log.warning("No node data")
+ return
+ try:
+ return b64decode(data)
+ except:
+ log.warning('b64decode broken')
+ return
+
+
+def successful(stanza):
+ """ Check if stanza type is result. """
+ return stanza.getAttr('type') == 'result'