[omemo] Remove plugin
It has been integrated into Gajim
This commit is contained in:
674
omemo/COPYING
674
omemo/COPYING
@@ -1,674 +0,0 @@
|
||||
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>.
|
||||
@@ -1 +0,0 @@
|
||||
from omemo.plugin import OmemoPlugin
|
||||
@@ -1 +0,0 @@
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,95 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import os
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
from cryptography.hazmat.primitives.ciphers.modes import GCM
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
log = logging.getLogger('gajim.p.omemo')
|
||||
|
||||
EncryptionResult = namedtuple('EncryptionResult', 'payload key iv')
|
||||
|
||||
IV_SIZE = 12
|
||||
|
||||
def _decrypt(key, iv, tag, data):
|
||||
decryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
GCM(iv, tag=tag),
|
||||
backend=default_backend()).decryptor()
|
||||
return decryptor.update(data) + decryptor.finalize()
|
||||
|
||||
|
||||
def aes_decrypt(_key, iv, payload):
|
||||
if len(_key) >= 32:
|
||||
# XEP-0384
|
||||
log.debug('XEP Compliant Key/Tag')
|
||||
data = payload
|
||||
key = _key[:16]
|
||||
tag = _key[16:]
|
||||
else:
|
||||
# Legacy
|
||||
log.debug('Legacy Key/Tag')
|
||||
data = payload[:-16]
|
||||
key = _key
|
||||
tag = payload[-16:]
|
||||
|
||||
return _decrypt(key, iv, tag, data).decode()
|
||||
|
||||
|
||||
def aes_decrypt_file(key, iv, payload):
|
||||
data = payload[:-16]
|
||||
tag = payload[-16:]
|
||||
return _decrypt(key, iv, tag, data)
|
||||
|
||||
|
||||
def _encrypt(data, key_size, iv_size=IV_SIZE):
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
key = os.urandom(key_size)
|
||||
iv = os.urandom(iv_size)
|
||||
encryptor = Cipher(
|
||||
algorithms.AES(key),
|
||||
GCM(iv),
|
||||
backend=default_backend()).encryptor()
|
||||
|
||||
payload = encryptor.update(data) + encryptor.finalize()
|
||||
return key, iv, encryptor.tag, payload
|
||||
|
||||
|
||||
def aes_encrypt(plaintext):
|
||||
key, iv, tag, payload = _encrypt(plaintext, 16)
|
||||
key += tag
|
||||
return EncryptionResult(payload=payload, key=key, iv=iv)
|
||||
|
||||
|
||||
def aes_encrypt_file(data):
|
||||
key, iv, tag, payload, = _encrypt(data, 32)
|
||||
payload += tag
|
||||
return EncryptionResult(payload=payload, key=key, iv=iv)
|
||||
|
||||
|
||||
def get_new_key():
|
||||
return os.urandom(16)
|
||||
|
||||
|
||||
def get_new_iv():
|
||||
return os.urandom(IV_SIZE)
|
||||
@@ -1,138 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
from omemo.backend.util import UNACKNOWLEDGED_COUNT
|
||||
|
||||
|
||||
class DeviceManager:
|
||||
def __init__(self):
|
||||
self.__device_store = defaultdict(set)
|
||||
self.__muc_member_store = defaultdict(set)
|
||||
|
||||
reg_id = self._storage.getLocalRegistrationId()
|
||||
if reg_id is None:
|
||||
raise ValueError('No own device found')
|
||||
|
||||
self.__own_device = reg_id
|
||||
self.add_device(self._own_jid, self.__own_device)
|
||||
self._log.info('Our device id: %s', self.__own_device)
|
||||
|
||||
for jid, device in self._storage.getActiveDeviceTuples():
|
||||
self._log.info('Load device from storage: %s - %s', jid, device)
|
||||
self.add_device(jid, device)
|
||||
|
||||
def update_devicelist(self, jid, devicelist):
|
||||
for device in list(devicelist):
|
||||
if device == self.own_device:
|
||||
continue
|
||||
count = self._storage.getUnacknowledgedCount(jid, device)
|
||||
if count > UNACKNOWLEDGED_COUNT:
|
||||
self._log.warning('Ignore device because of %s unacknowledged'
|
||||
' messages: %s %s', count, jid, device)
|
||||
devicelist.remove(device)
|
||||
|
||||
self.__device_store[jid] = set(devicelist)
|
||||
self._log.info('Saved devices for %s', jid)
|
||||
self._storage.setActiveState(jid, devicelist)
|
||||
|
||||
def add_muc_member(self, room_jid, jid):
|
||||
self._log.info('Saved MUC member %s %s', room_jid, jid)
|
||||
self.__muc_member_store[room_jid].add(jid)
|
||||
|
||||
def remove_muc_member(self, room_jid, jid):
|
||||
self._log.info('Removed MUC member %s %s', room_jid, jid)
|
||||
self.__muc_member_store[room_jid].discard(jid)
|
||||
|
||||
def get_muc_members(self, room_jid, without_self=True):
|
||||
members = set(self.__muc_member_store[room_jid])
|
||||
if without_self:
|
||||
members.discard(self._own_jid)
|
||||
return members
|
||||
|
||||
def add_device(self, jid, device):
|
||||
self.__device_store[jid].add(device)
|
||||
|
||||
def remove_device(self, jid, device):
|
||||
self.__device_store[jid].discard(device)
|
||||
self._storage.setInactive(jid, device)
|
||||
|
||||
def get_devices(self, jid, without_self=False):
|
||||
devices = set(self.__device_store[jid])
|
||||
if without_self:
|
||||
devices.discard(self.own_device)
|
||||
return devices
|
||||
|
||||
def get_devices_for_encryption(self, jid):
|
||||
devices_for_encryption = []
|
||||
|
||||
client = app.get_client(self._account)
|
||||
contact = client.get_module('Contacts').get_contact(jid)
|
||||
if contact.is_groupchat:
|
||||
devices_for_encryption = self._get_devices_for_muc_encryption(jid)
|
||||
else:
|
||||
devices_for_encryption = self._get_devices_for_encryption(jid)
|
||||
|
||||
if not devices_for_encryption:
|
||||
raise NoDevicesFound
|
||||
|
||||
devices_for_encryption += self._get_own_devices_for_encryption()
|
||||
return set(devices_for_encryption)
|
||||
|
||||
def _get_devices_for_muc_encryption(self, jid):
|
||||
devices_for_encryption = []
|
||||
for jid_ in self.__muc_member_store[jid]:
|
||||
devices_for_encryption += self._get_devices_for_encryption(jid_)
|
||||
return devices_for_encryption
|
||||
|
||||
def _get_own_devices_for_encryption(self):
|
||||
devices_for_encryption = []
|
||||
own_devices = self.get_devices(self._own_jid, without_self=True)
|
||||
for device in own_devices:
|
||||
if self._storage.isTrusted(self._own_jid, device):
|
||||
devices_for_encryption.append((self._own_jid, device))
|
||||
return devices_for_encryption
|
||||
|
||||
def _get_devices_for_encryption(self, jid):
|
||||
devices_for_encryption = []
|
||||
devices = self.get_devices(jid)
|
||||
|
||||
for device in devices:
|
||||
if self._storage.isTrusted(jid, device):
|
||||
devices_for_encryption.append((jid, device))
|
||||
return devices_for_encryption
|
||||
|
||||
@property
|
||||
def own_device(self):
|
||||
return self.__own_device
|
||||
|
||||
@property
|
||||
def devices_for_publish(self):
|
||||
devices = self.get_devices(self._own_jid)
|
||||
if self.own_device not in devices:
|
||||
devices.add(self.own_device)
|
||||
return devices
|
||||
|
||||
@property
|
||||
def is_own_device_published(self):
|
||||
return self.own_device in self.get_devices(self._own_jid)
|
||||
|
||||
|
||||
class NoDevicesFound(Exception):
|
||||
pass
|
||||
@@ -1,741 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
# Copyright (C) 2015 Tarek Galal <tare2.galal@gmail.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
import sqlite3
|
||||
from collections import namedtuple
|
||||
|
||||
from axolotl.state.axolotlstore import AxolotlStore
|
||||
from axolotl.state.signedprekeyrecord import SignedPreKeyRecord
|
||||
from axolotl.state.sessionrecord import SessionRecord
|
||||
from axolotl.state.prekeyrecord import PreKeyRecord
|
||||
from axolotl.invalidkeyidexception import InvalidKeyIdException
|
||||
from axolotl.ecc.djbec import DjbECPrivateKey
|
||||
from axolotl.ecc.djbec import DjbECPublicKey
|
||||
from axolotl.identitykeypair import IdentityKeyPair
|
||||
from axolotl.util.medium import Medium
|
||||
from axolotl.util.keyhelper import KeyHelper
|
||||
|
||||
from gajim.common import app
|
||||
|
||||
from omemo.backend.util import Trust
|
||||
from omemo.backend.util import IdentityKeyExtended
|
||||
from omemo.backend.util import DEFAULT_PREKEY_AMOUNT
|
||||
|
||||
|
||||
def _convert_to_string(text):
|
||||
return text.decode()
|
||||
|
||||
|
||||
def _convert_identity_key(key):
|
||||
if not key:
|
||||
return
|
||||
return IdentityKeyExtended(DjbECPublicKey(key[1:]))
|
||||
|
||||
|
||||
def _convert_record(record):
|
||||
return SessionRecord(serialized=record)
|
||||
|
||||
|
||||
sqlite3.register_converter('pk', _convert_identity_key)
|
||||
sqlite3.register_converter('session_record', _convert_record)
|
||||
|
||||
|
||||
class LiteAxolotlStore(AxolotlStore):
|
||||
def __init__(self, db_path, log):
|
||||
self._log = log
|
||||
self._con = sqlite3.connect(db_path,
|
||||
detect_types=sqlite3.PARSE_COLNAMES)
|
||||
self._con.row_factory = self._namedtuple_factory
|
||||
self.createDb()
|
||||
self.migrateDb()
|
||||
|
||||
self._con.execute("PRAGMA secure_delete=1")
|
||||
self._con.execute("PRAGMA synchronous=NORMAL;")
|
||||
mode = self._con.execute("PRAGMA journal_mode;").fetchone()[0]
|
||||
|
||||
# WAL is a persistent DB mode, don't override it if user has set it
|
||||
if mode != 'wal':
|
||||
self._con.execute("PRAGMA journal_mode=MEMORY;")
|
||||
self._con.commit()
|
||||
|
||||
if not self.getLocalRegistrationId():
|
||||
self._log.info("Generating OMEMO keys")
|
||||
self._generate_axolotl_keys()
|
||||
|
||||
@staticmethod
|
||||
def _is_blind_trust_enabled():
|
||||
plugin = app.plugin_manager.get_active_plugin('omemo')
|
||||
return plugin.config['BLIND_TRUST']
|
||||
|
||||
@staticmethod
|
||||
def _namedtuple_factory(cursor, row):
|
||||
fields = []
|
||||
for col in cursor.description:
|
||||
if col[0] == '_id':
|
||||
fields.append('id')
|
||||
elif 'strftime' in col[0]:
|
||||
fields.append('formated_time')
|
||||
elif 'MAX' in col[0] or 'COUNT' in col[0]:
|
||||
col_name = col[0].replace('(', '_')
|
||||
col_name = col_name.replace(')', '')
|
||||
fields.append(col_name.lower())
|
||||
else:
|
||||
fields.append(col[0])
|
||||
return namedtuple("Row", fields)(*row)
|
||||
|
||||
def _generate_axolotl_keys(self):
|
||||
identity_key_pair = KeyHelper.generateIdentityKeyPair()
|
||||
registration_id = KeyHelper.getRandomSequence(2147483647)
|
||||
pre_keys = KeyHelper.generatePreKeys(
|
||||
KeyHelper.getRandomSequence(4294967296),
|
||||
DEFAULT_PREKEY_AMOUNT)
|
||||
self.storeLocalData(registration_id, identity_key_pair)
|
||||
|
||||
signed_pre_key = KeyHelper.generateSignedPreKey(
|
||||
identity_key_pair, KeyHelper.getRandomSequence(65536))
|
||||
|
||||
self.storeSignedPreKey(signed_pre_key.getId(), signed_pre_key)
|
||||
|
||||
for pre_key in pre_keys:
|
||||
self.storePreKey(pre_key.getId(), pre_key)
|
||||
|
||||
def user_version(self):
|
||||
return self._con.execute('PRAGMA user_version').fetchone()[0]
|
||||
|
||||
def createDb(self):
|
||||
if self.user_version() == 0:
|
||||
|
||||
create_tables = '''
|
||||
CREATE TABLE IF NOT EXISTS secret (
|
||||
device_id INTEGER, public_key BLOB, private_key BLOB);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS identities (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT, recipient_id TEXT,
|
||||
registration_id INTEGER, public_key BLOB,
|
||||
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_db_sql = """
|
||||
BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=12;
|
||||
END TRANSACTION;
|
||||
""" % (create_tables)
|
||||
self._con.executescript(create_db_sql)
|
||||
|
||||
def migrateDb(self):
|
||||
""" Migrates the DB
|
||||
"""
|
||||
|
||||
# Find all double entries and delete them
|
||||
if self.user_version() < 2:
|
||||
delete_dupes = """ DELETE FROM identities WHERE _id not in (
|
||||
SELECT MIN(_id)
|
||||
FROM identities
|
||||
GROUP BY
|
||||
recipient_id, public_key
|
||||
);
|
||||
"""
|
||||
|
||||
self._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=2;
|
||||
END TRANSACTION;
|
||||
""" % (delete_dupes))
|
||||
|
||||
if self.user_version() < 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._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=3;
|
||||
END TRANSACTION;
|
||||
""" % (add_index))
|
||||
|
||||
if self.user_version() < 4:
|
||||
# Adds column "active" to the sessions table
|
||||
add_active = """ ALTER TABLE sessions
|
||||
ADD COLUMN active INTEGER DEFAULT 1;
|
||||
"""
|
||||
|
||||
self._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=4;
|
||||
END TRANSACTION;
|
||||
""" % (add_active))
|
||||
|
||||
if self.user_version() < 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._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=5;
|
||||
END TRANSACTION;
|
||||
""" % (add_timestamp))
|
||||
|
||||
if self.user_version() < 6:
|
||||
# Move secret data into own table
|
||||
# We add +1 to registration id because we did that in other code in
|
||||
# earlier versions. On this migration we correct this mistake now.
|
||||
move = """
|
||||
CREATE TABLE IF NOT EXISTS secret (
|
||||
device_id INTEGER, public_key BLOB, private_key BLOB);
|
||||
INSERT INTO secret (device_id, public_key, private_key)
|
||||
SELECT registration_id + 1, public_key, private_key
|
||||
FROM identities
|
||||
WHERE recipient_id = -1;
|
||||
"""
|
||||
|
||||
self._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=6;
|
||||
END TRANSACTION;
|
||||
""" % move)
|
||||
|
||||
if self.user_version() < 7:
|
||||
# Convert old device ids to integer
|
||||
convert = """
|
||||
UPDATE secret SET device_id = device_id % 2147483646;
|
||||
"""
|
||||
|
||||
self._con.executescript(
|
||||
""" BEGIN TRANSACTION;
|
||||
%s
|
||||
PRAGMA user_version=7;
|
||||
END TRANSACTION;
|
||||
""" % convert)
|
||||
|
||||
if self.user_version() < 8:
|
||||
# Sanitize invalid BLOBs from the python2 days
|
||||
query_keys = '''SELECT recipient_id,
|
||||
registration_id,
|
||||
CAST(public_key as BLOB) as public_key,
|
||||
CAST(private_key as BLOB) as private_key,
|
||||
timestamp, trust, shown
|
||||
FROM identities'''
|
||||
rows = self._con.execute(query_keys).fetchall()
|
||||
|
||||
delete = 'DELETE FROM identities'
|
||||
self._con.execute(delete)
|
||||
|
||||
insert = '''INSERT INTO identities (
|
||||
recipient_id, registration_id, public_key, private_key,
|
||||
timestamp, trust, shown)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'''
|
||||
for row in rows:
|
||||
try:
|
||||
self._con.execute(insert, row)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
self._con.execute('PRAGMA user_version=8')
|
||||
self._con.commit()
|
||||
|
||||
if self.user_version() < 9:
|
||||
# Sanitize invalid BLOBs from the python2 days
|
||||
query_keys = '''SELECT device_id,
|
||||
CAST(public_key as BLOB) as public_key,
|
||||
CAST(private_key as BLOB) as private_key
|
||||
FROM secret'''
|
||||
rows = self._con.execute(query_keys).fetchall()
|
||||
|
||||
delete = 'DELETE FROM secret'
|
||||
self._con.execute(delete)
|
||||
|
||||
insert = '''INSERT INTO secret (device_id, public_key, private_key)
|
||||
VALUES (?, ?, ?)'''
|
||||
for row in rows:
|
||||
try:
|
||||
self._con.execute(insert, row)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
self._con.execute('PRAGMA user_version=9')
|
||||
self._con.commit()
|
||||
|
||||
if self.user_version() < 10:
|
||||
# Sanitize invalid BLOBs from the python2 days
|
||||
query_keys = '''SELECT _id,
|
||||
recipient_id,
|
||||
device_id,
|
||||
CAST(record as BLOB) as record,
|
||||
timestamp,
|
||||
active
|
||||
FROM sessions'''
|
||||
rows = self._con.execute(query_keys).fetchall()
|
||||
|
||||
delete = 'DELETE FROM sessions'
|
||||
self._con.execute(delete)
|
||||
|
||||
insert = '''INSERT INTO sessions (_id, recipient_id, device_id,
|
||||
record, timestamp, active)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'''
|
||||
for row in rows:
|
||||
try:
|
||||
self._con.execute(insert, row)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
self._con.execute('PRAGMA user_version=10')
|
||||
self._con.commit()
|
||||
|
||||
if self.user_version() < 11:
|
||||
# Sanitize invalid BLOBs from the python2 days
|
||||
query_keys = '''SELECT _id,
|
||||
prekey_id,
|
||||
sent_to_server,
|
||||
CAST(record as BLOB) as record
|
||||
FROM prekeys'''
|
||||
rows = self._con.execute(query_keys).fetchall()
|
||||
|
||||
delete = 'DELETE FROM prekeys'
|
||||
self._con.execute(delete)
|
||||
|
||||
insert = '''INSERT INTO prekeys (
|
||||
_id, prekey_id, sent_to_server, record)
|
||||
VALUES (?, ?, ?, ?)'''
|
||||
for row in rows:
|
||||
try:
|
||||
self._con.execute(insert, row)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
self._con.execute('PRAGMA user_version=11')
|
||||
self._con.commit()
|
||||
|
||||
if self.user_version() < 12:
|
||||
# Sanitize invalid BLOBs from the python2 days
|
||||
query_keys = '''SELECT _id,
|
||||
prekey_id,
|
||||
timestamp,
|
||||
CAST(record as BLOB) as record
|
||||
FROM signed_prekeys'''
|
||||
rows = self._con.execute(query_keys).fetchall()
|
||||
|
||||
delete = 'DELETE FROM signed_prekeys'
|
||||
self._con.execute(delete)
|
||||
|
||||
insert = '''INSERT INTO signed_prekeys (
|
||||
_id, prekey_id, timestamp, record)
|
||||
VALUES (?, ?, ?, ?)'''
|
||||
for row in rows:
|
||||
try:
|
||||
self._con.execute(insert, row)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
self._con.execute('PRAGMA user_version=12')
|
||||
self._con.commit()
|
||||
|
||||
def loadSignedPreKey(self, signedPreKeyId):
|
||||
query = 'SELECT record FROM signed_prekeys WHERE prekey_id = ?'
|
||||
result = self._con.execute(query, (signedPreKeyId, )).fetchone()
|
||||
if result is None:
|
||||
raise InvalidKeyIdException("No such signedprekeyrecord! %s " %
|
||||
signedPreKeyId)
|
||||
return SignedPreKeyRecord(serialized=result.record)
|
||||
|
||||
def loadSignedPreKeys(self):
|
||||
query = 'SELECT record FROM signed_prekeys'
|
||||
results = self._con.execute(query).fetchall()
|
||||
return [SignedPreKeyRecord(serialized=row.record) for row in results]
|
||||
|
||||
def storeSignedPreKey(self, signedPreKeyId, signedPreKeyRecord):
|
||||
query = 'INSERT INTO signed_prekeys (prekey_id, record) VALUES(?,?)'
|
||||
self._con.execute(query, (signedPreKeyId,
|
||||
signedPreKeyRecord.serialize()))
|
||||
self._con.commit()
|
||||
|
||||
def containsSignedPreKey(self, signedPreKeyId):
|
||||
query = 'SELECT record FROM signed_prekeys WHERE prekey_id = ?'
|
||||
result = self._con.execute(query, (signedPreKeyId,)).fetchone()
|
||||
return result is not None
|
||||
|
||||
def removeSignedPreKey(self, signedPreKeyId):
|
||||
query = 'DELETE FROM signed_prekeys WHERE prekey_id = ?'
|
||||
self._con.execute(query, (signedPreKeyId,))
|
||||
self._con.commit()
|
||||
|
||||
def getNextSignedPreKeyId(self):
|
||||
result = self.getCurrentSignedPreKeyId()
|
||||
if result is None:
|
||||
return 1 # StartId if no SignedPreKeys exist
|
||||
return (result % (Medium.MAX_VALUE - 1)) + 1
|
||||
|
||||
def getCurrentSignedPreKeyId(self):
|
||||
query = 'SELECT MAX(prekey_id) FROM signed_prekeys'
|
||||
result = self._con.execute(query).fetchone()
|
||||
return result.max_prekey_id if result is not None else None
|
||||
|
||||
def getSignedPreKeyTimestamp(self, signedPreKeyId):
|
||||
query = '''SELECT strftime('%s', timestamp) FROM
|
||||
signed_prekeys WHERE prekey_id = ?'''
|
||||
|
||||
result = self._con.execute(query, (signedPreKeyId,)).fetchone()
|
||||
if result is None:
|
||||
raise InvalidKeyIdException('No such signedprekeyrecord! %s' %
|
||||
signedPreKeyId)
|
||||
|
||||
return result.formated_time
|
||||
|
||||
def removeOldSignedPreKeys(self, timestamp):
|
||||
query = '''DELETE FROM signed_prekeys
|
||||
WHERE timestamp < datetime(?, "unixepoch")'''
|
||||
self._con.execute(query, (timestamp,))
|
||||
self._con.commit()
|
||||
|
||||
def loadSession(self, recipientId, deviceId):
|
||||
query = '''SELECT record as "record [session_record]"
|
||||
FROM sessions WHERE recipient_id = ? AND device_id = ?'''
|
||||
result = self._con.execute(query, (recipientId, deviceId)).fetchone()
|
||||
return result.record if result is not None else SessionRecord()
|
||||
|
||||
def getJidFromDevice(self, device_id):
|
||||
query = '''SELECT recipient_id
|
||||
FROM sessions WHERE device_id = ?'''
|
||||
result = self._con.execute(query, (device_id, )).fetchone()
|
||||
return result.recipient_id if result is not None else None
|
||||
|
||||
def getActiveDeviceTuples(self):
|
||||
query = '''SELECT recipient_id, device_id
|
||||
FROM sessions WHERE active = 1'''
|
||||
return self._con.execute(query).fetchall()
|
||||
|
||||
def storeSession(self, recipientId, deviceId, sessionRecord):
|
||||
query = '''INSERT INTO sessions(recipient_id, device_id, record)
|
||||
VALUES(?,?,?)'''
|
||||
try:
|
||||
self._con.execute(query, (recipientId,
|
||||
deviceId,
|
||||
sessionRecord.serialize()))
|
||||
except sqlite3.IntegrityError:
|
||||
query = '''UPDATE sessions SET record = ?
|
||||
WHERE recipient_id = ? AND device_id = ?'''
|
||||
self._con.execute(query, (sessionRecord.serialize(),
|
||||
recipientId,
|
||||
deviceId))
|
||||
|
||||
self._con.commit()
|
||||
|
||||
def containsSession(self, recipientId, deviceId):
|
||||
query = '''SELECT record FROM sessions
|
||||
WHERE recipient_id = ? AND device_id = ?'''
|
||||
result = self._con.execute(query, (recipientId, deviceId)).fetchone()
|
||||
return result is not None
|
||||
|
||||
def deleteSession(self, recipientId, deviceId):
|
||||
self._log.info('Delete session for %s %s', recipientId, deviceId)
|
||||
query = "DELETE FROM sessions WHERE recipient_id = ? AND device_id = ?"
|
||||
self._con.execute(query, (recipientId, deviceId))
|
||||
self._con.commit()
|
||||
|
||||
def deleteAllSessions(self, recipientId):
|
||||
query = 'DELETE FROM sessions WHERE recipient_id = ?'
|
||||
self._con.execute(query, (recipientId,))
|
||||
self._con.commit()
|
||||
|
||||
def getSessionsFromJid(self, recipientId):
|
||||
query = '''SELECT recipient_id,
|
||||
device_id,
|
||||
record as "record [session_record]",
|
||||
active
|
||||
FROM sessions WHERE recipient_id = ?'''
|
||||
return self._con.execute(query, (recipientId,)).fetchall()
|
||||
|
||||
def getSessionsFromJids(self, recipientIds):
|
||||
query = '''SELECT recipient_id,
|
||||
device_id,
|
||||
record as "record [session_record]",
|
||||
active
|
||||
FROM sessions
|
||||
WHERE recipient_id IN ({})'''.format(
|
||||
', '.join(['?'] * len(recipientIds)))
|
||||
return self._con.execute(query, recipientIds).fetchall()
|
||||
|
||||
def setActiveState(self, jid, devicelist):
|
||||
query = '''UPDATE sessions SET active = 1
|
||||
WHERE recipient_id = ? AND device_id IN ({})'''.format(
|
||||
', '.join(['?'] * len(devicelist)))
|
||||
self._con.execute(query, (jid,) + tuple(devicelist))
|
||||
|
||||
query = '''UPDATE sessions SET active = 0
|
||||
WHERE recipient_id = ? AND device_id NOT IN ({})'''.format(
|
||||
', '.join(['?'] * len(devicelist)))
|
||||
self._con.execute(query, (jid,) + tuple(devicelist))
|
||||
self._con.commit()
|
||||
|
||||
def setInactive(self, jid, device_id):
|
||||
query = '''UPDATE sessions SET active = 0
|
||||
WHERE recipient_id = ? AND device_id = ?'''
|
||||
self._con.execute(query, (jid, device_id))
|
||||
self._con.commit()
|
||||
|
||||
def getInactiveSessionsKeys(self, recipientId):
|
||||
query = '''SELECT record as "record [session_record]" FROM sessions
|
||||
WHERE active = 0 AND recipient_id = ?'''
|
||||
results = self._con.execute(query, (recipientId,)).fetchall()
|
||||
|
||||
keys = []
|
||||
for result in results:
|
||||
key = result.record.getSessionState().getRemoteIdentityKey()
|
||||
keys.append(IdentityKeyExtended(key.getPublicKey()))
|
||||
return keys
|
||||
|
||||
def loadPreKey(self, preKeyId):
|
||||
query = '''SELECT record FROM prekeys WHERE prekey_id = ?'''
|
||||
|
||||
result = self._con.execute(query, (preKeyId,)).fetchone()
|
||||
if result is None:
|
||||
raise Exception("No such prekeyRecord!")
|
||||
return PreKeyRecord(serialized=result.record)
|
||||
|
||||
def loadPendingPreKeys(self):
|
||||
query = '''SELECT record FROM prekeys'''
|
||||
result = self._con.execute(query).fetchall()
|
||||
return [PreKeyRecord(serialized=row.record) for row in result]
|
||||
|
||||
def storePreKey(self, preKeyId, preKeyRecord):
|
||||
query = 'INSERT INTO prekeys (prekey_id, record) VALUES(?,?)'
|
||||
self._con.execute(query, (preKeyId, preKeyRecord.serialize()))
|
||||
self._con.commit()
|
||||
|
||||
def containsPreKey(self, preKeyId):
|
||||
query = 'SELECT record FROM prekeys WHERE prekey_id = ?'
|
||||
result = self._con.execute(query, (preKeyId,)).fetchone()
|
||||
return result is not None
|
||||
|
||||
def removePreKey(self, preKeyId):
|
||||
query = 'DELETE FROM prekeys WHERE prekey_id = ?'
|
||||
self._con.execute(query, (preKeyId,))
|
||||
self._con.commit()
|
||||
|
||||
def getCurrentPreKeyId(self):
|
||||
query = 'SELECT MAX(prekey_id) FROM prekeys'
|
||||
return self._con.execute(query).fetchone().max_prekey_id
|
||||
|
||||
def getPreKeyCount(self):
|
||||
query = 'SELECT COUNT(prekey_id) FROM prekeys'
|
||||
return self._con.execute(query).fetchone().count_prekey_id
|
||||
|
||||
def generateNewPreKeys(self, count):
|
||||
prekey_id = self.getCurrentPreKeyId() or 0
|
||||
pre_keys = KeyHelper.generatePreKeys(prekey_id + 1, count)
|
||||
for pre_key in pre_keys:
|
||||
self.storePreKey(pre_key.getId(), pre_key)
|
||||
|
||||
def getIdentityKeyPair(self):
|
||||
query = '''SELECT public_key as "public_key [pk]", private_key
|
||||
FROM secret LIMIT 1'''
|
||||
result = self._con.execute(query).fetchone()
|
||||
|
||||
return IdentityKeyPair(result.public_key,
|
||||
DjbECPrivateKey(result.private_key))
|
||||
|
||||
def getLocalRegistrationId(self):
|
||||
query = 'SELECT device_id FROM secret LIMIT 1'
|
||||
result = self._con.execute(query).fetchone()
|
||||
return result.device_id if result is not None else None
|
||||
|
||||
def storeLocalData(self, device_id, identityKeyPair):
|
||||
query = 'SELECT * FROM secret'
|
||||
result = self._con.execute(query).fetchone()
|
||||
if result is not None:
|
||||
self._log.error('Trying to save secret key into '
|
||||
'non-empty secret table')
|
||||
return
|
||||
|
||||
query = '''INSERT INTO secret(device_id, public_key, private_key)
|
||||
VALUES(?, ?, ?)'''
|
||||
|
||||
public_key = identityKeyPair.getPublicKey().getPublicKey().serialize()
|
||||
private_key = identityKeyPair.getPrivateKey().serialize()
|
||||
self._con.execute(query, (device_id, public_key, private_key))
|
||||
self._con.commit()
|
||||
|
||||
def saveIdentity(self, recipientId, identityKey):
|
||||
query = '''INSERT INTO identities (recipient_id, public_key, trust, shown)
|
||||
VALUES(?, ?, ?, ?)'''
|
||||
if not self.containsIdentity(recipientId, identityKey):
|
||||
trust = self.getDefaultTrust(recipientId)
|
||||
self._con.execute(query, (recipientId,
|
||||
identityKey.getPublicKey().serialize(),
|
||||
trust,
|
||||
1 if trust == Trust.BLIND else 0))
|
||||
self._con.commit()
|
||||
|
||||
def containsIdentity(self, recipientId, identityKey):
|
||||
query = '''SELECT * FROM identities WHERE recipient_id = ?
|
||||
AND public_key = ?'''
|
||||
|
||||
public_key = identityKey.getPublicKey().serialize()
|
||||
result = self._con.execute(query, (recipientId,
|
||||
public_key)).fetchone()
|
||||
|
||||
return result is not None
|
||||
|
||||
def deleteIdentity(self, recipientId, identityKey):
|
||||
query = '''DELETE FROM identities
|
||||
WHERE recipient_id = ? AND public_key = ?'''
|
||||
public_key = identityKey.getPublicKey().serialize()
|
||||
self._con.execute(query, (recipientId, public_key))
|
||||
self._con.commit()
|
||||
|
||||
def isTrustedIdentity(self, recipientId, identityKey):
|
||||
return True
|
||||
|
||||
def getTrustForIdentity(self, recipientId, identityKey):
|
||||
query = '''SELECT trust FROM identities WHERE recipient_id = ?
|
||||
AND public_key = ?'''
|
||||
public_key = identityKey.getPublicKey().serialize()
|
||||
result = self._con.execute(query, (recipientId, public_key)).fetchone()
|
||||
return result.trust if result is not None else None
|
||||
|
||||
def getFingerprints(self, jid):
|
||||
query = '''SELECT recipient_id,
|
||||
public_key as "public_key [pk]",
|
||||
trust,
|
||||
timestamp
|
||||
FROM identities
|
||||
WHERE recipient_id = ? ORDER BY trust ASC'''
|
||||
return self._con.execute(query, (jid,)).fetchall()
|
||||
|
||||
def getMucFingerprints(self, jids):
|
||||
query = '''
|
||||
SELECT recipient_id,
|
||||
public_key as "public_key [pk]",
|
||||
trust,
|
||||
timestamp
|
||||
FROM identities
|
||||
WHERE recipient_id IN ({}) ORDER BY trust ASC
|
||||
'''.format(', '.join(['?'] * len(jids)))
|
||||
|
||||
return self._con.execute(query, jids).fetchall()
|
||||
|
||||
def hasUndecidedFingerprints(self, jid):
|
||||
query = '''SELECT public_key as "public_key [pk]" FROM identities
|
||||
WHERE recipient_id = ? AND trust = ?'''
|
||||
result = self._con.execute(query, (jid, Trust.UNDECIDED)).fetchall()
|
||||
undecided = [row.public_key for row in result]
|
||||
|
||||
inactive = self.getInactiveSessionsKeys(jid)
|
||||
undecided = set(undecided) - set(inactive)
|
||||
return bool(undecided)
|
||||
|
||||
def getDefaultTrust(self, jid):
|
||||
if not self._is_blind_trust_enabled():
|
||||
return Trust.UNDECIDED
|
||||
|
||||
query = '''SELECT * FROM identities
|
||||
WHERE recipient_id = ? AND trust IN (0, 1)'''
|
||||
result = self._con.execute(query, (jid,)).fetchone()
|
||||
if result is None:
|
||||
return Trust.BLIND
|
||||
return Trust.UNDECIDED
|
||||
|
||||
def getTrustedFingerprints(self, jid):
|
||||
query = '''SELECT public_key as "public_key [pk]" FROM identities
|
||||
WHERE recipient_id = ? AND trust IN(1, 3)'''
|
||||
result = self._con.execute(query, (jid,)).fetchall()
|
||||
return [row.public_key for row in result]
|
||||
|
||||
def getNewFingerprints(self, jid):
|
||||
query = '''SELECT _id FROM identities WHERE shown = 0
|
||||
AND recipient_id = ?'''
|
||||
|
||||
result = self._con.execute(query, (jid,)).fetchall()
|
||||
return [row.id for row in result]
|
||||
|
||||
def setShownFingerprints(self, fingerprints):
|
||||
query = 'UPDATE identities SET shown = 1 WHERE _id IN ({})'.format(
|
||||
', '.join(['?'] * len(fingerprints)))
|
||||
self._con.execute(query, fingerprints)
|
||||
self._con.commit()
|
||||
|
||||
def setTrust(self, recipient_id, identityKey, trust):
|
||||
query = '''UPDATE identities SET trust = ? WHERE public_key = ?
|
||||
AND recipient_id = ?'''
|
||||
public_key = identityKey.getPublicKey().serialize()
|
||||
self._con.execute(query, (trust, public_key, recipient_id))
|
||||
self._con.commit()
|
||||
|
||||
def isTrusted(self, recipient_id, device_id):
|
||||
record = self.loadSession(recipient_id, device_id)
|
||||
if record.isFresh():
|
||||
return False
|
||||
identity_key = record.getSessionState().getRemoteIdentityKey()
|
||||
return self.getTrustForIdentity(
|
||||
recipient_id, identity_key) in (Trust.VERIFIED, Trust.BLIND)
|
||||
|
||||
def getIdentityLastSeen(self, recipient_id, identity_key):
|
||||
identity_key = identity_key.getPublicKey().serialize()
|
||||
query = '''SELECT timestamp FROM identities
|
||||
WHERE recipient_id = ? AND public_key = ?'''
|
||||
result = self._con.execute(query, (recipient_id,
|
||||
identity_key)).fetchone()
|
||||
return result.timestamp if result is not None else None
|
||||
|
||||
def setIdentityLastSeen(self, recipient_id, identity_key):
|
||||
timestamp = int(time.time())
|
||||
identity_key = identity_key.getPublicKey().serialize()
|
||||
self._log.info('Set last seen for %s %s', recipient_id, timestamp)
|
||||
query = '''UPDATE identities SET timestamp = ?
|
||||
WHERE recipient_id = ? AND public_key = ?'''
|
||||
self._con.execute(query, (timestamp, recipient_id, identity_key))
|
||||
self._con.commit()
|
||||
|
||||
def getUnacknowledgedCount(self, recipient_id, device_id):
|
||||
record = self.loadSession(recipient_id, device_id)
|
||||
if record.isFresh():
|
||||
return 0
|
||||
state = record.getSessionState()
|
||||
return state.getSenderChainKey().getIndex()
|
||||
@@ -1,362 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from nbxmpp.structs import OMEMOBundle
|
||||
from nbxmpp.structs import OMEMOMessage
|
||||
|
||||
from axolotl.ecc.djbec import DjbECPublicKey
|
||||
from axolotl.identitykey import IdentityKey
|
||||
|
||||
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 axolotl.duplicatemessagexception import DuplicateMessageException
|
||||
|
||||
from omemo.backend.aes import aes_decrypt
|
||||
from omemo.backend.aes import aes_encrypt
|
||||
from omemo.backend.aes import get_new_key
|
||||
from omemo.backend.aes import get_new_iv
|
||||
from omemo.backend.devices import DeviceManager
|
||||
from omemo.backend.devices import NoDevicesFound
|
||||
from omemo.backend.liteaxolotlstore import LiteAxolotlStore
|
||||
from omemo.backend.util import get_fingerprint
|
||||
from omemo.backend.util import Trust
|
||||
from omemo.backend.util import DEFAULT_PREKEY_AMOUNT
|
||||
from omemo.backend.util import MIN_PREKEY_AMOUNT
|
||||
from omemo.backend.util import SPK_CYCLE_TIME
|
||||
from omemo.backend.util import SPK_ARCHIVE_TIME
|
||||
from omemo.backend.util import UNACKNOWLEDGED_COUNT
|
||||
|
||||
|
||||
class OmemoState(DeviceManager):
|
||||
def __init__(self, own_jid, db_path, account, xmpp_con):
|
||||
self._account = account
|
||||
self._own_jid = own_jid
|
||||
self._log = xmpp_con._log
|
||||
self._session_ciphers = defaultdict(dict)
|
||||
self._storage = LiteAxolotlStore(db_path, self._log)
|
||||
|
||||
DeviceManager.__init__(self)
|
||||
|
||||
self.xmpp_con = xmpp_con
|
||||
|
||||
self._log.info('%s PreKeys available',
|
||||
self._storage.getPreKeyCount())
|
||||
|
||||
def build_session(self, jid, device_id, bundle):
|
||||
session = SessionBuilder(self._storage, self._storage, self._storage,
|
||||
self._storage, jid, device_id)
|
||||
|
||||
registration_id = self._storage.getLocalRegistrationId()
|
||||
|
||||
prekey = bundle.pick_prekey()
|
||||
otpk = DjbECPublicKey(prekey['key'][1:])
|
||||
|
||||
spk = DjbECPublicKey(bundle.spk['key'][1:])
|
||||
ik = IdentityKey(DjbECPublicKey(bundle.ik[1:]))
|
||||
|
||||
prekey_bundle = PreKeyBundle(registration_id,
|
||||
device_id,
|
||||
prekey['id'],
|
||||
otpk,
|
||||
bundle.spk['id'],
|
||||
spk,
|
||||
bundle.spk_signature,
|
||||
ik)
|
||||
|
||||
session.processPreKeyBundle(prekey_bundle)
|
||||
self._get_session_cipher(jid, device_id)
|
||||
|
||||
@property
|
||||
def storage(self):
|
||||
return self._storage
|
||||
|
||||
@property
|
||||
def own_fingerprint(self):
|
||||
return get_fingerprint(self._storage.getIdentityKeyPair())
|
||||
|
||||
@property
|
||||
def bundle(self):
|
||||
self._check_pre_key_count()
|
||||
|
||||
bundle = {'otpks': []}
|
||||
for k in self._storage.loadPendingPreKeys():
|
||||
key = k.getKeyPair().getPublicKey().serialize()
|
||||
bundle['otpks'].append({'key': key, 'id': k.getId()})
|
||||
|
||||
ik_pair = self._storage.getIdentityKeyPair()
|
||||
bundle['ik'] = ik_pair.getPublicKey().serialize()
|
||||
|
||||
self._cycle_signed_pre_key(ik_pair)
|
||||
|
||||
spk = self._storage.loadSignedPreKey(
|
||||
self._storage.getCurrentSignedPreKeyId())
|
||||
bundle['spk_signature'] = spk.getSignature()
|
||||
bundle['spk'] = {'key': spk.getKeyPair().getPublicKey().serialize(),
|
||||
'id': spk.getId()}
|
||||
|
||||
return OMEMOBundle(**bundle)
|
||||
|
||||
def decrypt_message(self, omemo_message, jid):
|
||||
if omemo_message.sid == self.own_device:
|
||||
self._log.info('Received previously sent message by us')
|
||||
raise SelfMessage
|
||||
|
||||
try:
|
||||
encrypted_key, prekey = omemo_message.keys[self.own_device]
|
||||
except KeyError:
|
||||
self._log.info('Received message not for our device')
|
||||
raise MessageNotForDevice
|
||||
|
||||
try:
|
||||
if prekey:
|
||||
key, fingerprint, trust = self._process_pre_key_message(
|
||||
jid, omemo_message.sid, encrypted_key)
|
||||
else:
|
||||
key, fingerprint, trust = self._process_message(
|
||||
jid, omemo_message.sid, encrypted_key)
|
||||
|
||||
except DuplicateMessageException:
|
||||
self._log.info('Received duplicated message')
|
||||
raise DuplicateMessage
|
||||
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
raise DecryptionFailed
|
||||
|
||||
if omemo_message.payload is None:
|
||||
self._log.debug("Decrypted Key Exchange Message")
|
||||
raise KeyExchangeMessage
|
||||
|
||||
try:
|
||||
result = aes_decrypt(key, omemo_message.iv, omemo_message.payload)
|
||||
except Exception as error:
|
||||
self._log.warning(error)
|
||||
raise DecryptionFailed
|
||||
|
||||
self._log.debug("Decrypted Message => %s", result)
|
||||
return result, fingerprint, trust
|
||||
|
||||
def _get_whisper_message(self, jid, device, key):
|
||||
cipher = self._get_session_cipher(jid, device)
|
||||
cipher_key = cipher.encrypt(key)
|
||||
prekey = isinstance(cipher_key, PreKeyWhisperMessage)
|
||||
return cipher_key.serialize(), prekey
|
||||
|
||||
def encrypt(self, jid, plaintext):
|
||||
try:
|
||||
devices_for_encryption = self.get_devices_for_encryption(jid)
|
||||
except NoDevicesFound:
|
||||
self._log.warning('No devices for encryption found for: %s', jid)
|
||||
return
|
||||
|
||||
result = aes_encrypt(plaintext)
|
||||
whisper_messages = defaultdict(dict)
|
||||
|
||||
for jid_, device in devices_for_encryption:
|
||||
count = self._storage.getUnacknowledgedCount(jid_, device)
|
||||
if count >= UNACKNOWLEDGED_COUNT:
|
||||
self._log.warning('Set device inactive %s because of %s '
|
||||
'unacknowledged messages', device, count)
|
||||
self.remove_device(jid_, device)
|
||||
|
||||
try:
|
||||
whisper_messages[jid_][device] = self._get_whisper_message(
|
||||
jid_, device, result.key)
|
||||
except Exception:
|
||||
self._log.exception('Failed to encrypt')
|
||||
continue
|
||||
|
||||
recipients = set(whisper_messages.keys())
|
||||
if jid != self._own_jid:
|
||||
recipients -= set([self._own_jid])
|
||||
if not recipients:
|
||||
self._log.error('Encrypted keys empty')
|
||||
return
|
||||
|
||||
encrypted_keys = {}
|
||||
for jid_ in whisper_messages:
|
||||
encrypted_keys.update(whisper_messages[jid_])
|
||||
|
||||
self._log.debug('Finished encrypting message')
|
||||
return OMEMOMessage(sid=self.own_device,
|
||||
keys=encrypted_keys,
|
||||
iv=result.iv,
|
||||
payload=result.payload)
|
||||
|
||||
def encrypt_key_transport(self, jid, devices):
|
||||
whisper_messages = defaultdict(dict)
|
||||
for device in devices:
|
||||
try:
|
||||
whisper_messages[jid][device] = self._get_whisper_message(
|
||||
jid, device, get_new_key())
|
||||
except Exception:
|
||||
self._log.exception('Failed to encrypt')
|
||||
continue
|
||||
|
||||
if not whisper_messages[jid]:
|
||||
self._log.error('Encrypted keys empty')
|
||||
return
|
||||
|
||||
self._log.debug('Finished Key Transport message')
|
||||
return OMEMOMessage(sid=self.own_device,
|
||||
keys=whisper_messages[jid],
|
||||
iv=get_new_iv(),
|
||||
payload=None)
|
||||
|
||||
def has_trusted_keys(self, jid):
|
||||
inactive = self._storage.getInactiveSessionsKeys(jid)
|
||||
trusted = self._storage.getTrustedFingerprints(jid)
|
||||
return bool(set(trusted) - set(inactive))
|
||||
|
||||
def devices_without_sessions(self, jid):
|
||||
known_devices = self.get_devices(jid, without_self=True)
|
||||
missing_devices = [dev
|
||||
for dev in known_devices
|
||||
if not self._storage.containsSession(jid, dev)]
|
||||
if missing_devices:
|
||||
self._log.info('Missing device sessions for %s: %s',
|
||||
jid, missing_devices)
|
||||
return missing_devices
|
||||
|
||||
def _get_session_cipher(self, jid, device_id):
|
||||
try:
|
||||
return self._session_ciphers[jid][device_id]
|
||||
except KeyError:
|
||||
cipher = SessionCipher(self._storage, self._storage, self._storage,
|
||||
self._storage, jid, device_id)
|
||||
self._session_ciphers[jid][device_id] = cipher
|
||||
return cipher
|
||||
|
||||
def _process_pre_key_message(self, jid, device, key):
|
||||
self._log.info('Process pre key message from %s', jid)
|
||||
pre_key_message = PreKeyWhisperMessage(serialized=key)
|
||||
if not pre_key_message.getPreKeyId():
|
||||
raise Exception('Received Pre Key Message '
|
||||
'without PreKey => %s' % jid)
|
||||
|
||||
session_cipher = self._get_session_cipher(jid, device)
|
||||
key = session_cipher.decryptPkmsg(pre_key_message)
|
||||
|
||||
identity_key = pre_key_message.getIdentityKey()
|
||||
trust = self._get_trust_from_identity_key(jid, identity_key)
|
||||
fingerprint = get_fingerprint(identity_key)
|
||||
|
||||
self._storage.setIdentityLastSeen(jid, identity_key)
|
||||
|
||||
self.xmpp_con.set_bundle()
|
||||
self.add_device(jid, device)
|
||||
return key, fingerprint, trust
|
||||
|
||||
def _process_message(self, jid, device, key):
|
||||
self._log.info('Process message from %s', jid)
|
||||
message = WhisperMessage(serialized=key)
|
||||
|
||||
session_cipher = self._get_session_cipher(jid, device)
|
||||
key = session_cipher.decryptMsg(message, textMsg=False)
|
||||
|
||||
identity_key = self._get_identity_key_from_device(jid, device)
|
||||
trust = self._get_trust_from_identity_key(jid, identity_key)
|
||||
fingerprint = get_fingerprint(identity_key)
|
||||
|
||||
self._storage.setIdentityLastSeen(jid, identity_key)
|
||||
|
||||
self.add_device(jid, device)
|
||||
|
||||
return key, fingerprint, trust
|
||||
|
||||
@staticmethod
|
||||
def _get_identity_key_from_pk_message(key):
|
||||
pre_key_message = PreKeyWhisperMessage(serialized=key)
|
||||
return pre_key_message.getIdentityKey()
|
||||
|
||||
def _get_identity_key_from_device(self, jid, device):
|
||||
session_record = self._storage.loadSession(jid, device)
|
||||
return session_record.getSessionState().getRemoteIdentityKey()
|
||||
|
||||
def _get_trust_from_identity_key(self, jid, identity_key):
|
||||
trust = self._storage.getTrustForIdentity(jid, identity_key)
|
||||
return Trust(trust) if trust is not None else Trust.UNDECIDED
|
||||
|
||||
def _check_pre_key_count(self):
|
||||
# Check if enough PreKeys are available
|
||||
pre_key_count = self._storage.getPreKeyCount()
|
||||
if pre_key_count < MIN_PREKEY_AMOUNT:
|
||||
missing_count = DEFAULT_PREKEY_AMOUNT - pre_key_count
|
||||
self._storage.generateNewPreKeys(missing_count)
|
||||
self._log.info('%s PreKeys created', missing_count)
|
||||
|
||||
def _cycle_signed_pre_key(self, ik_pair):
|
||||
# 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._storage.getCurrentSignedPreKeyId():
|
||||
spk = KeyHelper.generateSignedPreKey(
|
||||
ik_pair, self._storage.getNextSignedPreKeyId())
|
||||
self._storage.storeSignedPreKey(spk.getId(), spk)
|
||||
self._log.debug('New SignedPreKey created, because none existed')
|
||||
|
||||
# if SPK_CYCLE_TIME is reached, generate a new SignedPreKey
|
||||
now = int(time.time())
|
||||
timestamp = self._storage.getSignedPreKeyTimestamp(
|
||||
self._storage.getCurrentSignedPreKeyId())
|
||||
|
||||
if int(timestamp) < now - SPK_CYCLE_TIME:
|
||||
spk = KeyHelper.generateSignedPreKey(
|
||||
ik_pair, self._storage.getNextSignedPreKeyId())
|
||||
self._storage.storeSignedPreKey(spk.getId(), spk)
|
||||
self._log.debug('Cycled SignedPreKey')
|
||||
|
||||
# Delete all SignedPreKeys that are older than SPK_ARCHIVE_TIME
|
||||
timestamp = now - SPK_ARCHIVE_TIME
|
||||
self._storage.removeOldSignedPreKeys(timestamp)
|
||||
|
||||
|
||||
class NoValidSessions(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SelfMessage(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MessageNotForDevice(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DecryptionFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KeyExchangeMessage(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidMessage(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateMessage(Exception):
|
||||
pass
|
||||
@@ -1,56 +0,0 @@
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import binascii
|
||||
import textwrap
|
||||
from enum import IntEnum
|
||||
|
||||
from axolotl.identitykey import IdentityKey
|
||||
|
||||
DEFAULT_PREKEY_AMOUNT = 100
|
||||
MIN_PREKEY_AMOUNT = 80
|
||||
SPK_ARCHIVE_TIME = 86400 * 15 # 15 Days
|
||||
SPK_CYCLE_TIME = 86400 # 24 Hours
|
||||
UNACKNOWLEDGED_COUNT = 2000
|
||||
|
||||
|
||||
class Trust(IntEnum):
|
||||
UNTRUSTED = 0
|
||||
VERIFIED = 1
|
||||
UNDECIDED = 2
|
||||
BLIND = 3
|
||||
|
||||
|
||||
def get_fingerprint(identity_key, formatted=False):
|
||||
public_key = identity_key.getPublicKey().serialize()
|
||||
fingerprint = binascii.hexlify(public_key).decode()[2:]
|
||||
if not formatted:
|
||||
return fingerprint
|
||||
fplen = len(fingerprint)
|
||||
wordsize = fplen // 8
|
||||
buf = ''
|
||||
for w in range(0, fplen, wordsize):
|
||||
buf += '{0} '.format(fingerprint[w:w + wordsize])
|
||||
buf = textwrap.fill(buf, width=36)
|
||||
return buf.rstrip().upper()
|
||||
|
||||
|
||||
class IdentityKeyExtended(IdentityKey):
|
||||
def __hash__(self):
|
||||
return hash(self.publicKey.serialize())
|
||||
|
||||
def get_fingerprint(self, formatted=False):
|
||||
return get_fingerprint(self, formatted=formatted)
|
||||
@@ -1,183 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
# Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.plugins.helpers import get_builder
|
||||
from gajim.plugins.plugins_i18n import _
|
||||
|
||||
from omemo.backend.util import get_fingerprint
|
||||
|
||||
log = logging.getLogger('gajim.p.omemo')
|
||||
|
||||
|
||||
class OMEMOConfigDialog(Gtk.ApplicationWindow):
|
||||
def __init__(self, plugin, transient):
|
||||
Gtk.ApplicationWindow.__init__(self)
|
||||
self.set_application(app.app)
|
||||
self.set_show_menubar(False)
|
||||
self.set_title(_('OMEMO Settings'))
|
||||
self.set_transient_for(transient)
|
||||
self.set_default_size(400, 400)
|
||||
self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
|
||||
self.set_destroy_with_parent(True)
|
||||
|
||||
self._plugin = plugin
|
||||
|
||||
path = self._plugin.local_file_path('gtk/config.ui')
|
||||
self._ui = get_builder(path)
|
||||
|
||||
image_path = self._plugin.local_file_path('omemo.png')
|
||||
self._ui.image.set_from_file(image_path)
|
||||
|
||||
try:
|
||||
self.disabled_accounts = self._plugin.config['DISABLED_ACCOUNTS']
|
||||
except KeyError:
|
||||
self._plugin.config['DISABLED_ACCOUNTS'] = []
|
||||
self.disabled_accounts = self._plugin.config['DISABLED_ACCOUNTS']
|
||||
|
||||
box = Gtk.Box()
|
||||
box.pack_start(self._ui.notebook1, True, True, 0)
|
||||
|
||||
self.add(box)
|
||||
|
||||
self._ui.connect_signals(self)
|
||||
self.show_all()
|
||||
|
||||
self.plugin_active = False
|
||||
for plugin in app.plugin_manager.active_plugins:
|
||||
log.debug(type(plugin))
|
||||
if type(plugin).__name__ == 'OmemoPlugin':
|
||||
self.plugin_active = True
|
||||
break
|
||||
|
||||
self.update_account_store()
|
||||
self.update_account_combobox()
|
||||
self.update_disabled_account_view()
|
||||
self.update_settings()
|
||||
|
||||
def is_in_accountstore(self, account):
|
||||
for row in self._ui.account_store:
|
||||
if row[0] == account:
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_account_store(self):
|
||||
for account in sorted(app.settings.get_active_accounts()):
|
||||
if account in self.disabled_accounts:
|
||||
continue
|
||||
if account == 'Local':
|
||||
continue
|
||||
if not self.is_in_accountstore(account):
|
||||
self._ui.account_store.append(row=(account,))
|
||||
|
||||
def update_account_combobox(self):
|
||||
if self.plugin_active is False:
|
||||
return
|
||||
if len(self._ui.account_store):
|
||||
self._ui.account_combobox.set_active(0)
|
||||
else:
|
||||
self.account_combobox_changed_cb(self._ui.account_combobox)
|
||||
|
||||
def account_combobox_changed_cb(self, box, *args):
|
||||
self.update_context_list()
|
||||
|
||||
def update_disabled_account_view(self):
|
||||
self._ui.disabled_account_store.clear()
|
||||
for account in self.disabled_accounts:
|
||||
self._ui.disabled_account_store.append(row=(account,))
|
||||
|
||||
def activate_accounts_btn_clicked(self, _button, *args):
|
||||
selection = self._ui.disabled_accounts_view.get_selection()
|
||||
mod, paths = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
it = mod.get_iter(path)
|
||||
account = mod.get(it, 0)
|
||||
if account[0] in self.disabled_accounts and \
|
||||
not self.is_in_accountstore(account[0]):
|
||||
self._ui.account_store.append(row=(account[0],))
|
||||
self.disabled_accounts.remove(account[0])
|
||||
self.update_disabled_account_view()
|
||||
self._plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
|
||||
self.update_account_combobox()
|
||||
|
||||
def disable_accounts_btn_clicked(self, _button, *args):
|
||||
selection = self._ui.active_accounts_view.get_selection()
|
||||
mod, paths = selection.get_selected_rows()
|
||||
for path in paths:
|
||||
it = mod.get_iter(path)
|
||||
account = mod.get(it, 0)
|
||||
if account[0] not in self.disabled_accounts and \
|
||||
self.is_in_accountstore(account[0]):
|
||||
self.disabled_accounts.append(account[0])
|
||||
self._ui.account_store.remove(it)
|
||||
self.update_disabled_account_view()
|
||||
self._plugin.config['DISABLED_ACCOUNTS'] = self.disabled_accounts
|
||||
self.update_account_combobox()
|
||||
|
||||
def cleardevice_button_clicked_cb(self, button, *args):
|
||||
active = self._ui.account_combobox.get_active()
|
||||
account = self._ui.account_store[active][0]
|
||||
app.get_client(account).get_module('OMEMO').clear_devicelist()
|
||||
self.update_context_list()
|
||||
|
||||
def refresh_button_clicked_cb(self, button, *args):
|
||||
self.update_context_list()
|
||||
|
||||
def _on_blind_trust(self, button):
|
||||
self._plugin.config['BLIND_TRUST'] = button.get_active()
|
||||
|
||||
def update_context_list(self):
|
||||
self._ui.deviceid_store.clear()
|
||||
|
||||
if not len(self._ui.account_store):
|
||||
self._ui.ID.set_markup('')
|
||||
self._ui.fingerprint_label.set_markup('')
|
||||
self._ui.refresh.set_sensitive(False)
|
||||
self._ui.cleardevice_button.set_sensitive(False)
|
||||
return
|
||||
active = self._ui.account_combobox.get_active()
|
||||
account = self._ui.account_store[active][0]
|
||||
|
||||
# Set buttons active
|
||||
self._ui.refresh.set_sensitive(True)
|
||||
if account == 'Local':
|
||||
self._ui.cleardevice_button.set_sensitive(False)
|
||||
else:
|
||||
self._ui.cleardevice_button.set_sensitive(True)
|
||||
|
||||
# Set FPR Label and DeviceID
|
||||
omemo = self._plugin.get_omemo(account)
|
||||
self._ui.ID.set_markup('<tt>%s</tt>' % omemo.backend.own_device)
|
||||
|
||||
identity_key = omemo.backend.storage.getIdentityKeyPair()
|
||||
fpr = get_fingerprint(identity_key, formatted=True)
|
||||
self._ui.fingerprint_label.set_markup('<tt>%s</tt>' % fpr)
|
||||
|
||||
own_jid = app.get_jid_from_account(account)
|
||||
# Set Device ID List
|
||||
for item in omemo.backend.get_devices(own_jid):
|
||||
self._ui.deviceid_store.append([item])
|
||||
|
||||
def update_settings(self):
|
||||
self._ui.blind_trust_checkbutton.set_active(
|
||||
self._plugin.config['BLIND_TRUST'])
|
||||
@@ -1,614 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkListStore" id="account_store">
|
||||
<columns>
|
||||
<!-- column-name accounts -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="deviceid_store">
|
||||
<columns>
|
||||
<!-- column-name Device -->
|
||||
<column type="gint"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="disabled_account_store">
|
||||
<columns>
|
||||
<!-- column-name accounts -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkNotebook" id="notebook1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="row_spacing">6</property>
|
||||
<property name="column_spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="label" translatable="yes" comments="label for account selector">Acc_ount</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="mnemonic_widget">account_combobox</property>
|
||||
<style>
|
||||
<class name="bold"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="account_combobox">
|
||||
<property name="width_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="has_focus">True</property>
|
||||
<property name="halign">start</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="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="label" translatable="yes" comments="Descriptive label">Own _Fingerprint</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="mnemonic_widget">fingerprint_label</property>
|
||||
<style>
|
||||
<class name="bold"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="fingerprint_label">
|
||||
<property name="width_request">200</property>
|
||||
<property name="height_request">30</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="use_markup">True</property>
|
||||
<property name="selectable">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="label" translatable="yes">Own _Device ID</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="mnemonic_widget">ID</property>
|
||||
<style>
|
||||
<class name="bold"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ID">
|
||||
<property name="width_request">200</property>
|
||||
<property name="height_request">30</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Note: Fingerprints of your contacts are managed in the message window.</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">50</property>
|
||||
<property name="xalign">0</property>
|
||||
<attributes>
|
||||
<attribute name="style" value="italic"/>
|
||||
</attributes>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="stock">gtk-missing-image</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel">
|
||||
<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="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="height_request">25</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Published Devices</property>
|
||||
<style>
|
||||
<class name="bold"/>
|
||||
<class name="dim-label"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</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="halign">center</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="deviceid_view">
|
||||
<property name="width_request">300</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">deviceid_store</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="enable_grid_lines">horizontal</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview-selection2"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="deviceid_column">
|
||||
<property name="title" translatable="yes">Device ID</property>
|
||||
<property name="clickable">True</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="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">6</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cleardevice_button">
|
||||
<property name="label" translatable="yes">_Clear Devices</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">This clears your device list from the server.
|
||||
Clearing the device list helps you to remove unused devices from the encryption process.
|
||||
It is advised to go online with all of your actively used devices after clearing.</property>
|
||||
<property name="use_underline">True</property>
|
||||
<signal name="clicked" handler="cleardevice_button_clicked_cb" swapped="no"/>
|
||||
<style>
|
||||
<class name="destructive-action"/>
|
||||
</style>
|
||||
</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="sensitive">False</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">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Clear Devices</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
<property name="tab_fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">18</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="message_type">warning</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="height_request">30</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">You have to restart Gajim for changes to take effect !</property>
|
||||
<style>
|
||||
<class name="bold"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</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">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="row_spacing">6</property>
|
||||
<property name="column_spacing">12</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="height_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">out</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="active_accounts_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="model">account_store</property>
|
||||
<property name="enable_grid_lines">horizontal</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview-selection3"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="treeviewcolumn1">
|
||||
<property name="title" translatable="yes">Active Accounts</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext5"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">out</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="disabled_accounts_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="model">disabled_account_store</property>
|
||||
<property name="enable_grid_lines">horizontal</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview-selection4"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="treeviewcolumn2">
|
||||
<property name="title" translatable="yes">Disabled Accounts</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext6"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disable_accounts_btn">
|
||||
<property name="label" translatable="yes">_Disable Account</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="use_underline">True</property>
|
||||
<signal name="clicked" handler="disable_accounts_btn_clicked" swapped="no"/>
|
||||
<style>
|
||||
<class name="destructive-action"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="activate_accounts_btn">
|
||||
<property name="label" translatable="yes">_Enable Account</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="use_underline">True</property>
|
||||
<signal name="clicked" handler="activate_accounts_btn_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="disable_accounts">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Disable Accounts</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">2</property>
|
||||
<property name="tab_fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">18</property>
|
||||
<property name="margin_right">18</property>
|
||||
<property name="margin_top">18</property>
|
||||
<property name="margin_bottom">18</property>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">none</property>
|
||||
<child>
|
||||
<object class="GtkAlignment">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="top_padding">12</property>
|
||||
<property name="left_padding">12</property>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="blind_trust_checkbutton">
|
||||
<property name="label" translatable="yes">Blind Trust Before Verification</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="_on_blind_trust" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">General</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Settings</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">3</property>
|
||||
<property name="tab_fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</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"/>
|
||||
<!-- column-name deviceid -->
|
||||
<column type="gint"/>
|
||||
</columns>
|
||||
</object>
|
||||
</interface>
|
||||
454
omemo/gtk/key.py
454
omemo/gtk/key.py
@@ -1,454 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
import locale
|
||||
import logging
|
||||
import tempfile
|
||||
from packaging.version import Version as V
|
||||
|
||||
from pkg_resources import get_distribution
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.plugins.plugins_i18n import _
|
||||
from gajim.plugins.helpers import get_builder
|
||||
from gajim.gtk.dialogs import ConfirmationDialog
|
||||
from gajim.gtk.dialogs import DialogButton
|
||||
|
||||
from omemo.backend.util import Trust
|
||||
from omemo.backend.util import IdentityKeyExtended
|
||||
from omemo.backend.util import get_fingerprint
|
||||
|
||||
log = logging.getLogger('gajim.p.omemo')
|
||||
|
||||
|
||||
TRUST_DATA = {
|
||||
Trust.UNTRUSTED: ('dialog-error-symbolic',
|
||||
_('Untrusted'),
|
||||
'error-color'),
|
||||
Trust.UNDECIDED: ('security-low-symbolic',
|
||||
_('Not Decided'),
|
||||
'warning-color'),
|
||||
Trust.VERIFIED: ('security-high-symbolic',
|
||||
_('Verified'),
|
||||
'encrypted-color'),
|
||||
Trust.BLIND: ('security-medium-symbolic',
|
||||
_('Blind Trust'),
|
||||
'encrypted-color')
|
||||
}
|
||||
|
||||
|
||||
class KeyDialog(Gtk.Dialog):
|
||||
def __init__(self, plugin, contact, transient, windows,
|
||||
groupchat=False):
|
||||
super().__init__(title=_('OMEMO Fingerprints'),
|
||||
destroy_with_parent=True)
|
||||
|
||||
self.set_transient_for(transient)
|
||||
self.set_resizable(True)
|
||||
self.set_default_size(500, 450)
|
||||
|
||||
self.get_style_context().add_class('omemo-key-dialog')
|
||||
|
||||
self._groupchat = groupchat
|
||||
self._contact = contact
|
||||
self._windows = windows
|
||||
self._account = self._contact.account
|
||||
self._plugin = plugin
|
||||
self._omemo = self._plugin.get_omemo(self._account)
|
||||
self._own_jid = app.get_jid_from_account(self._account)
|
||||
self._show_inactive = False
|
||||
|
||||
path = self._plugin.local_file_path('gtk/key.ui')
|
||||
self._ui = get_builder(path)
|
||||
|
||||
markup = '<a href="%s">%s</a>' % (
|
||||
'https://dev.gajim.org/gajim/gajim-plugins/-/'
|
||||
'wikis/omemogajimplugin', _('Read more about blind trust.'))
|
||||
self._ui.btbv_link.set_markup(markup)
|
||||
self._ui.infobar.set_revealed(
|
||||
self._plugin.config['SHOW_HELP_FINGERPRINTS'])
|
||||
|
||||
self._ui.header.set_text(_('Fingerprints for %s') % self._contact.jid)
|
||||
|
||||
omemo_img_path = self._plugin.local_file_path('omemo.png')
|
||||
self._ui.omemo_image.set_from_file(omemo_img_path)
|
||||
|
||||
self._ui.list.set_filter_func(self._filter_func, None)
|
||||
self._ui.list.set_sort_func(self._sort_func, None)
|
||||
|
||||
self._identity_key = self._omemo.backend.storage.getIdentityKeyPair()
|
||||
ownfpr_format = get_fingerprint(self._identity_key, formatted=True)
|
||||
self._ui.own_fingerprint.set_text(ownfpr_format)
|
||||
|
||||
self.get_content_area().add(self._ui.box)
|
||||
|
||||
self.update()
|
||||
self._load_qrcode()
|
||||
self._ui.connect_signals(self)
|
||||
self.connect('destroy', self._on_destroy)
|
||||
self.show_all()
|
||||
|
||||
def _on_infobar_response(self, _widget, response):
|
||||
if response == Gtk.ResponseType.CLOSE:
|
||||
self._ui.infobar.set_revealed(False)
|
||||
self._plugin.config['SHOW_HELP_FINGERPRINTS'] = False
|
||||
|
||||
def _filter_func(self, row, _user_data):
|
||||
search_text = self._ui.search.get_text()
|
||||
if search_text and search_text.lower() not in str(row.jid):
|
||||
return False
|
||||
if self._show_inactive:
|
||||
return True
|
||||
return row.active
|
||||
|
||||
@staticmethod
|
||||
def _sort_func(row1, row2, _user_data):
|
||||
result = locale.strcoll(str(row1.jid), str(row2.jid))
|
||||
if result != 0:
|
||||
return result
|
||||
|
||||
if row1.active != row2.active:
|
||||
return -1 if row1.active else 1
|
||||
|
||||
if row1.trust != row2.trust:
|
||||
return -1 if row1.trust > row2.trust else 1
|
||||
return 0
|
||||
|
||||
def _on_search_changed(self, _entry):
|
||||
self._ui.list.invalidate_filter()
|
||||
|
||||
def update(self):
|
||||
self._ui.list.foreach(self._ui.list.remove)
|
||||
self._load_fingerprints(self._own_jid)
|
||||
self._load_fingerprints(self._contact.jid, self._groupchat is True)
|
||||
|
||||
def _load_fingerprints(self, contact_jid, groupchat=False):
|
||||
if groupchat:
|
||||
members = list(self._omemo.backend.get_muc_members(contact_jid))
|
||||
sessions = self._omemo.backend.storage.getSessionsFromJids(members)
|
||||
else:
|
||||
sessions = self._omemo.backend.storage.getSessionsFromJid(contact_jid)
|
||||
|
||||
rows = {}
|
||||
if groupchat:
|
||||
results = self._omemo.backend.storage.getMucFingerprints(members)
|
||||
else:
|
||||
results = self._omemo.backend.storage.getFingerprints(contact_jid)
|
||||
for result in results:
|
||||
rows[result.public_key] = KeyRow(result.recipient_id,
|
||||
result.public_key,
|
||||
result.trust,
|
||||
result.timestamp)
|
||||
|
||||
for item in sessions:
|
||||
if item.record.isFresh():
|
||||
return
|
||||
identity_key = item.record.getSessionState().getRemoteIdentityKey()
|
||||
identity_key = IdentityKeyExtended(identity_key.getPublicKey())
|
||||
try:
|
||||
key_row = rows[identity_key]
|
||||
except KeyError:
|
||||
log.warning('Could not find session identitykey %s',
|
||||
item.device_id)
|
||||
self._omemo.backend.storage.deleteSession(item.recipient_id,
|
||||
item.device_id)
|
||||
continue
|
||||
|
||||
key_row.active = item.active
|
||||
key_row.device_id = item.device_id
|
||||
|
||||
for row in rows.values():
|
||||
self._ui.list.add(row)
|
||||
|
||||
@staticmethod
|
||||
def _get_qrcode(jid, sid, identity_key):
|
||||
fingerprint = get_fingerprint(identity_key)
|
||||
path = os.path.join(tempfile.gettempdir(),
|
||||
'omemo_{}.png'.format(jid))
|
||||
|
||||
ver_string = 'xmpp:{}?omemo-sid-{}={}'.format(jid, sid, fingerprint)
|
||||
log.debug('Verification String: %s', ver_string)
|
||||
|
||||
import qrcode
|
||||
qr = qrcode.QRCode(version=None,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=6,
|
||||
border=4)
|
||||
qr.add_data(ver_string)
|
||||
qr.make(fit=True)
|
||||
|
||||
fill_color = 'black'
|
||||
back_color = 'white'
|
||||
if V(get_distribution('qrcode').version) < V('6.0'):
|
||||
# meaning of fill_color and back_color were switched
|
||||
# before this commit in qrcode between versions 5.3
|
||||
# and 6.0: https://github.com/lincolnloop/python-qrcode/
|
||||
# commit/01f440d64b7d1f61bb75161ce118b86eca85b15c
|
||||
back_color, fill_color = fill_color, back_color
|
||||
|
||||
img = qr.make_image(fill_color=fill_color, back_color=back_color)
|
||||
img.save(path)
|
||||
return path
|
||||
|
||||
def _load_qrcode(self):
|
||||
try:
|
||||
path = self._get_qrcode(self._own_jid,
|
||||
self._omemo.backend.own_device,
|
||||
self._identity_key)
|
||||
except ImportError:
|
||||
log.exception('Failed to generate QR code')
|
||||
self._ui.qrcode.hide()
|
||||
self._ui.qrinfo.show()
|
||||
else:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
|
||||
self._ui.qrcode.set_from_pixbuf(pixbuf)
|
||||
self._ui.qrcode.show()
|
||||
self._ui.qrinfo.hide()
|
||||
|
||||
def _on_show_inactive(self, switch, param):
|
||||
self._show_inactive = switch.get_active()
|
||||
self._ui.list.invalidate_filter()
|
||||
|
||||
def _on_destroy(self, *args):
|
||||
del self._windows['dialog']
|
||||
|
||||
|
||||
class KeyRow(Gtk.ListBoxRow):
|
||||
def __init__(self, jid, identity_key, trust, last_seen):
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
self.set_activatable(False)
|
||||
|
||||
self._active = False
|
||||
self._device_id = None
|
||||
self._identity_key = identity_key
|
||||
self.trust = trust
|
||||
self.jid = jid
|
||||
|
||||
grid = Gtk.Grid()
|
||||
grid.set_column_spacing(12)
|
||||
|
||||
self._trust_button = TrustButton(self)
|
||||
grid.attach(self._trust_button, 1, 1, 1, 3)
|
||||
|
||||
jid_label = Gtk.Label(label=jid)
|
||||
jid_label.get_style_context().add_class('dim-label')
|
||||
jid_label.set_selectable(False)
|
||||
jid_label.set_halign(Gtk.Align.START)
|
||||
jid_label.set_valign(Gtk.Align.START)
|
||||
jid_label.set_hexpand(True)
|
||||
grid.attach(jid_label, 2, 1, 1, 1)
|
||||
|
||||
self.fingerprint = Gtk.Label(
|
||||
label=self._identity_key.get_fingerprint(formatted=True))
|
||||
self.fingerprint.get_style_context().add_class('omemo-mono')
|
||||
self.fingerprint.get_style_context().add_class('omemo-inactive-color')
|
||||
self.fingerprint.set_selectable(True)
|
||||
self.fingerprint.set_halign(Gtk.Align.START)
|
||||
self.fingerprint.set_valign(Gtk.Align.START)
|
||||
self.fingerprint.set_hexpand(True)
|
||||
grid.attach(self.fingerprint, 2, 2, 1, 1)
|
||||
|
||||
if last_seen is not None:
|
||||
last_seen = time.strftime('%d-%m-%Y %H:%M:%S',
|
||||
time.localtime(last_seen))
|
||||
else:
|
||||
last_seen = _('Never')
|
||||
last_seen_label = Gtk.Label(label=_('Last seen: %s') % last_seen)
|
||||
last_seen_label.set_halign(Gtk.Align.START)
|
||||
last_seen_label.set_valign(Gtk.Align.START)
|
||||
last_seen_label.set_hexpand(True)
|
||||
last_seen_label.get_style_context().add_class('omemo-last-seen')
|
||||
last_seen_label.get_style_context().add_class('dim-label')
|
||||
grid.attach(last_seen_label, 2, 3, 1, 1)
|
||||
|
||||
self.add(grid)
|
||||
self.show_all()
|
||||
|
||||
def delete_fingerprint(self, *args):
|
||||
def _remove():
|
||||
backend = self.get_toplevel()._omemo.backend
|
||||
|
||||
backend.remove_device(self.jid, self.device_id)
|
||||
backend.storage.deleteSession(self.jid, self.device_id)
|
||||
backend.storage.deleteIdentity(self.jid, self._identity_key)
|
||||
|
||||
self.get_parent().remove(self)
|
||||
self.destroy()
|
||||
|
||||
ConfirmationDialog(
|
||||
_('Delete'),
|
||||
_('Delete Fingerprint'),
|
||||
_('Doing so will permanently delete this Fingerprint'),
|
||||
[DialogButton.make('Cancel'),
|
||||
DialogButton.make('Remove',
|
||||
text=_('Delete'),
|
||||
callback=_remove)],
|
||||
transient_for=self.get_toplevel()).show()
|
||||
|
||||
def set_trust(self):
|
||||
icon_name, tooltip, css_class = TRUST_DATA[self.trust]
|
||||
image = self._trust_button.get_child()
|
||||
image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
|
||||
image.get_style_context().add_class(css_class)
|
||||
image.set_tooltip_text(tooltip)
|
||||
|
||||
backend = self.get_toplevel()._omemo.backend
|
||||
backend.storage.setTrust(self.jid, self._identity_key, self.trust)
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
return self._active
|
||||
|
||||
@active.setter
|
||||
def active(self, active):
|
||||
context = self.fingerprint.get_style_context()
|
||||
self._active = bool(active)
|
||||
if self._active:
|
||||
context.remove_class('omemo-inactive-color')
|
||||
else:
|
||||
context.add_class('omemo-inactive-color')
|
||||
self._trust_button.update()
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
return self._device_id
|
||||
|
||||
@device_id.setter
|
||||
def device_id(self, device_id):
|
||||
self._device_id = device_id
|
||||
|
||||
|
||||
class TrustButton(Gtk.MenuButton):
|
||||
def __init__(self, row):
|
||||
Gtk.MenuButton.__init__(self)
|
||||
self._row = row
|
||||
self._css_class = ''
|
||||
self.set_popover(TrustPopver(row))
|
||||
self.set_valign(Gtk.Align.CENTER)
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
icon_name, tooltip, css_class = TRUST_DATA[self._row.trust]
|
||||
image = self.get_child()
|
||||
image.set_from_icon_name(icon_name, Gtk.IconSize.MENU)
|
||||
# Remove old color from icon
|
||||
image.get_style_context().remove_class(self._css_class)
|
||||
|
||||
if not self._row.active:
|
||||
css_class = 'omemo-inactive-color'
|
||||
tooltip = '%s - %s' % (_('Inactive'), tooltip)
|
||||
|
||||
image.get_style_context().add_class(css_class)
|
||||
self._css_class = css_class
|
||||
self.set_tooltip_text(tooltip)
|
||||
|
||||
|
||||
class TrustPopver(Gtk.Popover):
|
||||
def __init__(self, row):
|
||||
Gtk.Popover.__init__(self)
|
||||
self._row = row
|
||||
self._listbox = Gtk.ListBox()
|
||||
self._listbox.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
self.update()
|
||||
self.add(self._listbox)
|
||||
self._listbox.show_all()
|
||||
self._listbox.connect('row-activated', self._activated)
|
||||
self.get_style_context().add_class('omemo-trust-popover')
|
||||
|
||||
def _activated(self, _listbox, row):
|
||||
self.popdown()
|
||||
if row.type_ is None:
|
||||
self._row.delete_fingerprint()
|
||||
else:
|
||||
self._row.trust = row.type_
|
||||
self._row.set_trust()
|
||||
self.get_relative_to().update()
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
self._listbox.foreach(self._listbox.remove)
|
||||
if self._row.trust != Trust.VERIFIED:
|
||||
self._listbox.add(VerifiedOption())
|
||||
if self._row.trust != Trust.BLIND:
|
||||
self._listbox.add(BlindOption())
|
||||
if self._row.trust != Trust.UNTRUSTED:
|
||||
self._listbox.add(NotTrustedOption())
|
||||
self._listbox.add(DeleteOption())
|
||||
|
||||
|
||||
class MenuOption(Gtk.ListBoxRow):
|
||||
def __init__(self):
|
||||
Gtk.ListBoxRow.__init__(self)
|
||||
box = Gtk.Box()
|
||||
box.set_spacing(6)
|
||||
|
||||
image = Gtk.Image.new_from_icon_name(self.icon,
|
||||
Gtk.IconSize.MENU)
|
||||
label = Gtk.Label(label=self.label)
|
||||
image.get_style_context().add_class(self.color)
|
||||
|
||||
box.add(image)
|
||||
box.add(label)
|
||||
self.add(box)
|
||||
self.show_all()
|
||||
|
||||
|
||||
class BlindOption(MenuOption):
|
||||
|
||||
type_ = Trust.BLIND
|
||||
icon = 'security-medium-symbolic'
|
||||
label = _('Blind Trust')
|
||||
color = 'encrypted-color'
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
|
||||
|
||||
class VerifiedOption(MenuOption):
|
||||
|
||||
type_ = Trust.VERIFIED
|
||||
icon = 'security-high-symbolic'
|
||||
label = _('Verified')
|
||||
color = 'encrypted-color'
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
|
||||
|
||||
class NotTrustedOption(MenuOption):
|
||||
|
||||
type_ = Trust.UNTRUSTED
|
||||
icon = 'dialog-error-symbolic'
|
||||
label = _('Untrusted')
|
||||
color = 'error-color'
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
|
||||
|
||||
class DeleteOption(MenuOption):
|
||||
|
||||
type_ = None
|
||||
icon = 'user-trash-symbolic'
|
||||
label = _('Delete')
|
||||
color = ''
|
||||
|
||||
def __init__(self):
|
||||
MenuOption.__init__(self)
|
||||
332
omemo/gtk/key.ui
332
omemo/gtk/key.ui
@@ -1,332 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.36.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<object class="GtkPopover" id="popover">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="constrain_to">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">12</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="own_fingerprint">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="selectable">True</property>
|
||||
<style>
|
||||
<class name="omemo-mono"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="qrcode">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="qrinfo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">For verification via QR-Code
|
||||
you have to install python-qrcode</property>
|
||||
<style>
|
||||
<class name="omemo-qr-not-available"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkPopover" id="search_popover">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="search">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<signal name="search-changed" handler="_on_search_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="infobar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<property name="revealed">False</property>
|
||||
<signal name="response" handler="_on_infobar_response" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">dialog-information-symbolic</property>
|
||||
<property name="icon_size">3</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Click the shield icon to manage trust for each fingerprint.</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="max_width_chars">46</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="btbv_link">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Read more on blind trust.</property>
|
||||
<property name="xalign">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">True</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">0</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="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">18</property>
|
||||
<property name="row_spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="spacing">11</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="omemo_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="header">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<style>
|
||||
<class name="dim-label"/>
|
||||
<class name="bold"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="popover">search_popover</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">270</property>
|
||||
<property name="overlay_scrolling">False</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkListBox" id="list">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="selection_mode">none</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="direction">up</property>
|
||||
<property name="popover">popover</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Own Fingerprint</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkSwitch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<signal name="notify::active" handler="_on_show_inactive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Show inactive</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,53 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from gajim.plugins.helpers import get_builder
|
||||
|
||||
|
||||
class ProgressWindow:
|
||||
def __init__(self, plugin, window, event):
|
||||
self._plugin = plugin
|
||||
self._event = event
|
||||
|
||||
path = self._plugin.local_file_path('gtk/progress.ui')
|
||||
self._ui = get_builder(path)
|
||||
self._ui.progress_dialog.set_transient_for(window)
|
||||
self._ui.progressbar.set_text("")
|
||||
self._ui.progress_dialog.show_all()
|
||||
|
||||
image_path = self._plugin.local_file_path('omemo.png')
|
||||
self._ui.image.set_from_file(image_path)
|
||||
self._ui.connect_signals(self)
|
||||
self._seen = 0
|
||||
|
||||
def set_text(self, text):
|
||||
self._ui.label.set_markup('<big>%s</big>' % text)
|
||||
return False
|
||||
|
||||
def update_progress(self, seen, total):
|
||||
self._seen += seen
|
||||
pct = (self._seen / float(total)) * 100.0
|
||||
self._ui.progressbar.set_fraction(self._seen / float(total))
|
||||
self._ui.progressbar.set_text(str(int(pct)) + "%")
|
||||
return False
|
||||
|
||||
def close_dialog(self, *args):
|
||||
self._ui.progress_dialog.destroy()
|
||||
return False
|
||||
|
||||
def on_destroy(self, *args):
|
||||
self._event.set()
|
||||
@@ -1,123 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkDialog" id="progress_dialog">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="border_width">18</property>
|
||||
<property name="title" translatable="yes">Download</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="icon_name">go-down</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog-vbox">
|
||||
<property name="width_request">250</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">12</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog-action_area11">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment3">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="top_padding">2</property>
|
||||
<property name="bottom_padding">4</property>
|
||||
<property name="right_padding">3</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="close_dialog" swapped="no"/>
|
||||
<signal name="destroy" handler="on_destroy" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImage" id="image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text">OMEMO</property>
|
||||
<property name="margin_right">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="top_padding">8</property>
|
||||
<property name="bottom_padding">4</property>
|
||||
<property name="left_padding">8</property>
|
||||
<property name="right_padding">8</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkAlignment" id="alignment2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="top_padding">4</property>
|
||||
<property name="bottom_padding">4</property>
|
||||
<property name="left_padding">8</property>
|
||||
<property name="right_padding">8</property>
|
||||
<child>
|
||||
<object class="GtkProgressBar" id="progressbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pulse_step">0.10000000149</property>
|
||||
<property name="show_text">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,18 +0,0 @@
|
||||
.omemo-inactive-color { color: @insensitive_fg_color; }
|
||||
|
||||
.omemo-qr-not-available {color: red;}
|
||||
|
||||
.omemo-mono { font-size: 12px; font-family: monospace; }
|
||||
|
||||
.omemo-last-seen { font-size: 11px; }
|
||||
|
||||
.omemo-key-dialog scrolledwindow row {
|
||||
border-bottom: 1px solid;
|
||||
border-color: @unfocused_borders;
|
||||
padding: 10px 20px 10px 10px;
|
||||
}
|
||||
.omemo-key-dialog scrolledwindow row:last-child { border-bottom: 0px}
|
||||
.omemo-key-dialog scrolledwindow { border: 1px solid; border-color:@unfocused_borders; }
|
||||
.omemo-key-dialog list > row { outline: none; }
|
||||
|
||||
.omemo-trust-popover row { padding: 10px 15px 10px 10px; }
|
||||
@@ -1,28 +0,0 @@
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
|
||||
from gajim.common.events import ApplicationEvent
|
||||
|
||||
|
||||
@dataclass
|
||||
class OMEMONewFingerprint(ApplicationEvent):
|
||||
name: str = field(init=False, default='omemo-new-fingerprint')
|
||||
chat_control: Any
|
||||
@@ -1,514 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# XEP-0384: OMEMO Encryption
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
from nbxmpp.protocol import NodeProcessed
|
||||
from nbxmpp.errors import StanzaError
|
||||
from nbxmpp.const import PresenceType
|
||||
from nbxmpp.const import Affiliation
|
||||
from nbxmpp.structs import StanzaHandler
|
||||
from nbxmpp.modules.omemo import create_omemo_message
|
||||
from nbxmpp.modules.omemo import get_key_transport_message
|
||||
from nbxmpp.modules.util import is_error
|
||||
|
||||
from gajim.common import app
|
||||
from gajim.common import configpaths
|
||||
from gajim.common.events import MessageNotSent
|
||||
from gajim.common.const import EncryptionData
|
||||
from gajim.common.const import Trust as GajimTrust
|
||||
from gajim.common.modules.base import BaseModule
|
||||
from gajim.common.modules.util import event_node
|
||||
from gajim.common.modules.util import as_task
|
||||
|
||||
from gajim.plugins.plugins_i18n import _
|
||||
|
||||
from omemo.backend.state import OmemoState
|
||||
from omemo.backend.state import KeyExchangeMessage
|
||||
from omemo.backend.state import SelfMessage
|
||||
from omemo.backend.state import MessageNotForDevice
|
||||
from omemo.backend.state import DecryptionFailed
|
||||
from omemo.backend.state import DuplicateMessage
|
||||
from omemo.backend.util import Trust
|
||||
from omemo.modules.events import OMEMONewFingerprint
|
||||
from omemo.modules.util import prepare_stanza
|
||||
|
||||
|
||||
ALLOWED_TAGS = [
|
||||
('request', Namespace.RECEIPTS),
|
||||
('active', Namespace.CHATSTATES),
|
||||
('gone', Namespace.CHATSTATES),
|
||||
('inactive', Namespace.CHATSTATES),
|
||||
('paused', Namespace.CHATSTATES),
|
||||
('composing', Namespace.CHATSTATES),
|
||||
('markable', Namespace.CHATMARKERS),
|
||||
('no-store', Namespace.HINTS),
|
||||
('store', Namespace.HINTS),
|
||||
('no-copy', Namespace.HINTS),
|
||||
('no-permanent-store', Namespace.HINTS),
|
||||
('replace', Namespace.CORRECT),
|
||||
('thread', None),
|
||||
('origin-id', Namespace.SID),
|
||||
]
|
||||
|
||||
ENCRYPTION_NAME = 'OMEMO'
|
||||
|
||||
# Module name
|
||||
name = 'OMEMO'
|
||||
zeroconf = False
|
||||
|
||||
|
||||
class OMEMO(BaseModule):
|
||||
|
||||
_nbxmpp_extends = 'OMEMO'
|
||||
_nbxmpp_methods = [
|
||||
'set_devicelist',
|
||||
'request_devicelist',
|
||||
'set_bundle',
|
||||
'request_bundle',
|
||||
]
|
||||
|
||||
def __init__(self, client):
|
||||
BaseModule.__init__(self, client, plugin=True)
|
||||
|
||||
self.handlers = [
|
||||
StanzaHandler(name='message',
|
||||
callback=self._message_received,
|
||||
ns=Namespace.OMEMO_TEMP,
|
||||
priority=9),
|
||||
StanzaHandler(name='presence',
|
||||
callback=self._on_muc_user_presence,
|
||||
ns=Namespace.MUC_USER,
|
||||
priority=48),
|
||||
]
|
||||
|
||||
self._register_pubsub_handler(self._devicelist_notification_received)
|
||||
|
||||
self.available = True
|
||||
|
||||
self._own_jid = self._client.get_own_jid().bare
|
||||
self._backend = self._get_backend()
|
||||
|
||||
self._omemo_groupchats = set()
|
||||
self._muc_temp_store = {}
|
||||
self._query_for_bundles = []
|
||||
self._device_bundle_querys = []
|
||||
self._query_for_devicelists = []
|
||||
|
||||
def get_own_jid(self, stripped=False):
|
||||
if stripped:
|
||||
return self._client.get_own_jid().bare
|
||||
return self._client.get_own_jid()
|
||||
|
||||
@property
|
||||
def backend(self):
|
||||
return self._backend
|
||||
|
||||
def _get_backend(self):
|
||||
data_dir = Path(configpaths.get('MY_DATA'))
|
||||
db_path = data_dir / f'omemo_{self._own_jid}.db'
|
||||
return OmemoState(self._own_jid, db_path, self._account, self)
|
||||
|
||||
def is_omemo_groupchat(self, room_jid):
|
||||
return room_jid in self._omemo_groupchats
|
||||
|
||||
def on_signed_in(self):
|
||||
self._log.info('Announce Support after Sign In')
|
||||
self._query_for_bundles = []
|
||||
self.set_bundle()
|
||||
self.request_devicelist()
|
||||
|
||||
def activate(self):
|
||||
""" Method called when the Plugin is activated in the PluginManager
|
||||
"""
|
||||
self._client.get_module('Caps').update_caps()
|
||||
|
||||
if app.account_is_connected(self._account):
|
||||
self._log.info('Announce Support after Plugin Activation')
|
||||
self._query_for_bundles = []
|
||||
self.set_bundle()
|
||||
self.request_devicelist()
|
||||
|
||||
def deactivate(self):
|
||||
""" Method called when the Plugin is deactivated in the PluginManager
|
||||
"""
|
||||
self._query_for_bundles = []
|
||||
|
||||
def encrypt_message(self, conn, event, callback, groupchat):
|
||||
if not event.message:
|
||||
callback(event)
|
||||
return
|
||||
|
||||
omemo_message = self.backend.encrypt(event.jid, event.message)
|
||||
if omemo_message is None:
|
||||
app.ged.raise_event(
|
||||
MessageNotSent(client=conn,
|
||||
jid=event.jid,
|
||||
message=event.message,
|
||||
error=_('Encryption error'),
|
||||
time=time.time()))
|
||||
return
|
||||
|
||||
create_omemo_message(event.stanza, omemo_message,
|
||||
node_whitelist=ALLOWED_TAGS)
|
||||
|
||||
if groupchat:
|
||||
self._muc_temp_store[omemo_message.payload] = event.message
|
||||
else:
|
||||
event.xhtml = None
|
||||
event.encrypted = ENCRYPTION_NAME
|
||||
event.additional_data['encrypted'] = {
|
||||
'name': ENCRYPTION_NAME,
|
||||
'trust': GajimTrust[Trust.VERIFIED.name]}
|
||||
|
||||
self._debug_print_stanza(event.stanza)
|
||||
callback(event)
|
||||
|
||||
def _send_key_transport_message(self, typ, jid, devices):
|
||||
omemo_message = self.backend.encrypt_key_transport(jid, devices)
|
||||
if omemo_message is None:
|
||||
self._log.warning('Key transport message to %s (%s) failed',
|
||||
jid, devices)
|
||||
return
|
||||
|
||||
transport_message = get_key_transport_message(typ, jid, omemo_message)
|
||||
self._log.info('Send key transport message %s (%s)', jid, devices)
|
||||
self._client.connection.send(transport_message)
|
||||
|
||||
def _message_received(self, _con, stanza, properties):
|
||||
if not properties.is_omemo:
|
||||
return
|
||||
|
||||
if properties.is_carbon_message and properties.carbon.is_sent:
|
||||
from_jid = self._own_jid
|
||||
|
||||
elif properties.is_mam_message:
|
||||
from_jid = self._process_mam_message(properties)
|
||||
|
||||
elif properties.from_muc:
|
||||
from_jid = self._process_muc_message(properties)
|
||||
|
||||
else:
|
||||
from_jid = properties.jid.bare
|
||||
|
||||
if from_jid is None:
|
||||
return
|
||||
|
||||
self._log.info('Message received from: %s', from_jid)
|
||||
|
||||
try:
|
||||
plaintext, fingerprint, trust = self.backend.decrypt_message(
|
||||
properties.omemo, from_jid)
|
||||
except (KeyExchangeMessage, DuplicateMessage):
|
||||
raise NodeProcessed
|
||||
|
||||
except SelfMessage:
|
||||
if not properties.from_muc:
|
||||
raise NodeProcessed
|
||||
|
||||
if properties.omemo.payload not in self._muc_temp_store:
|
||||
self._log.warning("Can't decrypt own GroupChat Message")
|
||||
return
|
||||
|
||||
plaintext = self._muc_temp_store[properties.omemo.payload]
|
||||
fingerprint = self.backend.own_fingerprint
|
||||
trust = Trust.VERIFIED
|
||||
del self._muc_temp_store[properties.omemo.payload]
|
||||
|
||||
except DecryptionFailed:
|
||||
return
|
||||
|
||||
except MessageNotForDevice:
|
||||
if properties.omemo.payload is None:
|
||||
# Key Transport message for another device
|
||||
return
|
||||
|
||||
plaintext = _('This message was encrypted with OMEMO, '
|
||||
'but not for your device.')
|
||||
# Neither trust nor fingerprint can be verified if we didn't
|
||||
# successfully decrypt the message
|
||||
trust = Trust.UNTRUSTED
|
||||
fingerprint = None
|
||||
|
||||
prepare_stanza(stanza, plaintext)
|
||||
self._debug_print_stanza(stanza)
|
||||
properties.encrypted = EncryptionData({'name': ENCRYPTION_NAME,
|
||||
'fingerprint': fingerprint,
|
||||
'trust': GajimTrust[trust.name]})
|
||||
|
||||
def _process_muc_message(self, properties):
|
||||
resource = properties.jid.resource
|
||||
if properties.muc_ofrom is not None:
|
||||
# History Message from MUC
|
||||
return properties.muc_ofrom.bare
|
||||
|
||||
contact = self._client.get_module('Contacts').get_contact(
|
||||
properties.jid)
|
||||
if contact.real_jid is not None:
|
||||
return contact.real_jid.bare
|
||||
|
||||
self._log.info('Groupchat: Last resort trying to find SID in DB')
|
||||
from_jid = self.backend.storage.getJidFromDevice(properties.omemo.sid)
|
||||
if not from_jid:
|
||||
self._log.error("Can't decrypt GroupChat Message from %s", resource)
|
||||
return
|
||||
return from_jid
|
||||
|
||||
def _process_mam_message(self, properties):
|
||||
self._log.info('Message received, archive: %s', properties.mam.archive)
|
||||
if properties.from_muc:
|
||||
self._log.info('MUC MAM Message received')
|
||||
if properties.muc_user is None or properties.muc_user.jid is None:
|
||||
self._log.warning('Received MAM Message which can '
|
||||
'not be mapped to a real jid')
|
||||
return
|
||||
return properties.muc_user.jid.bare
|
||||
return properties.from_.bare
|
||||
|
||||
def _on_muc_user_presence(self, _con, _stanza, properties):
|
||||
if properties.type == PresenceType.ERROR:
|
||||
return
|
||||
|
||||
if properties.is_muc_destroyed:
|
||||
return
|
||||
|
||||
room = properties.jid.bare
|
||||
|
||||
if properties.muc_user is None or properties.muc_user.jid is None:
|
||||
# No real jid found
|
||||
return
|
||||
|
||||
jid = properties.muc_user.jid.bare
|
||||
if properties.muc_user.affiliation in (Affiliation.OUTCAST,
|
||||
Affiliation.NONE):
|
||||
self.backend.remove_muc_member(room, jid)
|
||||
else:
|
||||
self.backend.add_muc_member(room, jid)
|
||||
|
||||
if self.is_omemo_groupchat(room):
|
||||
if not self.is_contact_in_roster(jid):
|
||||
# Query Devicelists from JIDs not in our Roster
|
||||
self._log.info('%s not in Roster, query devicelist...', jid)
|
||||
self.request_devicelist(jid)
|
||||
|
||||
def get_affiliation_list(self, room_jid):
|
||||
for affiliation in ('owner', 'admin', 'member'):
|
||||
self._nbxmpp('MUC').get_affiliation(
|
||||
room_jid,
|
||||
affiliation,
|
||||
callback=self._on_affiliations_received,
|
||||
user_data=room_jid)
|
||||
|
||||
def _on_affiliations_received(self, task):
|
||||
room_jid = task.get_user_data()
|
||||
try:
|
||||
result = task.finish()
|
||||
except StanzaError as error:
|
||||
self._log.info('Affiliation request failed: %s', error)
|
||||
return
|
||||
|
||||
for user_jid in result.users:
|
||||
jid = str(user_jid)
|
||||
self.backend.add_muc_member(room_jid, jid)
|
||||
|
||||
if not self.is_contact_in_roster(jid):
|
||||
# Query Devicelists from JIDs not in our Roster
|
||||
self._log.info('%s not in Roster, query devicelist...', jid)
|
||||
self.request_devicelist(jid)
|
||||
|
||||
def is_contact_in_roster(self, jid):
|
||||
if jid == self._own_jid:
|
||||
return True
|
||||
|
||||
roster_item = self._client.get_module('Roster').get_item(jid)
|
||||
if roster_item is None:
|
||||
return False
|
||||
|
||||
contact = self._client.get_module('Contacts').get_contact(jid)
|
||||
return contact.subscription == 'both'
|
||||
|
||||
def on_muc_disco_update(self, event):
|
||||
self._check_if_omemo_capable(event.jid)
|
||||
|
||||
def on_room_joined(self, contact):
|
||||
jid = str(contact.jid)
|
||||
self._check_if_omemo_capable(jid)
|
||||
if self.is_omemo_groupchat(jid):
|
||||
self.get_affiliation_list(jid)
|
||||
|
||||
def _check_if_omemo_capable(self, jid):
|
||||
disco_info = app.storage.cache.get_last_disco_info(jid)
|
||||
if disco_info.muc_is_members_only and disco_info.muc_is_nonanonymous:
|
||||
self._log.info('OMEMO room discovered: %s', jid)
|
||||
self._omemo_groupchats.add(jid)
|
||||
else:
|
||||
self._log.info('OMEMO room removed due to config change: %s', jid)
|
||||
self._omemo_groupchats.discard(jid)
|
||||
|
||||
def _check_for_missing_sessions(self, jid):
|
||||
devices_without_session = self.backend.devices_without_sessions(jid)
|
||||
for device_id in devices_without_session:
|
||||
if device_id in self._device_bundle_querys:
|
||||
continue
|
||||
self._device_bundle_querys.append(device_id)
|
||||
self.request_bundle(jid, device_id)
|
||||
|
||||
def are_keys_missing(self, contact_jid):
|
||||
""" Checks if devicekeys are missing and queries the
|
||||
bundles
|
||||
|
||||
Parameters
|
||||
----------
|
||||
contact_jid : str
|
||||
bare jid of the contact
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Returns True if there are no trusted Fingerprints
|
||||
"""
|
||||
|
||||
# Fetch Bundles of own other Devices
|
||||
if self._own_jid not in self._query_for_bundles:
|
||||
|
||||
devices_without_session = self.backend \
|
||||
.devices_without_sessions(self._own_jid)
|
||||
|
||||
self._query_for_bundles.append(self._own_jid)
|
||||
|
||||
if devices_without_session:
|
||||
for device_id in devices_without_session:
|
||||
self.request_bundle(self._own_jid, device_id)
|
||||
|
||||
# Fetch Bundles of contacts devices
|
||||
if contact_jid not in self._query_for_bundles:
|
||||
|
||||
devices_without_session = self.backend \
|
||||
.devices_without_sessions(contact_jid)
|
||||
|
||||
self._query_for_bundles.append(contact_jid)
|
||||
|
||||
if devices_without_session:
|
||||
for device_id in devices_without_session:
|
||||
self.request_bundle(contact_jid, device_id)
|
||||
|
||||
if self.backend.has_trusted_keys(contact_jid):
|
||||
return False
|
||||
return True
|
||||
|
||||
def set_bundle(self):
|
||||
self._nbxmpp('OMEMO').set_bundle(self.backend.bundle,
|
||||
self.backend.own_device)
|
||||
|
||||
@as_task
|
||||
def request_bundle(self, jid, device_id):
|
||||
_task = yield
|
||||
|
||||
self._log.info('Fetch device bundle %s %s', device_id, jid)
|
||||
|
||||
bundle = yield self._nbxmpp('OMEMO').request_bundle(
|
||||
jid,
|
||||
device_id)
|
||||
|
||||
if is_error(bundle) or bundle is None:
|
||||
self._log.info('Bundle request failed: %s %s: %s',
|
||||
jid, device_id, bundle)
|
||||
return
|
||||
|
||||
self.backend.build_session(jid, device_id, bundle)
|
||||
self._log.info('Session created for: %s', jid)
|
||||
# TODO: In MUC we should send a groupchat message
|
||||
self._send_key_transport_message('chat', jid, [device_id])
|
||||
|
||||
# Trigger dialog to trust new Fingerprints if
|
||||
# the Chat Window is Open
|
||||
|
||||
# TODO: This does not work anymore
|
||||
# ctrl = app.window.get_control(self._account, jid)
|
||||
# if ctrl:
|
||||
# app.ged.raise_event(OMEMONewFingerprint(chat_control=ctrl))
|
||||
|
||||
def set_devicelist(self, devicelist=None):
|
||||
devicelist_ = set([self.backend.own_device])
|
||||
if devicelist is not None:
|
||||
devicelist_.update(devicelist)
|
||||
self._log.info('Publishing own devicelist: %s', devicelist_)
|
||||
self._nbxmpp('OMEMO').set_devicelist(devicelist_)
|
||||
|
||||
def clear_devicelist(self):
|
||||
self.backend.update_devicelist(self._own_jid, [self.backend.own_device])
|
||||
self.set_devicelist()
|
||||
|
||||
@as_task
|
||||
def request_devicelist(self, jid=None):
|
||||
_task = yield
|
||||
|
||||
if jid is None:
|
||||
jid = self._own_jid
|
||||
|
||||
if jid in self._query_for_devicelists:
|
||||
return
|
||||
|
||||
self._query_for_devicelists.append(jid)
|
||||
|
||||
devicelist = yield self._nbxmpp('OMEMO').request_devicelist(jid=jid)
|
||||
if is_error(devicelist) or devicelist is None:
|
||||
self._log.info('Devicelist request failed: %s %s', jid, devicelist)
|
||||
devicelist = []
|
||||
|
||||
self._process_devicelist_update(jid, devicelist)
|
||||
|
||||
@event_node(Namespace.OMEMO_TEMP_DL)
|
||||
def _devicelist_notification_received(self, _con, _stanza, properties):
|
||||
if properties.pubsub_event.retracted:
|
||||
return
|
||||
|
||||
devicelist = properties.pubsub_event.data or []
|
||||
|
||||
self._process_devicelist_update(str(properties.jid), devicelist)
|
||||
|
||||
def _process_devicelist_update(self, jid, devicelist):
|
||||
own_devices = jid is None or self._client.get_own_jid().bare_match(jid)
|
||||
if own_devices:
|
||||
jid = self._own_jid
|
||||
|
||||
self._log.info('Received device list for %s: %s', jid, devicelist)
|
||||
# Pass a copy, we need the full list for potential set_devicelist()
|
||||
self.backend.update_devicelist(jid, list(devicelist))
|
||||
|
||||
if jid in self._query_for_bundles:
|
||||
self._query_for_bundles.remove(jid)
|
||||
|
||||
if own_devices:
|
||||
if not self.backend.is_own_device_published:
|
||||
# Our own device_id is not in the list, it could be
|
||||
# overwritten by some other client
|
||||
self.set_devicelist(devicelist)
|
||||
|
||||
self._check_for_missing_sessions(jid)
|
||||
|
||||
def _debug_print_stanza(self, stanza):
|
||||
stanzastr = '\n' + stanza.__str__(fancy=True)
|
||||
stanzastr = stanzastr[0:-1]
|
||||
self._log.debug(stanzastr)
|
||||
|
||||
|
||||
def get_instance(*args, **kwargs):
|
||||
return OMEMO(*args, **kwargs), 'OMEMO'
|
||||
@@ -1,30 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
|
||||
def prepare_stanza(stanza, plaintext):
|
||||
delete_nodes(stanza, 'encrypted', Namespace.OMEMO_TEMP)
|
||||
delete_nodes(stanza, 'body')
|
||||
stanza.setBody(plaintext)
|
||||
|
||||
|
||||
def delete_nodes(stanza, name, namespace=None):
|
||||
nodes = stanza.getTags(name, namespace=namespace)
|
||||
for node in nodes:
|
||||
stanza.delChild(node)
|
||||
BIN
omemo/omemo.png
BIN
omemo/omemo.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 688 B |
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="addon">
|
||||
<id>org.gajim.Gajim.Plugin.omemo</id>
|
||||
<extends>org.gajim.Gajim</extends>
|
||||
<name>OMEMO Plugin</name>
|
||||
<summary>XMPP Extension Protocol (XEP) for secure multi-client end-to-end encryption</summary>
|
||||
<url type="homepage">https://gajim.org/</url>
|
||||
<metadata_license>CC-BY-SA-3.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<update_contact>gajim-devel_AT_gajim.org</update_contact>
|
||||
</component>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"authors": [
|
||||
"Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>",
|
||||
"Daniel Gultsch <daniel@gultsch.de>",
|
||||
"Philipp Hörist <philipp@hoerist.com>"
|
||||
],
|
||||
"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 dependencies, detailed in the installation instructions for your system in the GitLab wiki.",
|
||||
"homepage": "https://dev.gajim.org/gajim/gajim-plugins/wikis/OmemoGajimPlugin",
|
||||
"config_dialog": true,
|
||||
"name": "OMEMO",
|
||||
"platforms": [
|
||||
"others",
|
||||
"linux",
|
||||
"darwin",
|
||||
"win32"
|
||||
],
|
||||
"requirements": [
|
||||
"gajim>=1.5.0"
|
||||
],
|
||||
"short_name": "omemo",
|
||||
"version": "2.9.0"
|
||||
}
|
||||
342
omemo/plugin.py
342
omemo/plugin.py
@@ -1,342 +0,0 @@
|
||||
# Copyright (C) 2019 Philipp Hörist <philipp AT hoerist.com>
|
||||
# Copyright (C) 2015 Bahtiar `kalkin-` Gadimov <bahtiar@gadimov.de>
|
||||
# Copyright (C) 2015 Daniel Gultsch <daniel@cgultsch.de>
|
||||
#
|
||||
# This file is part of OMEMO Gajim Plugin.
|
||||
#
|
||||
# OMEMO Gajim 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; version 3 only.
|
||||
#
|
||||
# OMEMO Gajim Plugin 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 OMEMO Gajim Plugin. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
import binascii
|
||||
import threading
|
||||
from enum import IntEnum, unique
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
|
||||
from gi.repository import GLib
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
from nbxmpp.namespaces import Namespace
|
||||
|
||||
from gajim.common import app, ged
|
||||
from gajim.common.modules.contacts import GroupchatContact
|
||||
|
||||
from gajim.gtk.dialogs import ErrorDialog
|
||||
|
||||
from gajim.plugins import GajimPlugin
|
||||
from gajim.plugins.plugins_i18n import _
|
||||
|
||||
AXOLOTL_MISSING = 'You are missing Python3-Axolotl or use an outdated version'
|
||||
PROTOBUF_MISSING = "OMEMO can't import Google Protobuf, you can find help in " \
|
||||
"the GitLab Wiki"
|
||||
ERROR_MSG = ''
|
||||
|
||||
|
||||
log = logging.getLogger('gajim.p.omemo')
|
||||
if log.getEffectiveLevel() == logging.DEBUG:
|
||||
log_axolotl = logging.getLogger('axolotl')
|
||||
log_axolotl.setLevel(logging.DEBUG)
|
||||
log_axolotl.addHandler(logging.StreamHandler())
|
||||
log_axolotl.propagate = False
|
||||
|
||||
try:
|
||||
import google.protobuf
|
||||
except Exception as error:
|
||||
log.error(error)
|
||||
ERROR_MSG = PROTOBUF_MISSING
|
||||
|
||||
try:
|
||||
import axolotl
|
||||
except Exception as error:
|
||||
log.error(error)
|
||||
ERROR_MSG = AXOLOTL_MISSING
|
||||
|
||||
if not ERROR_MSG:
|
||||
try:
|
||||
from omemo.modules import omemo
|
||||
from omemo.gtk.key import KeyDialog
|
||||
from omemo.gtk.config import OMEMOConfigDialog
|
||||
from omemo.backend.aes import aes_encrypt_file
|
||||
except Exception as error:
|
||||
log.error(error)
|
||||
ERROR_MSG = 'Error: %s' % error
|
||||
|
||||
|
||||
@unique
|
||||
class UserMessages(IntEnum):
|
||||
QUERY_DEVICES = 0
|
||||
NO_FINGERPRINTS = 1
|
||||
UNDECIDED_FINGERPRINTS = 2
|
||||
|
||||
|
||||
class OmemoPlugin(GajimPlugin):
|
||||
def init(self):
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
if ERROR_MSG:
|
||||
self.activatable = False
|
||||
self.available_text = ERROR_MSG
|
||||
self.config_dialog = None
|
||||
return
|
||||
self.encryption_name = 'OMEMO'
|
||||
self.allow_groupchat = True
|
||||
self.events_handlers = {
|
||||
'omemo-new-fingerprint': (ged.PRECORE, self._on_new_fingerprints),
|
||||
'signed-in': (ged.PRECORE, self._on_signed_in),
|
||||
'muc-disco-update': (ged.GUI1, self._on_muc_disco_update),
|
||||
'muc-added': (ged.GUI1, self._on_muc_added),
|
||||
}
|
||||
self.modules = [omemo]
|
||||
|
||||
self.config_dialog = partial(OMEMOConfigDialog, self)
|
||||
self.gui_extension_points = {
|
||||
'encrypt' + self.encryption_name: (self._encrypt_message, None),
|
||||
'gc_encrypt' + self.encryption_name: (
|
||||
self._muc_encrypt_message, None),
|
||||
'send_message' + self.encryption_name: (
|
||||
self._before_sendmessage, None),
|
||||
'encryption_dialog' + self.encryption_name: (
|
||||
self._on_encryption_button_clicked, None),
|
||||
'encryption_state' + self.encryption_name: (
|
||||
self._encryption_state, None),
|
||||
'update_caps': (self._update_caps, None),
|
||||
}
|
||||
|
||||
self.disabled_accounts = []
|
||||
self._windows = {}
|
||||
|
||||
self.config_default_values = {
|
||||
'DISABLED_ACCOUNTS': ([], ''),
|
||||
'BLIND_TRUST': (True, ''),
|
||||
'SHOW_HELP_FINGERPRINTS': (True, ''),
|
||||
}
|
||||
|
||||
for account in self.config['DISABLED_ACCOUNTS']:
|
||||
self.disabled_accounts.append(account)
|
||||
|
||||
self._load_css()
|
||||
|
||||
def _is_enabled_account(self, account):
|
||||
if account in self.disabled_accounts:
|
||||
return False
|
||||
if account == 'Local':
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_omemo(account):
|
||||
return app.get_client(account).get_module('OMEMO')
|
||||
|
||||
@staticmethod
|
||||
def _load_css():
|
||||
path = Path(__file__).parent / 'gtk' / 'style.css'
|
||||
try:
|
||||
with path.open("r") as file:
|
||||
css = file.read()
|
||||
except Exception as exc:
|
||||
log.error('Error loading css: %s', exc)
|
||||
return
|
||||
|
||||
try:
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_data(bytes(css.encode('utf-8')))
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
|
||||
provider, 610)
|
||||
except Exception:
|
||||
log.exception('Error loading application css')
|
||||
|
||||
def activate(self):
|
||||
"""
|
||||
Method called when the Plugin is activated in the PluginManager
|
||||
"""
|
||||
for account in app.settings.get_active_accounts():
|
||||
if not self._is_enabled_account(account):
|
||||
continue
|
||||
self.get_omemo(account).activate()
|
||||
|
||||
def deactivate(self):
|
||||
"""
|
||||
Method called when the Plugin is deactivated in the PluginManager
|
||||
"""
|
||||
for account in app.settings.get_active_accounts():
|
||||
if not self._is_enabled_account(account):
|
||||
continue
|
||||
self.get_omemo(account).deactivate()
|
||||
|
||||
def _on_signed_in(self, event):
|
||||
if not self._is_enabled_account(event.account):
|
||||
return
|
||||
self.get_omemo(event.account).on_signed_in()
|
||||
|
||||
def _on_muc_disco_update(self, event):
|
||||
if not self._is_enabled_account(event.account):
|
||||
return
|
||||
self.get_omemo(event.account).on_muc_disco_update(event)
|
||||
|
||||
def _on_room_joined(self, contact, _signal_name: str):
|
||||
if not self._is_enabled_account(contact.account):
|
||||
return
|
||||
self.get_omemo(contact.account).on_room_joined(contact)
|
||||
|
||||
def _update_caps(self, account, features):
|
||||
if not self._is_enabled_account(account):
|
||||
return
|
||||
features.append('%s+notify' % Namespace.OMEMO_TEMP_DL)
|
||||
|
||||
@staticmethod
|
||||
def activate_encryption(chat_control):
|
||||
return True
|
||||
|
||||
def _muc_encrypt_message(self, conn, obj, callback):
|
||||
account = conn.name
|
||||
if not self._is_enabled_account(account):
|
||||
return
|
||||
self.get_omemo(account).encrypt_message(conn, obj, callback, True)
|
||||
|
||||
def _encrypt_message(self, conn, obj, callback):
|
||||
account = conn.name
|
||||
if not self._is_enabled_account(account):
|
||||
return
|
||||
self.get_omemo(account).encrypt_message(conn, obj, callback, False)
|
||||
|
||||
def encrypt_file(self, file, _account, callback):
|
||||
thread = threading.Thread(target=self._encrypt_file_thread,
|
||||
args=(file, callback))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
@staticmethod
|
||||
def _encrypt_file_thread(file, callback, *args, **kwargs):
|
||||
result = aes_encrypt_file(file.get_data())
|
||||
file.size = len(result.payload)
|
||||
fragment = binascii.hexlify(result.iv + result.key).decode()
|
||||
file.set_uri_transform_func(
|
||||
lambda uri: 'aesgcm%s#%s' % (uri[5:], fragment))
|
||||
file.set_encrypted_data(result.payload)
|
||||
GLib.idle_add(callback, file)
|
||||
|
||||
@staticmethod
|
||||
def _encryption_state(_chat_control, state):
|
||||
state['visible'] = True
|
||||
state['authenticated'] = True
|
||||
|
||||
def _on_encryption_button_clicked(self, chat_control):
|
||||
self._show_fingerprint_window(chat_control)
|
||||
|
||||
def _before_sendmessage(self, chat_control):
|
||||
account = chat_control.account
|
||||
if not self._is_enabled_account(account):
|
||||
return
|
||||
contact = chat_control.contact
|
||||
omemo = self.get_omemo(account)
|
||||
self.new_fingerprints_available(chat_control)
|
||||
if chat_control.is_groupchat:
|
||||
room = chat_control.room_jid
|
||||
if not omemo.is_omemo_groupchat(room):
|
||||
ErrorDialog(
|
||||
_('Bad Configuration'),
|
||||
_('To use OMEMO in a Groupchat, the Groupchat should be'
|
||||
' non-anonymous and members-only.'))
|
||||
chat_control.sendmessage = False
|
||||
return
|
||||
|
||||
missing = True
|
||||
for jid in omemo.backend.get_muc_members(room):
|
||||
if not omemo.are_keys_missing(jid):
|
||||
missing = False
|
||||
if missing:
|
||||
log.info('%s => No Trusted Fingerprints for %s',
|
||||
account, room)
|
||||
self.print_message(chat_control, UserMessages.NO_FINGERPRINTS)
|
||||
chat_control.sendmessage = False
|
||||
else:
|
||||
# check if we have devices for the contact
|
||||
if not omemo.backend.get_devices(contact.jid, without_self=True):
|
||||
omemo.request_devicelist(contact.jid)
|
||||
self.print_message(chat_control, UserMessages.QUERY_DEVICES)
|
||||
chat_control.sendmessage = False
|
||||
return
|
||||
# check if bundles are missing for some devices
|
||||
if omemo.backend.storage.hasUndecidedFingerprints(contact.jid):
|
||||
log.info('%s => Undecided Fingerprints for %s',
|
||||
account, contact.jid)
|
||||
self.print_message(chat_control, UserMessages.UNDECIDED_FINGERPRINTS)
|
||||
chat_control.sendmessage = False
|
||||
else:
|
||||
log.debug('%s => Sending Message to %s',
|
||||
account, contact.jid)
|
||||
|
||||
def _on_new_fingerprints(self, event):
|
||||
self.new_fingerprints_available(event.chat_control)
|
||||
|
||||
def new_fingerprints_available(self, chat_control):
|
||||
jid = chat_control.contact.jid
|
||||
account = chat_control.account
|
||||
omemo = self.get_omemo(account)
|
||||
if chat_control.is_groupchat:
|
||||
for jid_ in omemo.backend.get_muc_members(chat_control.room_jid,
|
||||
without_self=False):
|
||||
fingerprints = omemo.backend.storage.getNewFingerprints(jid_)
|
||||
if fingerprints:
|
||||
self._show_fingerprint_window(
|
||||
chat_control, fingerprints)
|
||||
break
|
||||
else:
|
||||
fingerprints = omemo.backend.storage.getNewFingerprints(jid)
|
||||
if fingerprints:
|
||||
self._show_fingerprint_window(
|
||||
chat_control, fingerprints)
|
||||
|
||||
def _show_fingerprint_window(self, chat_control, fingerprints=None):
|
||||
contact = chat_control.contact
|
||||
account = chat_control.account
|
||||
omemo = self.get_omemo(account)
|
||||
|
||||
if 'dialog' not in self._windows:
|
||||
self._windows['dialog'] = \
|
||||
KeyDialog(self, contact, app.window,
|
||||
self._windows, groupchat=chat_control.is_groupchat)
|
||||
if fingerprints:
|
||||
log.debug('%s => Showing Fingerprint Prompt for %s',
|
||||
account, contact.jid)
|
||||
omemo.backend.storage.setShownFingerprints(fingerprints)
|
||||
else:
|
||||
self._windows['dialog'].present()
|
||||
self._windows['dialog'].update()
|
||||
if fingerprints:
|
||||
omemo.backend.storage.setShownFingerprints(fingerprints)
|
||||
|
||||
@staticmethod
|
||||
def print_message(chat_control, kind):
|
||||
msg = None
|
||||
if kind == UserMessages.QUERY_DEVICES:
|
||||
msg = _('No devices found. Query in progress...')
|
||||
elif kind == UserMessages.NO_FINGERPRINTS:
|
||||
msg = _('To send an encrypted message, you have to '
|
||||
'first trust the fingerprint of your contact!')
|
||||
elif kind == UserMessages.UNDECIDED_FINGERPRINTS:
|
||||
msg = _('You have undecided fingerprints')
|
||||
if msg is None:
|
||||
return
|
||||
chat_control.add_info_message(msg)
|
||||
|
||||
def _on_muc_added(self, event):
|
||||
client = app.get_client(event.account)
|
||||
contact = client.get_module('Contacts').get_contact(event.jid)
|
||||
if not isinstance(contact, GroupchatContact):
|
||||
log.warning('%s is not a groupchat contact', contact)
|
||||
return
|
||||
|
||||
# Event is triggert on every join, avoid multiple connects
|
||||
contact.disconnect_all_from_obj(self)
|
||||
contact.connect('room-joined', self._on_room_joined)
|
||||
Reference in New Issue
Block a user