Compare commits

...

96 commits
ci ... main

Author SHA1 Message Date
c16d3f1cd9
Bump version to 0.9.0
I would have bumped to 0.8.1 only for a deprecation notice fix, but it
seems poezio breaks without this fix for some users..

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-10-19 21:52:14 +02:00
da3c74bd36
Add Python 3.11 in classifiers
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-10-19 21:41:58 +02:00
02b3380ac3
Replace ensure_future calls with create_task
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-10-19 21:40:33 +02:00
fa3e690154
Use asyncio.create_task to fix asyncio.wait deprecation notice
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-10-19 21:40:03 +02:00
83dd5d62c4
Bump version to 0.8.0
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-08-23 21:26:59 +02:00
3411d5f673
Update version requirements for OMEMO lib
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-08-23 21:26:06 +02:00
73bc6b7803
my_fingerprint: public_bundle.serialize isn't awaitable
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-07-11 12:15:54 +02:00
4bc8c5e6b6
get_devices and get_active_devices return Iterable[int] now
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-07-11 10:56:54 +02:00
cda11a82bc
Await get_active_device call in fetch_bundles
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-07-11 10:51:09 +02:00
8e9add345a
Expose fetch_devices and fetch_bundles
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-07-10 22:31:25 +02:00
dccb877b41
Release 0.7.0
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-04-03 20:38:25 +02:00
7324193966
Don't expose fetch_devices and fetch_bundle
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-04-03 20:36:37 +02:00
882b4d2294 Rename make_heartbeat to send_heartbeat
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:43 +01:00
c8341e0f83 Make fetch_devices and fetch_bundle public
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:43 +01:00
3a85411df8 f-strings
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:43 +01:00
13052817bf Remove get_device_list in favor of get_devices and get_active_devices
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:43 +01:00
48b0610f89 delete_session: new API
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:41 +01:00
284f49714e
Update changelog for py.typed
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:16:16 +01:00
4bfeb6b002 is_encrypted: now a staticmethod
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 20:07:25 +01:00
f4d2412443
encrypt: pass bundle in to encryptRatchetForwarding if available
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 19:50:44 +01:00
2c4dc24b84
decrypt: ensure_future is no more required
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 19:50:11 +01:00
9947fdbb2e
Add py.typed
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-24 15:56:36 +01:00
f932be5a4f
setup.py: Remove Python 3.{7,8}
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-14 22:49:11 +01:00
61f87c1e10
CI: Remove Python 3.{7,8}
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-14 16:19:15 +01:00
33e4f7c84b
0.6.1 release
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-14 16:18:25 +01:00
3cf3cebcaf
Update version requirements
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-14 16:16:29 +01:00
9c547be9aa
Release version 0.6.0
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:42:32 +01:00
9a5cc71b14
echo_bot: re-add space before echoing unencrypted message
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:38:56 +01:00
b586b62558
README: mention example bot
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:33:28 +01:00
0029d5114b
Update ChangeLog
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:28:26 +01:00
e4c9b54b85
Ensure heartbeats are stored in the archive
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:27:15 +01:00
320105988a
echo_bot: Remove useless returns
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:26:51 +01:00
a495d7d7c8
echo_bot: use f-strings where possible
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:23:43 +01:00
31380cd726
echo_bot: reply as plain if NOT encrypted
important detail. Got lost in translation between rewrites with the
Callback/CoroutineCallback

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:21:28 +01:00
89bc05b0c3 echo_bot: Removed unused module
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:11:55 +01:00
f397b0e8d7 echo_bot: Use CoroutineCallback rather than Callback
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-12 01:11:42 +01:00
dbeaca6b6a
typo in decryptRatchetForwardingMessage method name
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-11 23:43:35 +01:00
ec5fe90bce
echo_bot: Also match on heartbeats
The default slixmpp 'message' handler only matches on messages including
a body, which causes issues with heartbeats as they should be processed
by the OMEMO lib as well.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-11 13:32:30 +01:00
be5df8658b
echo_bot: Remove chain_length command
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-10 14:39:13 +01:00
488c254523
Add logging in _should_heartbeat
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-10 14:23:43 +01:00
a6ce12c0b3
encryptRatchetForwardingMessage: missing device_id param
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-10 13:41:02 +01:00
f00580e268
encrypt*Message: use named parameter for bundles
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-10 13:04:45 +01:00
05e6ff3b8e
encryptRatchetForwarding doesn't have an expect_problems parameter anymore
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-10 13:02:13 +01:00
3e92fc0516
encryptRatchetForwardingMessage: bundle isn't required for our use-cases
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-04 12:34:19 +01:00
6ab8bba4f0
Use new encryptRatchetForwardingMessage API added in f3c3a45e
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-03 20:02:00 +01:00
bf3f5472f7
Rename variable so types don't conflict
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-03 19:58:21 +01:00
a7e969b078
should_heartbeat: Ensure prekey variable is always availabe in scope
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-02 00:32:47 +01:00
3681856d54
Please linter, change import order
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-01 22:49:24 +01:00
c936703941
Only call should_heartbeat if auto_heartbeat is enabled
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-03-01 22:47:12 +01:00
f1750d6df3
CI: install omemo and omemo-backend-signal
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-23 11:43:19 +01:00
bb2426a534
Require upstream versions of omemo and omemo-backend-signal
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-23 11:42:30 +01:00
02b6afe10d
Add debug logging, lots
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2022-02-18 14:25:26 +01:00
2a9bade333 Rename short variable
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
28fe0d04c7 Remove unused variable in for loop
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
9e67d7d887 Use the new receiving_chain_length endpoint
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
80cdab3ba3 is_encrypted doesn't require the plugin to be instanciated
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
8e44c07aed Specify local=False parameter to ['xep_0030'].get_info
The API here might have changed? It seems this call was going just fine
until it wasn't. It was returning an empty string. Adding `local=False`
seems to force it to fetch the info remotely and return a proper Iq.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
29bf6e8650 Update omemo lib to 0.13 and asyncio changes
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-16 01:06:26 +01:00
080a27e7d8
Add 0030 as a direct dependency
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-15 22:50:10 +01:00
272ea80581
CI: Add py3.{9,10}
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-15 01:13:45 +01:00
cd5a09d2f0 Update ChangeLog
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-15 01:06:38 +01:00
aa91f43aa0 Merge branch 'decrypt-heartbeats' into 'main'
Don't fail on decrypting heartbeats

See merge request poezio/slixmpp-omemo!12
2021-12-15 01:03:15 +01:00
e00c646d95
echo_client: Handle decrypt_message's Optional-ness
Also comment about it

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-15 00:56:19 +01:00
05b5705f22
Don't fail on decrypting heartbeats
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-12-14 17:20:16 +01:00
494899bb3c
Add interface semi-hidden to RatchetForwardingMessage
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-18 00:30:07 +02:00
26665d9e6a should_heartbeat: factor out in internal method to avoid reparsing message
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-17 20:18:47 +02:00
bb52d93241
should_heartbeat: also return True on new sessions
And the docstring now reflects the reality again!

We're parsing the Encrypted dict again, when we just did it in
decrypt_message above, but this function is also part of the API and
doing that for them is the least we can do.

Maybe there should be an internal function that we can call from
decrypt_message, that also gets called by should_heartbeat.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-17 18:58:31 +02:00
91a04000d7
Treat receiving chain length None as 0
It appears a receiving chain length set to None just means we haven't
received any message for this session yet. It doesn't make sense to spam
heartbeats at this stage

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-17 11:31:36 +02:00
baf29cb05f
should_heartbeat: correctly return what last commit said
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-17 03:39:48 +02:00
c7a0a092d4 should_heartbeat: also take into account unacked sessions
receiving_chain_length would sometimes be None (thanks python strict
typing) causing the thing to fail.

When this is the case, I assume this means the session hasn't been
confirmed from our side yet and it would be good to ACK it. (To be
confirmed with people who know, in progress).

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-17 02:56:57 +02:00
a2a287ee5d
don't include empty sessions in _chain_lengths method
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-16 21:49:55 +02:00
7a887ccac3 Pass correct argument type to should_heartbeat call
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-16 21:37:56 +02:00
59543ac585
Add missing argument to should_heartbeat call
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-16 21:31:57 +02:00
a8ce75551a
Update ChangeLog
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-15 02:36:13 +02:00
311292a0be Merge branch 'initial-publish' into 'master'
Fix #10: ensure device list and bundle are published after startup

Closes #10

See merge request poezio/slixmpp-omemo!11
2021-07-15 00:48:58 +02:00
aa54f58649 Always publish on session_start
The condition wasn't necessary in session_start for PEP nodes not to be
published multiple times.

This also fixes the fact that we weren't publishing on reconnect.

And also fixes one indentation level too many in the _initial_publish
method.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-15 00:46:35 +02:00
fbe5e36c3e Fix #10: ensure device list and bundle are published after startup
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-15 00:46:35 +02:00
3ac03796dc Merge branch 'heartbeat' into 'master'
Automatically send heartbeats

See merge request poezio/slixmpp-omemo!10
2021-07-15 00:43:30 +02:00
7e079f4260 make_heartbeat needs to be async
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:45:15 +02:00
95481e64b2 decrypt_message: don't always send heartbeat
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:45:15 +02:00
89eb4dfece decrypt_message: msg.send isn't a coroutine
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:45:15 +02:00
7f1d48c529 make_heartbeat: add parameter to make_message
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:45:15 +02:00
ad5822b360 Automatically send heartbeats
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:45:15 +02:00
62fa03959a Ensure bundles are republished on decrypt
This would likely do that we had a fixed set of prekeys because we
wouldn't republish after one was used.

This shouldn't have any security implications. Just so that we wouldn't
have been able to initiate new sessions after all published prekeys were
used up, as we don't have the private keys anymore. They are deleted
when used (as should be) by the backend OMEMO library.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-14 09:39:40 +02:00
af33cd41e5
Learn about chain length: new should_heartbeat method
With this commit, slixmpp-omemo now reads the ratchet chain length,
(both receiving and sending), that we should track to know when to send
a heartbeat message.

This allows us to signal other devices that we are still active and
listening. Some clients will stop encrypting to us if we haven't replied
for a certain number of messages.

The current 0384 spec (0.7) says we should send a heartbeat message at
least once this number goes over 53 (fair dice roll). It doesn't say
when a client may/should stop encrypting to us, or what it should do at
all once we go over 53.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-13 20:29:14 +02:00
38701075e9
echo_client: fix wrong variable error
Thanks mathieui

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-13 20:28:19 +02:00
59e82ec9ee
echo_bot: implement basic commands
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-12 23:25:57 +02:00
ba028d98c8
echo_bot: Don't pass the whole message to plain_reply and encrypted_reply anymore
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-12 23:12:09 +02:00
cea2345f29
echo_bot: change messages style
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-12 19:26:24 +02:00
d9b77dbf86
Release 0.5.0
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-12 11:22:23 +02:00
78db04a1a8 Add my_fingerprint method
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-07-12 11:14:19 +02:00
92a379a327 Merge branch 'change-fp-repr' into 'master'
fp_from_ik: change fp repr to lowercase with no colon

See merge request poezio/slixmpp-omemo!8
2021-07-12 11:09:42 +02:00
6c61d0f237 fp_from_ik: change fp repr to lowercase with no colon
This allows the user to be able to do what they want with the string.
They can then insert colons, or split up in groups of chars or..

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2021-06-29 02:02:43 +02:00
957b555d93
Raise an exception when the payload is not of the form we expect
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-03-28 12:28:52 +01:00
94e3a62d8a
Error out when data_dir is not specified. #11
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-03-12 01:30:30 +01:00
534492fe45
Simplify fp_from_ik method, remove codec import
Reuse changes from python-omemo, explicitly the following PR under CC0:
https://github.com/Syndace/python-omemo/pull/28

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-03-11 03:26:32 +01:00
9 changed files with 511 additions and 146 deletions

View file

@ -1,17 +1,21 @@
---
stages:
- lint
- lint
.python-3.7:
image: python:3.7
.python-3.9:
image: python:3.9
.python-3.8:
image: python:3.8
.python-3.10:
image: python:3.10
.pylint:
stage: lint
script:
- apt update && apt install -y libidn11-dev build-essential cmake
- pip3 install pylint pyasn1-modules cffi --upgrade
- pip3 install -e git+https://github.com/syndace/python-omemo#egg=omemo
- pip3 install -e git+https://github.com/syndace/python-omemo-backend-signal#egg=omemo-backend-signal
- pip3 install -e git+https://lab.louiz.org/poezio/slixmpp.git#egg=slixmpp
- python3 setup.py install
- pylint -E slixmpp_omemo
@ -24,22 +28,22 @@ stages:
- mypyc --ignore-missing-imports ./slixmpp_omemo
allow_failure: true
lint-3.7-pylint:
lint-3.9-pylint:
extends:
- .python-3.7
- .python-3.9
- .pylint
lint-3.8-pylint:
lint-3.9-mypy:
extends:
- .python-3.8
- .python-3.9
- .mypy
lint-3.10-pylint:
extends:
- .python-3.10
- .pylint
lint-3.7-mypy:
lint-3.10-mypy:
extends:
- .python-3.7
- .mypy
lint-3.8-mypy:
extends:
- .python-3.8
- .python-3.10
- .mypy

View file

@ -1,3 +1,51 @@
Version 0.9.0:
2022-10-19 Maxime “pep” Buquet <pep@bouah.net>
* Added:
- Coroutines in asyncio.wait is now deprecated. Added create_task calls
- Replaced all ensure_future calls by create_task
Version 0.8.0:
2022-08-23 Maxime “pep” Buquet <pep@bouah.net>
* Breaking:
- get_devices and get_active_devices now return Iterable[int] instead of Iterable[str]
* Changes:
- fetch_bundle and fetch_device methods are now public
- my_fingerprint doesn't traceback anymore on normal operation
* Added:
- New fetch_bundles method to fetch all bundles at once
- Add upper bound on OMEMO lib version requirements as it'll become significant
Version 0.7.0:
2022-04-03 Maxime “pep” Buquet <pep@bouah.net>
* Breaking:
- Removed get_device_list method in favor of newly added get_devices and
get_active_devices methods.
- Renamed make_heartbeat to send_heartbeat and make it send the message as
well.
* Improvements:
- Added py.typed to the repository for static type checking tools
- New delete_session method
Version 0.6.1:
2022-03-14 Maxime “pep” Buquet <pep@bouah.net>
* Improvements:
- Add minimal version requirements in requirements.txt and setup.py
Version 0.6.0:
2022-03-12 Maxime “pep” Buquet <pep@bouah.net>
* Improvements:
- Ensure device list is published even if we're already connected (#10)
- Ensure bundles are republished on decrypt
* Added:
- Heartbeat messages. Signal to other devices we're still active after
some amount of messages. Stop raising exceptions when there is no payload.
- Ensure heartbeats are stored in the archive.
- Commands to echo_bot. (verbose, error)
Version 0.5.0:
2021-07-12 Maxime “pep” Buquet <pep@bouah.net>
* Added:
- New my_fingerprint method
* Breaking:
- Raise exception when no data dir is specified instead of simply logging
- Removed colons from output format of fp_from_ik helper
- Raise exception when payload is not of the form we expect (missing
payload, key, or iv element)
Version 0.4.0:
2020-03-10 Maxime “pep” Buquet <pep@bouah.net>
* Improvements:

View file

@ -27,6 +27,19 @@ Installation
- PIP: `slixmpp-omemo`
- Manual: `python3 setup.py install`
Examples
--------
The repository contains an example bot that contains many comments, and
can be used to test against other setups. To use it:
```
python examples/echo_bot.py --debug -j foo@bar -p passwd --data-dir /foo/bar
```
It also contains commands. Feel free to open merge requests or issues to
add new useful ones.
Credits
-------

View file

@ -11,8 +11,8 @@
"""
import os
import re
import sys
import asyncio
import logging
from getpass import getpass
from argparse import ArgumentParser
@ -20,6 +20,8 @@ from argparse import ArgumentParser
from slixmpp import ClientXMPP, JID
from slixmpp.exceptions import IqTimeout, IqError
from slixmpp.stanza import Message
from slixmpp.xmlstream.handler import CoroutineCallback
from slixmpp.xmlstream.matcher import MatchXPath
import slixmpp_omemo
from slixmpp_omemo import PluginCouldNotLoad, MissingOwnKey, EncryptionPrepareException
from slixmpp_omemo import UndecidedException, UntrustedException, NoAvailableSession
@ -27,6 +29,10 @@ from omemo.exceptions import MissingBundleException
log = logging.getLogger(__name__)
# Used by the EchoBot
LEVEL_DEBUG = 0
LEVEL_ERROR = 1
class EchoBot(ClientXMPP):
@ -39,12 +45,20 @@ class EchoBot(ClientXMPP):
"""
eme_ns = 'eu.siacs.conversations.axolotl'
cmd_prefix = '!'
debug_level: int = LEVEL_DEBUG # or LEVEL_ERROR
def __init__(self, jid, password):
ClientXMPP.__init__(self, jid, password)
self.prefix_re: re.Pattern = re.compile('^%s' % self.cmd_prefix)
self.cmd_re: re.Pattern = re.compile('^%s(?P<command>\w+)(?:\s+(?P<args>.*))?' % self.cmd_prefix)
self.add_event_handler("session_start", self.start)
self.add_event_handler("message", self.message_handler)
self.register_handler(CoroutineCallback('Messages',
MatchXPath(f'{{{self.default_ns}}}message'),
self.message_handler,
))
def start(self, _event) -> None:
"""
@ -62,10 +76,47 @@ class EchoBot(ClientXMPP):
self.send_presence()
self.get_roster()
def message_handler(self, msg: Message) -> None:
asyncio.ensure_future(self.message(msg))
def is_command(self, body: str) -> bool:
return self.prefix_re.match(body) is not None
async def message(self, msg: Message, allow_untrusted: bool = False) -> None:
async def handle_command(self, mto: JID, mtype: str, body: str) -> None:
match = self.cmd_re.match(body)
if match is None:
return None
groups = match.groupdict()
cmd = groups['command']
# args = groups['args']
if cmd == 'help':
await self.cmd_help(mto, mtype)
elif cmd == 'verbose':
await self.cmd_verbose(mto, mtype)
elif cmd == 'error':
await self.cmd_error(mto, mtype)
return None
async def cmd_help(self, mto: JID, mtype: str) -> None:
body = (
'I\'m the slixmpp-omemo echo bot! '
'The following commands are available:\n'
f'{self.cmd_prefix}verbose Send message or reply with log messages\n'
f'{self.cmd_prefix}error Send message or reply only on error\n'
)
return await self.encrypted_reply(mto, mtype, body)
async def cmd_verbose(self, mto: JID, mtype: str) -> None:
self.debug_level = LEVEL_DEBUG
body = '''Debug level set to 'verbose'.'''
return await self.encrypted_reply(mto, mtype, body)
async def cmd_error(self, mto: JID, mtype: str) -> None:
self.debug_level = LEVEL_ERROR
body = '''Debug level set to 'error'.'''
return await self.encrypted_reply(mto, mtype, body)
async def message_handler(self, msg: Message, allow_untrusted: bool = False) -> None:
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
@ -77,28 +128,36 @@ class EchoBot(ClientXMPP):
for stanza objects and the Message stanza to see
how it may be used.
"""
mfrom = mto = msg['from']
mtype = msg['type']
if msg['type'] not in ('chat', 'normal'):
if mtype not in ('chat', 'normal'):
return None
if not self['xep_0384'].is_encrypted(msg):
await self.plain_reply(msg, 'This message was not encrypted.\n%(body)s' % msg)
if self.debug_level == LEVEL_DEBUG:
await self.plain_reply(mto, mtype, f"Echo unencrypted message: {msg['body']}")
return None
try:
mfrom = msg['from']
encrypted = msg['omemo_encrypted']
body = self['xep_0384'].decrypt_message(encrypted, mfrom, allow_untrusted)
await self.encrypted_reply(msg, 'Thanks for sending\n%s' % body.decode("utf8"))
return None
body = await self['xep_0384'].decrypt_message(encrypted, mfrom, allow_untrusted)
# decrypt_message returns Optional[str]. It is possible to get
# body-less OMEMO message (see KeyTransportMessages), currently
# used for example to send heartbeats to other devices.
if body is not None:
decoded = body.decode('utf8')
if self.is_command(decoded):
await self.handle_command(mto, mtype, decoded)
elif self.debug_level == LEVEL_DEBUG:
await self.encrypted_reply(mto, mtype, f'Echo: {decoded}')
except (MissingOwnKey,):
# The message is missing our own key, it was not encrypted for
# us, and we can't decrypt it.
await self.plain_reply(
msg,
'I can\'t decrypt this message as it is not encrypted for me.',
mto, mtype,
'Error: Message not encrypted for me.',
)
return None
except (NoAvailableSession,) as exn:
# We received a message from that contained a session that we
# don't know about (deleted session storage, etc.). We can't
@ -107,11 +166,10 @@ class EchoBot(ClientXMPP):
# best if we send an encrypted message directly. XXX: Is it
# where we talk about self-healing messages?
await self.encrypted_reply(
msg,
'I can\'t decrypt this message as it uses an encrypted '
mto, mtype,
'Error: Message uses an encrypted '
'session I don\'t know about.',
)
return None
except (UndecidedException, UntrustedException) as exn:
# We received a message from an untrusted device. We can
# choose to decrypt the message nonetheless, with the
@ -122,40 +180,34 @@ class EchoBot(ClientXMPP):
# trusted, or in undecided state, if they decide to decrypt it
# anyway.
await self.plain_reply(
msg,
"Your device '%s' is not in my trusted devices." % exn.device,
mto, mtype,
f"Error: Your device '{exn.device}' is not in my trusted devices.",
)
# We resend, setting the `allow_untrusted` parameter to True.
await self.message(msg, allow_untrusted=True)
return None
await self.message_handler(msg, allow_untrusted=True)
except (EncryptionPrepareException,):
# Slixmpp tried its best, but there were errors it couldn't
# resolve. At this point you should have seen other exceptions
# and given a chance to resolve them already.
await self.plain_reply(msg, 'I was not able to decrypt the message.')
return None
await self.plain_reply(mto, mtype, 'Error: I was not able to decrypt the message.')
except (Exception,) as exn:
await self.plain_reply(msg, 'An error occured while attempting decryption.\n%r' % exn)
await self.plain_reply(mto, mtype, 'Error: Exception occured while attempting decryption.\n%r' % exn)
raise
return None
async def plain_reply(self, original_msg, body):
async def plain_reply(self, mto: JID, mtype: str, body):
"""
Helper to reply to messages
"""
mto = original_msg['from']
mtype = original_msg['type']
msg = self.make_message(mto=mto, mtype=mtype)
msg['body'] = body
return msg.send()
async def encrypted_reply(self, original_msg, body):
async def encrypted_reply(self, mto: JID, mtype: str, body):
"""Helper to reply with encrypted messages"""
mto = original_msg['from']
mtype = original_msg['type']
msg = self.make_message(mto=mto, mtype=mtype)
msg['eme']['namespace'] = self.eme_ns
msg['eme']['name'] = self['xep_0380'].mechanisms[self.eme_ns]
@ -183,7 +235,7 @@ class EchoBot(ClientXMPP):
# untrusted/undecided barejid, so we need to make a decision here.
# This is where you prompt your user to ask what to do. In
# this bot we will automatically trust undecided recipients.
self['xep_0384'].trust(exn.bare_jid, exn.device, exn.ik)
await self['xep_0384'].trust(exn.bare_jid, exn.device, exn.ik)
# TODO: catch NoEligibleDevicesException
except EncryptionPrepareException as exn:
# This exception is being raised when the library has tried
@ -201,22 +253,22 @@ class EchoBot(ClientXMPP):
# generic message. The receiving end-user at this
# point can bring up the issue if it happens.
self.plain_reply(
original_msg,
'Could not find keys for device "%d" of recipient "%s". Skipping.' %
(error.device, error.bare_jid),
mto, mtype,
f'Could not find keys for device "{error.device}"'
f' of recipient "{error.bare_jid}". Skipping.',
)
jid = JID(error.bare_jid)
device_list = expect_problems.setdefault(jid, [])
device_list.append(error.device)
except (IqError, IqTimeout) as exn:
self.plain_reply(
original_msg,
mto, mtype,
'An error occured while fetching information on a recipient.\n%r' % exn,
)
return None
except Exception as exn:
await self.plain_reply(
original_msg,
mto, mtype,
'An error occured while attempting to encrypt.\n%r' % exn,
)
raise

View file

@ -1,3 +1,3 @@
slixmpp
omemo
omemo-backend-signal
slixmpp>=1.8.0
omemo-backend-signal>=0.3.0
omemo>=0.14.0,<0.15

View file

@ -40,8 +40,9 @@ CLASSIFIERS = [
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Internet :: XMPP',
'Topic :: Security :: Cryptography',
'Topic :: Software Development :: Libraries :: Python Modules',
@ -58,7 +59,12 @@ setup(
url='https://lab.louiz.org/poezio/slixmpp-omemo',
license='GPLv3',
platforms=['any'],
package_data={'slixmpp_omemo': ['py.typed']},
packages=['slixmpp_omemo'],
install_requires=['slixmpp', 'omemo', 'omemo-backend-signal'],
install_requires=[
'slixmpp>=1.8.0',
'omemo-backend-signal>=0.3.0',
'omemo>=0.14.0,<0.15',
],
classifiers=CLASSIFIERS,
)

View file

@ -11,13 +11,18 @@
import logging
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
import os
import json
import base64
import codecs
import asyncio
from functools import reduce
# Not available in Python 3.7, and slixmpp already imports the right things
# for me
from slixmpp.types import TypedDict
from slixmpp.plugins.xep_0060.stanza import Items, EventItems
from slixmpp.plugins.xep_0004 import Form
from slixmpp.plugins.base import BasePlugin, register_plugin
@ -83,7 +88,7 @@ def _load_device_id(data_dir: str) -> int:
def fp_from_ik(identity_key: bytes) -> str:
"""Convert identityKey to a string representation (fingerprint)"""
return codecs.getencoder("hex")(identity_key)[0].decode("US-ASCII").upper()
return "".join("{:02x}".format(octet) for octet in identity_key)
def _parse_bundle(backend: Backend, bundle: Bundle) -> ExtendedPublicBundle:
@ -109,9 +114,10 @@ def _generate_encrypted_payload(encrypted) -> Encrypted:
tag['header']['sid'] = str(encrypted['sid'])
tag['header']['iv']['value'] = b64enc(encrypted['iv'])
tag['payload']['value'] = b64enc(encrypted['payload'])
if 'payload' in encrypted:
tag['payload']['value'] = b64enc(encrypted['payload'])
for bare_jid, devices in encrypted['keys'].items():
for devices in encrypted['keys'].values():
for rid, device in devices.items():
key = Key()
key['value'] = b64enc(device['data'])
@ -152,6 +158,9 @@ class MissingOwnKey(XEP0384): pass
class NoAvailableSession(XEP0384): pass
class UninitializedOMEMOSession(XEP0384): pass
class EncryptionPrepareException(XEP0384):
def __init__(self, errors):
self.errors = errors
@ -171,6 +180,15 @@ class UndecidedException(XEP0384):
self.ik = ik
class ErroneousPayload(XEP0384):
"""To be raised when the payload is not of the form we expect"""
class ErroneousParameter(XEP0384):
"""To be raised when parameters to the `encrypt_message` method aren't
used as expected."""
class XEP_0384(BasePlugin):
"""
@ -179,12 +197,15 @@ class XEP_0384(BasePlugin):
name = 'xep_0384'
description = 'XEP-0384 OMEMO'
dependencies = {'xep_0004', 'xep_0060', 'xep_0163'}
dependencies = {'xep_0004', 'xep_0030', 'xep_0060', 'xep_0163', 'xep_0334'}
default_config = {
'data_dir': None,
'storage_backend': None,
'otpk_policy': DefaultOTPKPolicy,
'omemo_backend': SignalBackend,
'auto_heartbeat': True,
'heartbeat_after': 53,
# TODO: 'drop_inactive_after': 300,
}
backend_loaded = HAS_OMEMO and HAS_OMEMO_BACKEND
@ -192,6 +213,12 @@ class XEP_0384(BasePlugin):
# OMEMO Bundles used for encryption
bundles = {} # type: Dict[str, Dict[int, ExtendedPublicBundle]]
# Used at startup to prevent publishing device list and bundles multiple times
_initial_publish_done = False
# Initiated once the OMEMO session is created.
__omemo_session: Optional[SessionManager] = None
def plugin_init(self) -> None:
if not self.backend_loaded:
log_str = ("xep_0384 cannot be loaded as the backend omemo library "
@ -205,34 +232,20 @@ class XEP_0384(BasePlugin):
raise PluginCouldNotLoad
if not self.data_dir:
log.info("xep_0384 cannot be loaded as there is not data directory "
"specified")
return None
raise PluginCouldNotLoad("xep_0384 cannot be loaded as there is "
"no data directory specified.")
storage = self.storage_backend
if self.storage_backend is None:
storage = JSONFileStorage(self.data_dir)
otpkpolicy = self.otpk_policy
bare_jid = self.xmpp.boundjid.bare
self._device_id = _load_device_id(self.data_dir)
try:
self._omemo = SessionManager.create(
storage,
otpkpolicy,
self.omemo_backend,
bare_jid,
self._device_id,
)
except:
log.error("Couldn't load the OMEMO object; ¯\\_(ツ)_/¯")
raise PluginCouldNotLoad
asyncio.create_task(self.session_start_omemo())
self.xmpp.add_event_handler('session_start', self.session_start)
self.xmpp['xep_0060'].map_node_event(OMEMO_DEVICES_NS, 'omemo_device_list')
self.xmpp.add_event_handler('omemo_device_list_publish', self._receive_device_list)
return None
# If this plugin is loaded after 'session_start' has fired, we still
# need to publish bundles
if self.xmpp.is_connected and not self._initial_publish_done:
asyncio.create_task(self._initial_publish())
def plugin_end(self):
if not self.backend_loaded:
@ -242,17 +255,53 @@ class XEP_0384(BasePlugin):
self.xmpp.remove_event_handler('omemo_device_list_publish', self._receive_device_list)
self.xmpp['xep_0163'].remove_interest(OMEMO_DEVICES_NS)
async def session_start_omemo(self):
"""Creates the OMEMO session object"""
storage = self.storage_backend
if self.storage_backend is None:
storage = JSONFileStorage(self.data_dir)
otpkpolicy = self.otpk_policy
bare_jid = self.xmpp.boundjid.bare
try:
self.__omemo_session = await SessionManager.create(
storage,
otpkpolicy,
self.omemo_backend,
bare_jid,
self._device_id,
)
except Exception as exn:
log.error("Couldn't load the OMEMO object; ¯\\_(ツ)_/¯")
raise PluginCouldNotLoad from exn
def _omemo(self) -> SessionManager:
"""Helper method to unguard potentially uninitialized SessionManager"""
if self.__omemo_session is None:
raise UninitializedOMEMOSession
return self.__omemo_session
async def session_start(self, _jid):
await self._initial_publish()
async def _initial_publish(self):
if self.backend_loaded:
self.xmpp['xep_0163'].add_interest(OMEMO_DEVICES_NS)
await asyncio.wait([
self._set_device_list(),
self._publish_bundle(),
asyncio.create_task(self._set_device_list()),
asyncio.create_task(self._publish_bundle()),
])
self._initial_publish_done = True
def my_device_id(self) -> int:
return self._device_id
async def my_fingerprint(self) -> str:
bundle = self._omemo().public_bundle.serialize(self.omemo_backend)
return fp_from_ik(bundle['ik'])
def _set_node_config(
self,
node: str,
@ -300,10 +349,10 @@ class XEP_0384(BasePlugin):
)
async def _generate_bundle_iq(self, publish_options: bool = True) -> Iq:
bundle = self._omemo.public_bundle.serialize(self.omemo_backend)
bundle = self._omemo().public_bundle.serialize(self.omemo_backend)
jid = self.xmpp.boundjid
disco = await self.xmpp['xep_0030'].get_info(jid.bare)
disco = await self.xmpp['xep_0030'].get_info(jid=jid.bare, local=False)
publish_options = PUBLISH_OPTIONS_NODE in disco['disco_info'].get_features()
iq = self.xmpp.Iq(stype='set')
@ -338,15 +387,17 @@ class XEP_0384(BasePlugin):
return iq
async def _publish_bundle(self) -> None:
if self._omemo.republish_bundle:
log.debug('Publishing our own bundle. Do we need to?')
if self._omemo().republish_bundle:
log.debug('Publishing.')
iq = await self._generate_bundle_iq()
try:
await iq.send()
except IqError as e:
except IqError as exn:
# TODO: Slixmpp should handle pubsub#errors so we don't have to
# fish the element ourselves
precondition = e.iq['error'].xml.find(
'{%s}%s' % (PUBSUB_ERRORS, 'precondition-not-met'),
precondition = exn.iq['error'].xml.find(
f'{{{PUBSUB_ERRORS}}}precondition-not-met'
)
if precondition is not None:
log.debug('The node we tried to publish was already '
@ -361,40 +412,71 @@ class XEP_0384(BasePlugin):
raise
iq = await self._generate_bundle_iq(publish_options=False)
await iq.send()
else:
log.debug('Not publishing.')
async def _fetch_bundle(self, jid: str, device_id: int) -> Optional[ExtendedPublicBundle]:
node = '%s:%d' % (OMEMO_BUNDLES_NS, device_id)
async def fetch_bundle(self, jid: JID, device_id: int) -> None:
"""
Fetch bundle for specified jid / device_id pair.
"""
log.debug('Fetching bundle for JID: %r, device: %r', jid, device_id)
node = f'{OMEMO_BUNDLES_NS}:{device_id}'
try:
iq = await self.xmpp['xep_0060'].get_items(jid, node)
except (IqError, IqTimeout):
return None
bundle = iq['pubsub']['items']['item']['bundle']
return _parse_bundle(self.omemo_backend, bundle)
bundle = _parse_bundle(self.omemo_backend, bundle)
if bundle is not None:
log.debug('Encryption: Bundle %r found!', device_id)
devices = self.bundles.setdefault(jid.bare, {})
devices[device_id] = bundle
else:
log.debug('Encryption: Bundle %r not found!', device_id)
async def _fetch_device_list(self, jid: JID) -> None:
"""Manually query PEP OMEMO_DEVICES_NS nodes"""
async def fetch_bundles(self, jid: JID) -> None:
"""
Fetch bundles of active devices for specified JID.
This is a helper function to allow the user to request a store
update. Failed bundles are not retried.
"""
# Ignore failures
await asyncio.gather(
*map(
lambda did: self.fetch_bundle(jid, did),
await self.get_active_devices(jid)
),
return_exceptions=True,
)
async def fetch_devices(self, jid: JID) -> None:
"""
Manually query PEP OMEMO_DEVICES_NS nodes
"""
log.debug('Fetching device list for JID: %r', jid)
iq = await self.xmpp['xep_0060'].get_items(jid.full, OMEMO_DEVICES_NS)
return await self._read_device_list(jid, iq['pubsub']['items'])
def _store_device_ids(self, jid: str, items: Union[Items, EventItems]) -> None:
async def _store_device_ids(self, jid: str, items: Union[Items, EventItems]) -> None:
"""Store Device list"""
device_ids = [] # type: List[int]
items = list(items)
if items:
device_ids = [int(d['id']) for d in items[0]['devices']]
return self._omemo.newDeviceList(str(jid), device_ids)
return await self._omemo().newDeviceList(str(jid), device_ids)
def _receive_device_list(self, msg: Message) -> None:
"""Handler for received PEP OMEMO_DEVICES_NS payloads"""
asyncio.ensure_future(
asyncio.create_task(
self._read_device_list(msg['from'], msg['pubsub_event']['items']),
)
async def _read_device_list(self, jid: JID, items: Union[Items, EventItems]) -> None:
"""Read items and devices if we need to set the device list again or not"""
bare_jid = jid.bare
self._store_device_ids(bare_jid, items)
await self._store_device_ids(bare_jid, items)
items = list(items)
device_ids = []
@ -407,7 +489,7 @@ class XEP_0384(BasePlugin):
return None
async def _set_device_list(self, device_ids: Optional[Set[int]] = None) -> None:
async def _set_device_list(self, device_ids: Optional[Iterable[int]] = None) -> None:
own_jid = self.xmpp.boundjid
try:
@ -415,15 +497,15 @@ class XEP_0384(BasePlugin):
own_jid.bare, OMEMO_DEVICES_NS,
)
items = iq['pubsub']['items']
self._store_device_ids(own_jid.bare, items)
await self._store_device_ids(own_jid.bare, items)
except IqError as iq_err:
if iq_err.condition == "item-not-found":
self._store_device_ids(own_jid.bare, [])
await self._store_device_ids(own_jid.bare, [])
else:
return # XXX: Handle this!
if device_ids is None:
device_ids = self.get_device_list(own_jid)
device_ids = await self.get_active_devices(own_jid)
devices = []
for i in device_ids:
@ -434,7 +516,7 @@ class XEP_0384(BasePlugin):
payload['devices'] = devices
jid = self.xmpp.boundjid
disco = await self.xmpp['xep_0030'].get_info(jid.bare)
disco = await self.xmpp['xep_0030'].get_info(jid=jid.bare, local=False)
publish_options = PUBLISH_OPTIONS_NODE in disco['disco_info'].get_features()
options = None
@ -449,6 +531,7 @@ class XEP_0384(BasePlugin):
})
try:
log.debug('Setting own device list to %r', device_ids)
await self.xmpp['xep_0060'].publish(
own_jid.bare, OMEMO_DEVICES_NS, payload=payload, options=options,
)
@ -471,17 +554,113 @@ class XEP_0384(BasePlugin):
own_jid.bare, OMEMO_DEVICES_NS, payload=payload,
)
def get_device_list(self, jid: JID) -> List[str]:
"""Return active device ids. Always contains our own device id."""
return self._omemo.getDevices(jid.bare).get('active', [])
async def get_devices(self, jid: JID) -> Iterable[int]:
"""
Get all devices for a JID.
"""
devices = await self._omemo().getDevices(jid.bare)
return map(int, set(devices.get('active', []) + devices.get('inactive', [])))
def trust(self, jid: JID, device_id: int, ik: bytes) -> None:
self._omemo.setTrust(jid.bare, device_id, ik, True)
async def get_active_devices(self, jid: JID) -> Iterable[int]:
"""
Return active device ids. Always contains our own device id.
"""
devices = await self._omemo().getDevices(jid.bare)
return map(int, set(devices.get('active', [])))
def distrust(self, jid: JID, device_id: int, ik: bytes) -> None:
self._omemo.setTrust(jid.bare, device_id, ik, False)
async def _should_heartbeat(self, jid: JID, device_id: int, prekey: bool) -> bool:
"""
Internal helper for :py:func:`XEP_0384.should_heartbeat`.
def get_trust_for_jid(self, jid: JID) -> Dict[str, List[Optional[Dict[str, Any]]]]:
Returns whether we should send a heartbeat message for (JID,
device_id).
We check if the message is a prekey message, in which case we
assume it's a new session and we want to ACK relatively early.
Otherwise we look at the number of messages since we have last
replied and if above a certain threshold we notify them that we're
still active.
"""
length = await self._omemo().receiving_chain_length(jid.bare, device_id)
inactive_session = (length or 0) > self.heartbeat_after
log.debug(
'Chain length for %r / %d: %d -> inactive_session? %r',
jid, device_id, length, inactive_session,
)
log.debug('Is this a prekey message: %r', prekey)
res = prekey or inactive_session
log.debug('Should heartbeat? %r', res)
return res
async def should_heartbeat(self, jid: JID, msg: Union[Message, Encrypted]) -> bool:
"""
Returns whether we should send a heartbeat message to the sender
device. See notes about heartbeat in
https://xmpp.org/extensions/xep-0384.html#rules.
This method will return True if this session (to the sender
device) is not yet confirmed, or if it hasn't been answered in a
while.
"""
prekey: bool = False
# Get prekey information from message
encrypted = msg
if isinstance(msg, Message):
encrypted = msg['omemo_encrypted']
header = encrypted['header']
sid = header['sid']
key = header.xml.find("{%s}key[@rid='%s']" % (
OMEMO_BASE_NS, self._device_id))
# Don't error out. If it's not encrypted to us we don't need to send a
# heartbeat.
prekey = False
if key is not None:
key = Key(key)
prekey = key['prekey'] in TRUE_VALUES
return await self._should_heartbeat(jid, sid, prekey)
async def send_heartbeat(self, jid: JID, device_id: int) -> None:
"""
Returns a heartbeat message.
This is mainly used to tell receiving clients that our device is
still active. This is an empty key transport message of which we
won't use the generated shared secret.
"""
msg = self.xmpp.make_message(mto=jid)
encrypted = await self.encrypt_message(
plaintext=None,
recipients=[jid],
expect_problems=None,
_ignore_trust=True,
_device_id=device_id,
)
msg.append(encrypted)
msg.enable('store')
msg.send()
async def delete_session(self, jid: JID, device_id: int) -> None:
"""
Delete the session for the provided jid/device_id pair.
"""
await self._omemo().deleteSession(jid.bare, device_id)
async def trust(self, jid: JID, device_id: int, ik: bytes) -> None:
await self._omemo().setTrust(jid.bare, device_id, ik, True)
async def distrust(self, jid: JID, device_id: int, ik: bytes) -> None:
await self._omemo().setTrust(jid.bare, device_id, ik, False)
async def get_trust_for_jid(self, jid: JID) -> Dict[str, List[Optional[Dict[str, Any]]]]:
"""
Fetches trust for JID. The returned dictionary will contain active
and inactive devices. Each of these dict will contain device ids
@ -502,19 +681,23 @@ class XEP_0384(BasePlugin):
}
"""
return self._omemo.getTrustForJID(jid.bare)
return await self._omemo().getTrustForJID(jid.bare)
def is_encrypted(self, msg: Message) -> bool:
@staticmethod
def is_encrypted(msg: Message) -> bool:
return msg.xml.find('{%s}encrypted' % OMEMO_BASE_NS) is not None
def decrypt_message(
async def decrypt_message(
self,
encrypted: Encrypted,
sender: JID,
allow_untrusted: bool = False,
) -> Optional[str]:
header = encrypted['header']
payload = b64dec(encrypted['payload']['value'])
payload = None
if encrypted['payload']['value'] is not None:
payload = b64dec(encrypted['payload']['value'])
jid = sender.bare
sid = int(header['sid'])
@ -526,22 +709,37 @@ class XEP_0384(BasePlugin):
key = Key(key)
isPrekeyMessage = key['prekey'] in TRUE_VALUES
if key['value'] is None:
raise ErroneousPayload('The key element was empty')
message = b64dec(key['value'])
if header['iv']['value'] is None:
raise ErroneousPayload('The iv element was empty')
iv = b64dec(header['iv']['value'])
# XXX: 'cipher' is part of KeyTransportMessages and is used when no payload
# is passed. We do not implement this yet.
try:
body = self._omemo.decryptMessage(
jid,
sid,
iv,
message,
isPrekeyMessage,
payload,
allow_untrusted=allow_untrusted,
)
return body
log.debug('Decryption: Attempt to decrypt message from JID: %r', sender)
if payload is None:
await self._omemo().decryptRatchetForwardingMessage(
jid,
sid,
iv,
message,
isPrekeyMessage,
allow_untrusted=allow_untrusted,
)
body = None
else:
body = await self._omemo().decryptMessage(
jid,
sid,
iv,
message,
isPrekeyMessage,
payload,
allow_untrusted=allow_untrusted,
)
except (omemo.exceptions.NoSessionException,):
# This might happen when the sender is sending using a session
# that we don't know about (deleted session storage, etc.). In
@ -550,18 +748,31 @@ class XEP_0384(BasePlugin):
raise NoAvailableSession(jid, sid)
except (omemo.exceptions.TrustException,) as exn:
if exn.problem == 'undecided':
log.debug('Decryption: trust state for JID: %r, device: %r, is undecided', exn.bare_jid, exn.device)
raise UndecidedException(exn.bare_jid, exn.device, exn.ik)
if exn.problem == 'untrusted':
log.debug('Decryption: trust state for JID: %r, device: %r, set to untrusted', exn.bare_jid, exn.device)
raise UntrustedException(exn.bare_jid, exn.device, exn.ik)
raise
finally:
asyncio.ensure_future(self._publish_bundle())
asyncio.create_task(self._publish_bundle())
if self.auto_heartbeat:
log.debug('Checking if heartbeat is required. auto_hearbeat enabled.')
should_heartbeat = await self._should_heartbeat(sender, sid, isPrekeyMessage)
if should_heartbeat:
log.debug('Decryption: Sending hearbeat to %s / %d', jid, sid)
await self.send_heartbeat(JID(jid), sid)
return body
async def encrypt_message(
self,
plaintext: str,
plaintext: Optional[str],
recipients: List[JID],
expect_problems: Optional[Dict[JID, List[int]]] = None,
_ignore_trust: bool = False,
_device_id: Optional[int] = None,
) -> Encrypted:
"""
Returns an encrypted payload to be placed into a message.
@ -569,9 +780,22 @@ class XEP_0384(BasePlugin):
The API for getting an encrypted payload consists of trying first
and fixing errors progressively. The actual sending happens once the
application (us) thinks we're good to go.
If `plaintext` is specified, this will generate a full OMEMO payload. If
not, if `_ignore_trust` is True, this will generate a ratchet forwarding
message, and otherwise it will generate a key transport message.
These are rather technical details to the user and fiddling with
parameters else than `plaintext` and `recipients` should be rarely
needed.
The `_device_id` parameter is required in the case of a ratchet
forwarding message. That is, `plaintext` to None, and `_ignore_trust`
to True. If specified, a single recipient JID is required. If not all
these conditions are met, ErroneousParameter will be raised.
"""
recipients = [jid.bare for jid in recipients]
barejids: List[str] = [jid.bare for jid in recipients]
old_errors = None # type: Optional[List[Tuple[Exception, Any, Any]]]
while True:
@ -585,29 +809,46 @@ class XEP_0384(BasePlugin):
expect_problems = {jid.bare: did for (jid, did) in expect_problems.items()}
try:
encrypted = self._omemo.encryptMessage(
recipients,
plaintext.encode('utf-8'),
self.bundles,
expect_problems=expect_problems,
)
log.debug('Encryption: attempt to encrypt for JIDs: %r', barejids)
if plaintext is not None:
encrypted = await self._omemo().encryptMessage(
barejids,
plaintext.encode('utf-8'),
bundles=self.bundles,
expect_problems=expect_problems,
)
elif _ignore_trust:
if not _device_id or len(barejids) != 1:
raise ErroneousParameter
bundle = self.bundles.get(barejids[0], {}).get(_device_id, None)
encrypted = await self._omemo().encryptRatchetForwardingMessage(
bare_jid=barejids[0],
device_id=_device_id,
bundle=bundle,
)
else:
encrypted = await self._omemo().encryptKeyTransportMessage(
barejids,
bundles=self.bundles,
expect_problems=expect_problems,
)
return _generate_encrypted_payload(encrypted)
except omemo.exceptions.EncryptionProblemsException as exception:
errors = exception.problems
if errors == old_errors:
log.debug('Encryption: Still not possible after another iteration.')
raise EncryptionPrepareException(errors)
old_errors = errors
for exn in errors:
if isinstance(exn, omemo.exceptions.NoDevicesException):
await self._fetch_device_list(JID(exn.bare_jid))
log.debug('Encryption: Missing device list for JID: %r', exn.bare_jid)
await self.fetch_devices(JID(exn.bare_jid))
elif isinstance(exn, omemo.exceptions.MissingBundleException):
bundle = await self._fetch_bundle(exn.bare_jid, exn.device)
if bundle is not None:
devices = self.bundles.setdefault(exn.bare_jid, {})
devices[exn.device] = bundle
log.debug('Encryption: Missing bundle for JID: %r, device: %r', exn.bare_jid, exn.device)
await self.fetch_bundle(JID(exn.bare_jid), exn.device)
elif isinstance(exn, omemo.exceptions.TrustException):
# On TrustException, there are two possibilities.
# Either trust has not been explicitely set yet, and is
@ -616,6 +857,7 @@ class XEP_0384(BasePlugin):
# a choice. If untrusted, then we can safely tell the
# OMEMO lib to not encrypt to this device
if exn.problem == 'undecided':
log.debug('Encryption: Trust state not set for JID: %r, device: %r', exn.bare_jid, exn.device)
raise UndecidedException(exn.bare_jid, exn.device, exn.ik)
distrusted_jid = JID(exn.bare_jid)
expect_problems.setdefault(distrusted_jid, []).append(exn.device)

0
slixmpp_omemo/py.typed Normal file
View file

View file

@ -9,5 +9,5 @@
See the file LICENSE for copying permission.
"""
__version__ = "0.4.0"
__version_info__ = (0, 4, 0)
__version__ = "0.9.0"
__version_info__ = (0, 9, 0)