├── .github
└── FUNDING.yml
├── .gitignore
├── .gitlint
├── .pubignore
├── .woodpecker.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── example
└── omemo_dart_example.dart
├── flake.lock
├── flake.nix
├── lib
├── omemo_dart.dart
└── src
│ ├── common
│ └── constants.dart
│ ├── crypto.dart
│ ├── double_ratchet
│ ├── double_ratchet.dart
│ └── kdf.dart
│ ├── errors.dart
│ ├── helpers.dart
│ ├── keys.dart
│ ├── omemo
│ ├── bundle.dart
│ ├── decryption_result.dart
│ ├── device.dart
│ ├── encrypted_key.dart
│ ├── encryption_result.dart
│ ├── errors.dart
│ ├── fingerprint.dart
│ ├── omemo.dart
│ ├── queue.dart
│ ├── ratchet_data.dart
│ ├── ratchet_map_key.dart
│ └── stanza.dart
│ ├── protobuf
│ ├── .gitkeep
│ ├── schema.pb.dart
│ ├── schema.pbenum.dart
│ ├── schema.pbjson.dart
│ └── schema.pbserver.dart
│ ├── trust
│ ├── always.dart
│ ├── base.dart
│ ├── btbv.dart
│ └── never.dart
│ └── x3dh
│ └── x3dh.dart
├── omemo_dart.doap
├── protobuf
└── schema.proto
├── pubspec.yaml
└── test
├── double_ratchet_test.dart
├── omemo_test.dart
├── queue_test.dart
├── trust_test.dart
└── x3dh_test.dart
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: papatutuwawa
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub.
2 | .dart_tool/
3 | .packages
4 |
5 | # Conventional directory for build outputs.
6 | build/
7 |
8 | # Omit committing pubspec.lock for library packages; see
9 | # https://dart.dev/guides/libraries/private-files#pubspeclock.
10 | pubspec.lock
11 |
12 | # NixOS
13 | .direnv
14 | .envrc
15 |
--------------------------------------------------------------------------------
/.gitlint:
--------------------------------------------------------------------------------
1 | [general]
2 | ignore=B5,B6,B7,B8
3 | [title-max-length]
4 | line-length=72
5 | [title-trailing-punctuation]
6 | [title-hard-tab]
7 | [title-match-regex]
8 | regex=^(feat|fix|test|release|chore|security|docs|refactor|style|ci):.*$
9 | [body-trailing-whitespace]
10 | [body-first-line-empty]
11 |
--------------------------------------------------------------------------------
/.pubignore:
--------------------------------------------------------------------------------
1 | lib/protobuf
2 |
--------------------------------------------------------------------------------
/.woodpecker.yml:
--------------------------------------------------------------------------------
1 | pipeline:
2 | analysis:
3 | image: dart:3.0.7
4 | commands:
5 | # Proxy requests to pub.dev using pubcached
6 | - PUB_HOSTED_URL=http://172.17.0.1:8000 dart pub get
7 | - dart analyze --fatal-infos --fatal-warnings
8 | - dart test
9 | when:
10 | path:
11 | includes: ['lib/**', 'test/**']
12 | notify:
13 | image: git.polynom.me/papatutuwawa/woodpecker-xmpp
14 | settings:
15 | xmpp_is_muc: 1
16 | xmpp_tls: 1
17 | xmpp_recipient: moxxy-build@muc.moxxy.org
18 | xmpp_alias: 2Bot
19 | secrets: [ xmpp_jid, xmpp_password, xmpp_server ]
20 | when:
21 | status:
22 | - failure
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.1.0
2 |
3 | - Initial version
4 | - Implement the Double Ratchet, X3DH and OMEMO specific bits
5 | - Add a Blind-Trust-Before-Verification TrustManager
6 | - Supported OMEMO version: 0.8.3
7 |
8 | ## 0.1.3
9 |
10 | - Fix bug with the Double Ratchet causing only the initial message to be decryptable
11 | - Expose `getDeviceMap` as a developer usable function
12 | ## 0.2.0
13 |
14 | - Add convenience functions `getDeviceId` and `getDeviceBundle`
15 | - Creating a new ratchet with an id for which we already have a ratchet will now overwrite the old ratchet
16 | - Ratchet now carry an "acknowledged" attribute
17 |
18 | ## 0.2.1
19 |
20 | - Add `isRatchetAcknowledged`
21 | - Ratchets that are created due to accepting a kex are now unacknowledged
22 |
23 | ## 0.3.0
24 |
25 | - Implement enabling and disabling ratchets via the TrustManager interface
26 | - Fix deserialization of the various objects
27 | - Remove the BTBV TrustManager's loadState method. Just use the constructor
28 | - Allow removing all ratchets for a given Jid
29 | - If an error occurs while decrypting the message, the ratchet will now be reset to its prior state
30 | - Fix a bug within the Varint encoding function. This should fix some occasional UnknownSignedPrekeyExceptions
31 | - Remove OmemoSessionManager's toJson and fromJson. Use toJsonWithoutSessions and fromJsonWithoutSessions. Restoring sessions is not out-of-scope for that function
32 |
33 | ## 0.3.1
34 |
35 | - Fix a bug that caused the device's id to change when replacing a OPK
36 | - Every decryption failure now causes the ratchet to be restored to a pre-decryption state
37 | - Add method to get the device's fingerprint
38 |
39 | ## 0.4.0
40 |
41 | - Deprecate `OmemoSessionManager`. Use `OmemoManager` instead.
42 | - Implement queued access to the ratchets inside the `OmemoManager`.
43 | - Implement heartbeat messages.
44 | - [BREAKING] Rename `Device` to `OmemoDevice`.
45 |
46 | ## 0.4.1
47 |
48 | - Fix fetching the current device and building a ratchet session with it when encrypting for our own JID
49 |
50 | ## 0.4.2
51 |
52 | - Fix removeAllRatchets not removing, well, all ratchets. In fact, it did not remove any ratchet.
53 |
54 | ## 0.4.3
55 |
56 | - Fix bug that causes ratchets to be unable to decrypt anything after receiving a heartbeat with a completely new session
57 |
58 | ## 0.5.0
59 |
60 | This version is a complete rework of omemo_dart!
61 |
62 | - Removed events from `OmemoManager`
63 | - Removed `OmemoSessionManager`
64 | - Removed serialization/deserialization code
65 | - Replace exceptions with errors inside a result type
66 | - Ratchets and trust data is now loaded and cached on demand
67 | - Accessing the trust manager must happen via `withTrustManager`
68 | - Overriding the base implementations is replaced by providing callback functions
69 |
70 | ## 0.5.1
71 |
72 | - Remove `added` and `replaced` from the data passed to the `CommitRatchetsCallback`
73 | - Added a list of newly added and replaced ratchets to the encryption and decryption results. This is useful for displaying messages like "Contact added a new device"
74 |
75 | ## 0.6.0
76 |
77 | - Bump dependencies to fix running with never version of Dart
78 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Alexander "PapaTutuWawa"
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # omemo_dart
2 |
3 | [](https://ci.polynom.me/repos/16)
4 |
5 | `omemo_dart` is a Dart library to help developers of Dart/Flutter XMPP clients to implement
6 | [OMEMO](https://xmpp.org/extensions/xep-0384.html) in its newest version - currently 0.8.3.
7 |
8 | The library provides an implementation of the [X3DH](https://signal.org/docs/specifications/x3dh/)
9 | key exchange, the [Double Ratchet](https://signal.org/docs/specifications/doubleratchet/) with
10 | the OMEMO 0.8.3 specific `ENCRYPT`, `DECRYPT` and `KDF_*` functions and a very high-level
11 | `OmemoSessionManager` that manages all Double Ratchet sessions and provides a clean and simple
12 | interface for encrypting a message for all known Ratchet sessions we have with a user.
13 |
14 | This library also has no dependency on any XMPP library. `omemo_dart` instead defines an
15 | intermediary format for the required data that you, the user, will need to transform to and from
16 | the stanza format of your preferred XMPP library yourself.
17 |
18 | ## Important Notes
19 |
20 | - **Please note that this library has not been audited for its security! Use at your own risk!**
21 | - This library is not tested with other implementations of OMEMO 0.8.3 as I do not know of any client implementing spec compliant OMEMO 0.8.3. It does, however, work with itself.
22 |
23 | ## Usage
24 |
25 | Include `omemo_dart` in your `pubspec.yaml` like this:
26 |
27 | ```yaml
28 | # [...]
29 |
30 | dependencies:
31 | omemo_dart:
32 | hosted: https://git.polynom.me/api/packages/PapaTutuWawa/pub
33 | version: ^0.5.0
34 | # [...]
35 |
36 | # [...]
37 | ```
38 |
39 | ### Example
40 |
41 | This repository includes a documented ["example"](./example/omemo_dart_example.dart) that explains the basic usage of the library while
42 | leaving out the XMPP specific bits. For a more functional and integrated example, see the `omemo_client.dart` example from
43 | [moxxmpp](https://codeberg.org/moxxy/moxxmpp).
44 |
45 | ### Persistence
46 |
47 | By default, `omemo_dart` uses in-memory implementations for everything. For a real-world application, this is unsuitable as OMEMO devices would be constantly added.
48 | In order to allow persistence, your application needs to keep track of the following:
49 |
50 | - The `OmemoDevice` assigned to the `OmemoManager`
51 | - `JID -> [int]`: The device list for each JID
52 | - `(JID, device) -> Ratchet`: The actual ratchet
53 |
54 | If you also use the `BlindTrustBeforeVerificationTrustManager`, you additionally need to keep track of:
55 |
56 | - `(JID, device) -> (int, bool)`: The trust level and the enablement state
57 |
58 | ## Contributing
59 |
60 | When submitting a PR, please run the linter using `dart analyze` and make sure that all
61 | tests still pass using `dart test`.
62 |
63 | To ensure uniform commit message formatting, please also use `gitlint` to lint your commit
64 | messages' formatting.
65 |
66 | ## License
67 |
68 | Licensed under the MIT license.
69 |
70 | See `LICENSE`.
71 |
72 | ## Support
73 |
74 | If you like what I do and you want to support me, feel free to donate to me on Ko-Fi.
75 |
76 | [](https://ko-fi.com/papatutuwawa)
77 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:very_good_analysis/analysis_options.yaml
2 | linter:
3 | rules:
4 | public_member_api_docs: false
5 | lines_longer_than_80_chars: false
6 | use_setters_to_change_properties: false
7 | avoid_positional_boolean_parameters: false
8 | avoid_bool_literals_in_conditional_expressions: false
9 |
10 | analyzer:
11 | exclude:
12 | - "lib/src/protobuf/*.dart"
13 | - "example/omemo_dart_example.dart"
14 |
--------------------------------------------------------------------------------
/example/omemo_dart_example.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:omemo_dart/omemo_dart.dart';
3 |
4 | /// This example aims to demonstrate how omemo_dart is used. Since omemo_dart is not
5 | /// dependent on any XMPP library, you need to convert stanzas to the appropriate
6 | /// intermediary format and back.
7 | void main() async {
8 | const aliceJid = 'alice@some.server';
9 | const bobJid = 'bob@other.serve';
10 |
11 | // You are Alice and want to begin using OMEMO, so you first create an OmemoManager.
12 | final aliceManager = OmemoManager(
13 | // Generate Alice's OMEMO device bundle. We can specify how many One-time Prekeys we want, but
14 | // per default, omemo_dart generates 100 (recommended by XEP-0384).
15 | await OmemoDevice.generateNewDevice(aliceJid),
16 | // The trust manager we want to use. In this case, we use the provided one that
17 | // implements "Blind Trust Before Verification". To make things simpler, we keep
18 | // no persistent data and can thus use the MemoryBTBVTrustManager. If we wanted to keep
19 | // the state, we would have to override BlindTrustBeforeVerificationTrustManager.
20 | BlindTrustBeforeVerificationTrustManager(),
21 | // This function is called whenever we need to send an OMEMO heartbeat to [recipient].
22 | // [result] is the encryted data to include. This needs to be wired into your XMPP library's
23 | // OMEMO implementation.
24 | // For simplicity, we use an empty function and imagine it works.
25 | (result, recipient) async => {},
26 | // This function is called whenever we need to fetch the device list for [jid].
27 | // This needs to be wired into your XMPP library's OMEMO implementation.
28 | // For simplicity, we use an empty function and imagine it works.
29 | (jid) async => [],
30 | // This function is called whenever we need to fetch the device bundle with id [id] from [jid].
31 | // This needs to be wired into your XMPP library's OMEMO implementation.
32 | // For simplicity, we use an empty function and imagine it works.
33 | (jid, id) async => null,
34 | // This function is called whenever we need to subscribe to [jid]'s device list PubSub node.
35 | // This needs to be wired into your XMPP library's OMEMO implementation.
36 | // For simplicity, we use an empty function and imagine it works.
37 | (jid) async {},
38 | // This function is called whenever our own device bundle has to be republished to our PEP node.
39 | // This needs to be wired into your XMPP library's OMEMO implementation.
40 | // For simplicity, we use an empty function and imagine it works.
41 | (device) async {},
42 | );
43 |
44 | // Bob, on his side, also creates an [OmemoManager] similar to Alice.
45 | final bobManager = OmemoManager(
46 | await OmemoDevice.generateNewDevice(bobJid),
47 | BlindTrustBeforeVerificationTrustManager(),
48 | (result, recipient) async => {},
49 | (jid) async => [],
50 | (jid, id) async => null,
51 | (jid) async {},
52 | (device) async {},
53 | );
54 |
55 | // Alice prepares to send the message to Bob, so she builds the message stanza and
56 | // collects all the children of the stanza that should be encrypted into a string.
57 | // Note that this leaves out the wrapping stanza, i.e. if we want to send a
58 | // we only include the 's children.
59 | const aliceMessageStanzaBody = '''
60 |
Hello Bob, it's me, Alice!
61 |
62 | ''';
63 |
64 | // Since OMEMO 0.8.3 mandates usage of XEP-0420: Stanza Content Encryption, we have to
65 | // wrap our acual payload - aliceMessageStanzaBody - into an SCE envelope. Note that
66 | // the rpad element must contain a random string. See XEP-0420 for recommendations.
67 | // OMEMO makes the element optional, but let's use for this example.
68 | const envelope = '''
69 |
70 |
71 | $aliceMessageStanzaBody
72 |
73 | s0m3-r4nd0m-b9t3s
74 |
75 |
76 |
77 | ''';
78 |
79 | // Next, we encrypt the envelope element using Alice's [OmemoManager]. It will
80 | // automatically attempt to fetch the device bundles of Bob.
81 | final message = await aliceManager.onOutgoingStanza(
82 | const OmemoOutgoingStanza(
83 | // The bare receiver Jid
84 | [bobJid],
85 |
86 | // The payload we want to encrypt, i.e. the envelope.
87 | envelope,
88 | ),
89 | );
90 |
91 | // In a proper implementation, we would also do some error checking here.
92 |
93 | // Alice now builds the actual message stanza for Bob
94 | final payload = base64.encode(message.ciphertext!);
95 | final aliceDevice = await aliceManager.getDevice();
96 | // Since we know we have just one key for Bob, we take a shortcut. However, in the real
97 | // world, we have to serialise every EncryptedKey to a element and group them
98 | // per Jid.
99 | final key = message.encryptedKeys[bobJid]![0];
100 |
101 | // Note that the key's "kex" attribute refers to key.kex. It just means that the
102 | // encrypted key also contains the required data for Bob to build a session with Alice.
103 | // ignore: unused_local_variable
104 | final aliceStanza = '''
105 |
106 |
107 |
108 |
109 |
110 | ${key.value}
111 |
112 |
113 |
114 |
115 | $payload
116 |
117 |
118 |
119 | ''';
120 |
121 | // Alice can now send this message to Bob using our preferred XMPP library.
122 | // ...
123 |
124 | // Bob now receives an OMEMO encrypted message from Alice and wants to decrypt it.
125 | // Since we have just one key, let's just deserialise the one key by hand.
126 | final keys = [
127 | EncryptedKey(key.rid, key.value, true),
128 | ];
129 |
130 | // Bob extracts the payload and attempts to decrypt it.
131 | // ignore: unused_local_variable
132 | final bobMessage = await bobManager.onIncomingStanza(
133 | OmemoIncomingStanza(
134 | // The bare sender JID of the message. In this case, it's Alice's.
135 | aliceJid,
136 | // The 'sid' attribute of the element. Here, we know that Alice only has one device.
137 | aliceDevice.id,
138 |
139 | /// The decoded elements. from the header. Note that we only include the ones
140 | /// relevant for Bob, so all children of .
141 | keys,
142 |
143 | /// The text of the element, if it exists. If not, then the message might be
144 | /// a hearbeat, where no payload is sent. In that case, use null.
145 | payload,
146 |
147 | /// Since we did not receive this message due to a catch-up mechanism, like MAM, we
148 | /// set this to false. If we, however, did use a catch-up mechanism, we must set this
149 | /// to true to prevent the OPKs from being replaced.
150 | false,
151 | ),
152 | );
153 |
154 | // All Bob has to do now is replace the OMEMO wrapper element
155 | // ) with the content of the element
156 | // of the envelope we just decrypted.
157 |
158 | // Bob now has a session with Alice and can send encrypted message to her.
159 | // Since they both used the BlindTrustBeforeVerificationTrustManager, they currently
160 | // use blind trust, meaning that both Alice and Bob accept new devices without any
161 | // hesitation. If Alice, however, decides to verify one of Bob's devices and sets
162 | // it as verified using
163 | // ```
164 | // await aliceSession.trustManager.setDeviceTrust(bobJid, bobDevice.id, BTBVTrustState.verified)
165 | // ```
166 | // then Alice's OmemoSessionManager won't encrypt to new devices unless they are also
167 | // verified. To prevent user confusion, you should check if every device is trusted
168 | // before sending the message and ask the user for a trust decision.
169 | // If you want to make the BlindTrustBeforeVerificationTrustManager persistent, then
170 | // you need to subclass it and override the `Future commitState()` and
171 | // `Future loadState()` functions. commitState is called everytime the internal
172 | // state gets changed. loadState never gets automatically called but is more of a
173 | // function for the user to restore the trust manager. In those functions you have
174 | // access to `ratchetMap`, which maps a `RatchetMapKey` - essentially a tuple consisting
175 | // of a bare Jid and the device identifier - to the trust state, and `devices` which
176 | // maps a bare Jid to its device identifiers.
177 | // To make the entire OmemoSessionManager persistent, you have two options:
178 | // - use the provided `toJson()` and `fromJson()` functions. They, however, serialise
179 | // and deserialise *ALL* known sessions, so it might be slow.
180 | // - subscribe to the session manager's `eventStream`. There, events get triggered
181 | // everytime a ratchet changes, our own device changes or the internal ratchet map
182 | // gets changed. This give finer control over the the serialisation. The session
183 | // manager can then be restored using its constructor. For a list of events, see
184 | // lib/src/omemo/events.dart.
185 | }
186 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1692799911,
9 | "narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1727586919,
24 | "narHash": "sha256-e/YXG0tO5GWHDS8QQauj8aj4HhXEm602q9swrrlTlKQ=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "2dcd9c55e8914017226f5948ac22c53872a13ee2",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixpkgs-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "root": {
38 | "inputs": {
39 | "flake-utils": "flake-utils",
40 | "nixpkgs": "nixpkgs"
41 | }
42 | },
43 | "systems": {
44 | "locked": {
45 | "lastModified": 1681028828,
46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47 | "owner": "nix-systems",
48 | "repo": "default",
49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50 | "type": "github"
51 | },
52 | "original": {
53 | "owner": "nix-systems",
54 | "repo": "default",
55 | "type": "github"
56 | }
57 | }
58 | },
59 | "root": "root",
60 | "version": 7
61 | }
62 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "omemo_dart";
3 | inputs = {
4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
5 | flake-utils.url = "github:numtide/flake-utils";
6 | };
7 | outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let
8 | pkgs = import nixpkgs {
9 | inherit system;
10 | config = {
11 | android_sdk.accept_license = true;
12 | allowUnfree = true;
13 | };
14 | };
15 | android = pkgs.androidenv.composeAndroidPackages {
16 | # TODO: Find a way to pin these
17 | #toolsVersion = "26.1.1";
18 | #platformToolsVersion = "31.0.3";
19 | #buildToolsVersions = [ "31.0.0" ];
20 | #includeEmulator = true;
21 | #emulatorVersion = "30.6.3";
22 | platformVersions = [ "28" ];
23 | includeSources = false;
24 | includeSystemImages = true;
25 | systemImageTypes = [ "default" ];
26 | abiVersions = [ "x86_64" ];
27 | includeNDK = false;
28 | useGoogleAPIs = false;
29 | useGoogleTVAddOns = false;
30 | };
31 | pinnedJDK = pkgs.jdk11;
32 | in {
33 | devShell = pkgs.mkShell {
34 | buildInputs = with pkgs; [
35 | flutter pinnedJDK android.platform-tools dart # Flutter/Android
36 | gitlint jq # Code hygiene
37 | ripgrep # General utilities
38 | protobuf
39 | ];
40 | ANDROID_HOME = "${android.androidsdk}/libexec/android-sdk";
41 | JAVA_HOME = pinnedJDK;
42 | ANDROID_AVD_HOME = (toString ./.) + "/.android/avd";
43 | };
44 | });
45 | }
46 |
--------------------------------------------------------------------------------
/lib/omemo_dart.dart:
--------------------------------------------------------------------------------
1 | library omemo_dart;
2 |
3 | export 'src/double_ratchet/double_ratchet.dart';
4 | export 'src/errors.dart';
5 | export 'src/helpers.dart';
6 | export 'src/keys.dart';
7 | export 'src/omemo/bundle.dart';
8 | export 'src/omemo/device.dart';
9 | export 'src/omemo/encrypted_key.dart';
10 | export 'src/omemo/encryption_result.dart';
11 | export 'src/omemo/errors.dart';
12 | export 'src/omemo/fingerprint.dart';
13 | export 'src/omemo/omemo.dart';
14 | export 'src/omemo/ratchet_data.dart';
15 | export 'src/omemo/ratchet_map_key.dart';
16 | export 'src/omemo/stanza.dart';
17 | export 'src/trust/base.dart';
18 | export 'src/trust/btbv.dart';
19 | export 'src/x3dh/x3dh.dart';
20 |
--------------------------------------------------------------------------------
/lib/src/common/constants.dart:
--------------------------------------------------------------------------------
1 | /// The overarching assumption is that we use Ed25519 keys for the identity keys
2 | const omemoX3DHInfoString = 'OMEMO X3DH';
3 |
4 | /// The info used for when encrypting the AES key for the actual payload.
5 | const omemoPayloadInfoString = 'OMEMO Payload';
6 |
7 | /// Info string for ENCRYPT
8 | const encryptHkdfInfoString = 'OMEMO Message Key Material';
9 |
10 | /// Amount of messages we may skip per session
11 | const maxSkip = 1000;
12 |
--------------------------------------------------------------------------------
/lib/src/crypto.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:cryptography/cryptography.dart';
3 | import 'package:moxlib/moxlib.dart';
4 | import 'package:omemo_dart/src/errors.dart';
5 | import 'package:omemo_dart/src/keys.dart';
6 |
7 | /// Performs X25519 with [kp] and [pk]. If [identityKey] is set, then
8 | /// it indicates which of [kp] ([identityKey] == 1) or [pk] ([identityKey] == 2)
9 | /// is the identity key. This is needed since the identity key pair/public key is
10 | /// an Ed25519 key, but we need them as X25519 keys for DH.
11 | Future> omemoDH(
12 | OmemoKeyPair kp,
13 | OmemoPublicKey pk,
14 | int identityKey,
15 | ) async {
16 | var ckp = kp;
17 | var cpk = pk;
18 |
19 | if (identityKey == 1) {
20 | ckp = await kp.toCurve25519();
21 | } else if (identityKey == 2) {
22 | cpk = await pk.toCurve25519();
23 | }
24 |
25 | final shared = await Cryptography.instance.x25519().sharedSecretKey(
26 | keyPair: await ckp.asKeyPair(),
27 | remotePublicKey: cpk.asPublicKey(),
28 | );
29 |
30 | return shared.extractBytes();
31 | }
32 |
33 | class HkdfKeyResult {
34 | const HkdfKeyResult(this.encryptionKey, this.authenticationKey, this.iv);
35 | final List encryptionKey;
36 | final List authenticationKey;
37 | final List iv;
38 | }
39 |
40 | /// cryptography _really_ wants to check the MAC output from AES-256-CBC. Since
41 | /// we don't have it, we need the MAC check to always "pass".
42 | class NoMacSecretBox extends SecretBox {
43 | NoMacSecretBox(super.cipherText, {required super.nonce})
44 | : super(mac: Mac.empty);
45 |
46 | @override
47 | Future checkMac({
48 | required MacAlgorithm macAlgorithm,
49 | required SecretKey secretKey,
50 | required List aad,
51 | }) async {}
52 | }
53 |
54 | /// OMEMO 0.8.3 often derives the three keys for encryption, authentication and the IV from
55 | /// some input using HKDF-SHA-256. As such, this is a helper function that already provides
56 | /// those three keys from [input] and the info string [info].
57 | Future deriveEncryptionKeys(List input, String info) async {
58 | final algorithm = Hkdf(
59 | hmac: Hmac(Sha256()),
60 | outputLength: 80,
61 | );
62 | final result = await algorithm.deriveKey(
63 | secretKey: SecretKey(input),
64 | nonce: List.filled(32, 0x0),
65 | info: utf8.encode(info),
66 | );
67 | final bytes = await result.extractBytes();
68 |
69 | return HkdfKeyResult(
70 | bytes.sublist(0, 32),
71 | bytes.sublist(32, 64),
72 | bytes.sublist(64, 80),
73 | );
74 | }
75 |
76 | /// A small helper function to make AES-256-CBC easier. Encrypt [plaintext] using [key] as
77 | /// the encryption key and [iv] as the IV. Returns the ciphertext.
78 | Future> aes256CbcEncrypt(
79 | List plaintext,
80 | List key,
81 | List iv,
82 | ) async {
83 | final algorithm = AesCbc.with256bits(
84 | macAlgorithm: MacAlgorithm.empty,
85 | );
86 | final result = await algorithm.encrypt(
87 | plaintext,
88 | secretKey: SecretKey(key),
89 | nonce: iv,
90 | );
91 |
92 | return result.cipherText;
93 | }
94 |
95 | /// A small helper function to make AES-256-CBC easier. Decrypt [ciphertext] using [key] as
96 | /// the encryption key and [iv] as the IV. Returns the ciphertext.
97 | Future>> aes256CbcDecrypt(
98 | List ciphertext,
99 | List key,
100 | List iv,
101 | ) async {
102 | final algorithm = AesCbc.with256bits(
103 | macAlgorithm: MacAlgorithm.empty,
104 | );
105 | try {
106 | return Result(
107 | await algorithm.decrypt(
108 | NoMacSecretBox(
109 | ciphertext,
110 | nonce: iv,
111 | ),
112 | secretKey: SecretKey(key),
113 | ),
114 | );
115 | } catch (ex) {
116 | return Result(MalformedCiphertextError(ex));
117 | }
118 | }
119 |
120 | /// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes.
121 | /// Calculate the HMAC-SHA-256 of [input] using the authentication key [key] and
122 | /// truncate the output to 16 bytes.
123 | Future> truncatedHmac(List input, List key) async {
124 | final algorithm = Hmac.sha256();
125 | final result = await algorithm.calculateMac(
126 | input,
127 | secretKey: SecretKey(key),
128 | );
129 |
130 | return result.bytes.sublist(0, 16);
131 | }
132 |
--------------------------------------------------------------------------------
/lib/src/double_ratchet/double_ratchet.dart:
--------------------------------------------------------------------------------
1 | import 'package:cryptography/cryptography.dart';
2 | import 'package:hex/hex.dart';
3 | import 'package:meta/meta.dart';
4 | import 'package:moxlib/moxlib.dart';
5 | import 'package:omemo_dart/src/common/constants.dart';
6 | import 'package:omemo_dart/src/crypto.dart';
7 | import 'package:omemo_dart/src/double_ratchet/kdf.dart';
8 | import 'package:omemo_dart/src/errors.dart';
9 | import 'package:omemo_dart/src/helpers.dart';
10 | import 'package:omemo_dart/src/keys.dart';
11 | import 'package:omemo_dart/src/protobuf/schema.pb.dart';
12 |
13 | @immutable
14 | class SkippedKey {
15 | const SkippedKey(this.dh, this.n);
16 |
17 | /// The DH public key for which we skipped a message key.
18 | final OmemoPublicKey dh;
19 |
20 | /// The associated number of the message key we skipped.
21 | final int n;
22 |
23 | @override
24 | bool operator ==(Object other) {
25 | return other is SkippedKey && other.dh == dh && other.n == n;
26 | }
27 |
28 | @override
29 | int get hashCode => dh.hashCode ^ n.hashCode;
30 | }
31 |
32 | @immutable
33 | class KeyExchangeData {
34 | const KeyExchangeData(
35 | this.pkId,
36 | this.spkId,
37 | this.ik,
38 | this.ek,
39 | );
40 |
41 | /// The id of the used OPK.
42 | final int pkId;
43 |
44 | /// The id of the used SPK.
45 | final int spkId;
46 |
47 | /// The ephemeral key used while the key exchange.
48 | final OmemoPublicKey ek;
49 |
50 | /// The identity key used in the key exchange.
51 | final OmemoPublicKey ik;
52 | }
53 |
54 | class OmemoDoubleRatchet {
55 | OmemoDoubleRatchet(
56 | this.dhs, // DHs
57 | this.dhr, // DHr
58 | this.rk, // RK
59 | this.cks, // CKs
60 | this.ckr, // CKr
61 | this.ns, // Ns
62 | this.nr, // Nr
63 | this.pn, // Pn
64 | this.ik,
65 | this.sessionAd,
66 | this.mkSkipped, // MKSKIPPED
67 | this.acknowledged,
68 | this.kex,
69 | );
70 |
71 | /// Sending DH keypair
72 | OmemoKeyPair dhs;
73 |
74 | /// Receiving Public key
75 | OmemoPublicKey? dhr;
76 |
77 | /// 32 byte Root Key
78 | List rk;
79 |
80 | /// Sending and receiving Chain Keys
81 | List? cks;
82 | List? ckr;
83 |
84 | /// Sending and receiving message numbers
85 | int ns;
86 | int nr;
87 |
88 | /// Previous sending chain number
89 | int pn;
90 |
91 | /// The IK public key from the chat partner. Not used for the actual encryption but
92 | /// for verification purposes
93 | final OmemoPublicKey ik;
94 |
95 | /// Associated data for this ratchet.
96 | final List sessionAd;
97 |
98 | /// List of skipped message keys.
99 | final Map> mkSkipped;
100 |
101 | /// The key exchange that was used for initiating the session.
102 | final KeyExchangeData kex;
103 |
104 | /// Indicates whether we received an empty OMEMO message after building a session with
105 | /// the device.
106 | bool acknowledged;
107 |
108 | /// Create an OMEMO session using the Signed Pre Key [spk], the shared secret [sk] that
109 | /// was obtained using a X3DH and the associated data [ad] that was also obtained through
110 | /// a X3DH. [ik] refers to Bob's (the receiver's) IK public key.
111 | static Future initiateNewSession(
112 | OmemoPublicKey spk,
113 | int spkId,
114 | OmemoPublicKey ik,
115 | OmemoPublicKey ownIk,
116 | OmemoPublicKey ek,
117 | List sk,
118 | List ad,
119 | int pkId,
120 | ) async {
121 | final dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
122 | final rk = await kdfRk(sk, await omemoDH(dhs, spk, 0));
123 |
124 | return OmemoDoubleRatchet(
125 | dhs,
126 | spk,
127 | List.from(rk),
128 | List.from(rk),
129 | null,
130 | 0,
131 | 0,
132 | 0,
133 | ik,
134 | ad,
135 | {},
136 | false,
137 | KeyExchangeData(
138 | pkId,
139 | spkId,
140 | ownIk,
141 | ek,
142 | ),
143 | );
144 | }
145 |
146 | /// Create an OMEMO session that was not initiated by the caller using the used Signed
147 | /// Pre Key keypair [spk], the shared secret [sk] that was obtained through a X3DH and
148 | /// the associated data [ad] that was also obtained through a X3DH. [ik] refers to
149 | /// Alice's (the initiator's) IK public key.
150 | static Future acceptNewSession(
151 | OmemoKeyPair spk,
152 | int spkId,
153 | OmemoPublicKey ik,
154 | int pkId,
155 | OmemoPublicKey ek,
156 | List sk,
157 | List ad,
158 | ) async {
159 | return OmemoDoubleRatchet(
160 | spk,
161 | null,
162 | sk,
163 | null,
164 | null,
165 | 0,
166 | 0,
167 | 0,
168 | ik,
169 | ad,
170 | {},
171 | true,
172 | KeyExchangeData(
173 | pkId,
174 | spkId,
175 | ik,
176 | ek,
177 | ),
178 | );
179 | }
180 |
181 | /// Performs a single ratchet step in case we received a new
182 | /// public key in [header].
183 | Future _dhRatchet(OMEMOMessage header) async {
184 | pn = ns;
185 | ns = 0;
186 | nr = 0;
187 | dhr = OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519);
188 | final newRk1 = await kdfRk(
189 | rk,
190 | await omemoDH(
191 | dhs,
192 | dhr!,
193 | 0,
194 | ),
195 | );
196 | rk = List.from(newRk1);
197 | ckr = List.from(newRk1);
198 |
199 | dhs = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
200 | final newRk2 = await kdfRk(
201 | rk,
202 | await omemoDH(
203 | dhs,
204 | dhr!,
205 | 0,
206 | ),
207 | );
208 | rk = List.from(newRk2);
209 | cks = List.from(newRk2);
210 | }
211 |
212 | /// Skip (and keep track of) message keys until our receive counter is
213 | /// equal to [until]. If we would skip too many messages, returns
214 | /// a [SkippingTooManyKeysError]. If not, returns null.
215 | Future _skipMessageKeys(int until) async {
216 | if (nr + maxSkip < until) {
217 | return SkippingTooManyKeysError();
218 | }
219 |
220 | if (ckr != null) {
221 | while (nr < until) {
222 | final newCkr = await kdfCk(ckr!, kdfCkNextChainKey);
223 | final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
224 | ckr = newCkr;
225 |
226 | mkSkipped[SkippedKey(dhr!, nr)] = mk;
227 | nr++;
228 | }
229 | }
230 |
231 | return null;
232 | }
233 |
234 | /// Decrypt [ciphertext] using keys derived from the message key [mk]. Also computes the
235 | /// HMAC from the [OMEMOMessage] embedded in [message].
236 | ///
237 | /// If the computed HMAC does not match the HMAC in [message], returns
238 | /// [InvalidMessageHMACError]. If it matches, returns the decrypted
239 | /// payload.
240 | Future>> _decrypt(
241 | OMEMOAuthenticatedMessage message,
242 | List ciphertext,
243 | List mk,
244 | ) async {
245 | final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
246 |
247 | final hmacInput = concat([sessionAd, message.message]);
248 | final hmacResult = await truncatedHmac(hmacInput, keys.authenticationKey);
249 | if (!listsEqual(hmacResult, message.mac)) {
250 | return Result(InvalidMessageHMACError());
251 | }
252 |
253 | final plaintext =
254 | await aes256CbcDecrypt(ciphertext, keys.encryptionKey, keys.iv);
255 | if (plaintext.isType()) {
256 | return Result(plaintext.get());
257 | }
258 |
259 | return Result(plaintext.get>());
260 | }
261 |
262 | /// Checks whether we could decrypt the payload in [header] with a skipped key. If yes,
263 | /// attempts to decrypt it. If not, returns null.
264 | ///
265 | /// If the decryption is successful, returns the plaintext payload. If an error occurs, like
266 | /// an [InvalidMessageHMACError], that is returned instead.
267 | Future?>> _trySkippedMessageKeys(
268 | OMEMOAuthenticatedMessage message,
269 | OMEMOMessage header,
270 | ) async {
271 | final key = SkippedKey(
272 | OmemoPublicKey.fromBytes(header.dhPub, KeyPairType.x25519),
273 | header.n,
274 | );
275 | if (mkSkipped.containsKey(key)) {
276 | final mk = mkSkipped[key]!;
277 | mkSkipped.remove(key);
278 |
279 | return _decrypt(message, header.ciphertext, mk);
280 | }
281 |
282 | return const Result(null);
283 | }
284 |
285 | /// Decrypt the payload (deeply) embedded in [message].
286 | ///
287 | /// If everything goes well, returns the plaintext payload. If an error occurs, that
288 | /// is returned instead.
289 | Future>> ratchetDecrypt(
290 | OMEMOAuthenticatedMessage message,
291 | ) async {
292 | final header = OMEMOMessage.fromBuffer(message.message);
293 |
294 | // Try skipped keys
295 | final plaintextRaw = await _trySkippedMessageKeys(message, header);
296 | if (plaintextRaw.isType()) {
297 | // Propagate the error
298 | return Result(plaintextRaw.get());
299 | }
300 |
301 | final plaintext = plaintextRaw.get?>();
302 | if (plaintext != null) {
303 | return Result(plaintext);
304 | }
305 |
306 | if (dhr == null || !listsEqual(header.dhPub, await dhr!.getBytes())) {
307 | final skipResult1 = await _skipMessageKeys(header.pn);
308 | if (skipResult1 != null) {
309 | return Result(skipResult1);
310 | }
311 |
312 | await _dhRatchet(header);
313 | }
314 |
315 | final skipResult2 = await _skipMessageKeys(header.n);
316 | if (skipResult2 != null) {
317 | return Result(skipResult2);
318 | }
319 |
320 | final ck = await kdfCk(ckr!, kdfCkNextChainKey);
321 | final mk = await kdfCk(ckr!, kdfCkNextMessageKey);
322 | ckr = ck;
323 | nr++;
324 |
325 | return _decrypt(message, header.ciphertext, mk);
326 | }
327 |
328 | /// Encrypt the payload [plaintext] using the double ratchet session.
329 | Future ratchetEncrypt(List plaintext) async {
330 | // Advance the ratchet
331 | final ck = await kdfCk(cks!, kdfCkNextChainKey);
332 | final mk = await kdfCk(cks!, kdfCkNextMessageKey);
333 | cks = ck;
334 |
335 | // Generate encryption, authentication key and IV
336 | final keys = await deriveEncryptionKeys(mk, encryptHkdfInfoString);
337 | final ciphertext =
338 | await aes256CbcEncrypt(plaintext, keys.encryptionKey, keys.iv);
339 |
340 | // Fill-in the header and serialize it here so we do it only once
341 | final header = OMEMOMessage()
342 | ..dhPub = await dhs.pk.getBytes()
343 | ..pn = pn
344 | ..n = ns
345 | ..ciphertext = ciphertext;
346 | final headerBytes = header.writeToBuffer();
347 |
348 | // Increment the send counter
349 | ns++;
350 |
351 | final newAd = concat([sessionAd, headerBytes]);
352 | final hmac = await truncatedHmac(newAd, keys.authenticationKey);
353 | return OMEMOAuthenticatedMessage()
354 | ..mac = hmac
355 | ..message = headerBytes;
356 | }
357 |
358 | /// Returns a copy of the ratchet.
359 | OmemoDoubleRatchet clone() {
360 | return OmemoDoubleRatchet(
361 | dhs,
362 | dhr,
363 | rk,
364 | cks != null ? List.from(cks!) : null,
365 | ckr != null ? List.from(ckr!) : null,
366 | ns,
367 | nr,
368 | pn,
369 | ik,
370 | sessionAd,
371 | Map>.from(mkSkipped),
372 | acknowledged,
373 | kex,
374 | );
375 | }
376 |
377 | /// Computes the fingerprint of the double ratchet, according to
378 | /// XEP-0384.
379 | Future get fingerprint async {
380 | final curveKey = await ik.toCurve25519();
381 | return HEX.encode(
382 | await curveKey.getBytes(),
383 | );
384 | }
385 |
386 | @visibleForTesting
387 | Future equals(OmemoDoubleRatchet other) async {
388 | final dhrMatch = dhr == null
389 | ? other.dhr == null
390 | :
391 | // ignore: invalid_use_of_visible_for_testing_member
392 | other.dhr != null && await dhr!.equals(other.dhr!);
393 | final ckrMatch = ckr == null
394 | ? other.ckr == null
395 | : other.ckr != null && listsEqual(ckr!, other.ckr!);
396 | final cksMatch = cks == null
397 | ? other.cks == null
398 | : other.cks != null && listsEqual(cks!, other.cks!);
399 |
400 | // ignore: invalid_use_of_visible_for_testing_member
401 | final dhsMatch = await dhs.equals(other.dhs);
402 | // ignore: invalid_use_of_visible_for_testing_member
403 | final ikMatch = await ik.equals(other.ik);
404 |
405 | return dhsMatch &&
406 | ikMatch &&
407 | dhrMatch &&
408 | listsEqual(rk, other.rk) &&
409 | cksMatch &&
410 | ckrMatch &&
411 | ns == other.ns &&
412 | nr == other.nr &&
413 | pn == other.pn &&
414 | listsEqual(sessionAd, other.sessionAd);
415 | }
416 | }
417 |
--------------------------------------------------------------------------------
/lib/src/double_ratchet/kdf.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:cryptography/cryptography.dart';
3 |
4 | /// Info string for KDF_RK
5 | const kdfRkInfoString = 'OMEMO Root Chain';
6 |
7 | /// Flags for KDF_CK
8 | const kdfCkNextMessageKey = 0x01;
9 | const kdfCkNextChainKey = 0x02;
10 |
11 | /// Signals KDF_CK function as specified by OMEMO 0.8.3.
12 | Future> kdfCk(List ck, int constant) async {
13 | final hkdf = Hkdf(hmac: Hmac(Sha256()), outputLength: 32);
14 | final result = await hkdf.deriveKey(
15 | secretKey: SecretKey(ck),
16 | nonce: [constant],
17 | );
18 |
19 | return result.extractBytes();
20 | }
21 |
22 | /// Signals KDF_RK function as specified by OMEMO 0.8.3.
23 | Future> kdfRk(List rk, List dhOut) async {
24 | final algorithm = Hkdf(
25 | hmac: Hmac(Sha256()),
26 | outputLength: 32,
27 | );
28 | final result = await algorithm.deriveKey(
29 | secretKey: SecretKey(dhOut),
30 | nonce: rk,
31 | info: utf8.encode(kdfRkInfoString),
32 | );
33 |
34 | return result.extractBytes();
35 | }
36 |
--------------------------------------------------------------------------------
/lib/src/errors.dart:
--------------------------------------------------------------------------------
1 | abstract class OmemoError {}
2 |
3 | /// Triggered during X3DH if the signature if the SPK does verify to the actual SPK.
4 | class InvalidKeyExchangeSignatureError extends OmemoError {}
5 |
6 | /// Triggered by the Double Ratchet if the computed HMAC does not match the attached HMAC.
7 | class InvalidMessageHMACError extends OmemoError {}
8 |
9 | /// Triggered by the Double Ratchet if skipping messages would cause skipping more than
10 | /// MAXSKIP messages
11 | class SkippingTooManyKeysError extends OmemoError {}
12 |
13 | /// Triggered by the Session Manager if the message key is not encrypted for the device.
14 | class NotEncryptedForDeviceError extends OmemoError {}
15 |
16 | /// Triggered by the Session Manager when the identifier of the used Signed Prekey
17 | /// is neither the current SPK's identifier nor the old one's.
18 | class UnknownSignedPrekeyError extends OmemoError {}
19 |
20 | /// Triggered by the OmemoManager when we could not encrypt a message as we have
21 | /// no key material available. That happens, for example, when we want to create a
22 | /// ratchet session with a JID we had no session with but fetching the device bundle
23 | /// failed.
24 | class NoKeyMaterialAvailableError extends OmemoError {}
25 |
26 | /// A non-key-exchange message was received that was encrypted for our device, but we have no ratchet with
27 | /// the device that sent the message.
28 | class NoSessionWithDeviceError extends OmemoError {}
29 |
30 | /// Caused when the AES-256 CBC decryption failed.
31 | class MalformedCiphertextError extends OmemoError {
32 | MalformedCiphertextError(this.ex);
33 |
34 | /// The exception that was raised while decryption.
35 | final Object ex;
36 | }
37 |
38 | /// Caused by an empty element
39 | class MalformedEncryptedKeyError extends OmemoError {}
40 |
--------------------------------------------------------------------------------
/lib/src/helpers.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:math';
3 |
4 | /// Flattens [inputs] and concatenates the elements.
5 | List concat(List> inputs) {
6 | final tmp = List.empty(growable: true);
7 | for (final input in inputs) {
8 | tmp.addAll(input);
9 | }
10 |
11 | return tmp;
12 | }
13 |
14 | /// Compares the two lists [a] and [b] and return true if [a] and [b] are index-by-index
15 | /// equal. Returns false, if they are not "equal";
16 | bool listsEqual(List a, List b) {
17 | // TODO(Unknown): Do we need to use a constant time comparison?
18 | if (a.length != b.length) return false;
19 |
20 | for (var i = 0; i < a.length; i++) {
21 | if (a[i] != b[i]) return false;
22 | }
23 |
24 | return true;
25 | }
26 |
27 | /// Use Dart's cryptographically secure random number generator at Random.secure()
28 | /// to generate [length] random numbers between 0 and 256 exclusive.
29 | List generateRandomBytes(int length) {
30 | final bytes = List.empty(growable: true);
31 | final r = Random.secure();
32 | for (var i = 0; i < length; i++) {
33 | bytes.add(r.nextInt(256));
34 | }
35 |
36 | return bytes;
37 | }
38 |
39 | /// Generate a random number between 0 inclusive and 2**32 exclusive (2**32 - 1 inclusive).
40 | int generateRandom32BitNumber() {
41 | return Random.secure().nextInt(4294967295 /*pow(2, 32) - 1*/);
42 | }
43 |
44 | /// Describes the differences between two lists in terms of its items.
45 | class ListDiff {
46 | ListDiff(this.added, this.removed);
47 |
48 | /// The items that were added.
49 | final List added;
50 |
51 | /// The items that were removed.
52 | final List removed;
53 | }
54 |
55 | extension AppendToListOrCreateExtension on Map> {
56 | /// Create or append [value] to the list identified with key [key].
57 | void appendOrCreate(K key, V value, {bool checkExistence = false}) {
58 | if (containsKey(key)) {
59 | if (!checkExistence) {
60 | this[key]!.add(value);
61 | }
62 | if (!this[key]!.contains(value)) {
63 | this[key]!.add(value);
64 | }
65 | } else {
66 | this[key] = [value];
67 | }
68 | }
69 | }
70 |
71 | extension StringFromBase64Extension on String {
72 | /// Base64-decode this string. Useful for doing `someString?.fromBase64()` instead
73 | /// of `someString != null ? base64Decode(someString) : null`.
74 | List fromBase64() => base64Decode(this);
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/keys.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:cryptography/cryptography.dart';
3 | import 'package:meta/meta.dart';
4 | import 'package:omemo_dart/src/helpers.dart';
5 | import 'package:pinenacl/api.dart';
6 | import 'package:pinenacl/tweetnacl.dart';
7 |
8 | const privateKeyLength = 32;
9 | const publicKeyLength = 32;
10 |
11 | class OmemoPublicKey {
12 | const OmemoPublicKey(this._pubkey);
13 |
14 | factory OmemoPublicKey.fromBytes(List bytes, KeyPairType type) {
15 | return OmemoPublicKey(
16 | SimplePublicKey(
17 | bytes,
18 | type: type,
19 | ),
20 | );
21 | }
22 |
23 | final SimplePublicKey _pubkey;
24 |
25 | KeyPairType get type => _pubkey.type;
26 |
27 | /// Return the bytes that comprise the public key.
28 | Future> getBytes() async => _pubkey.bytes;
29 |
30 | /// Returns the public key encoded as base64.
31 | Future asBase64() async => base64Encode(_pubkey.bytes);
32 |
33 | Future toCurve25519() async {
34 | assert(
35 | type == KeyPairType.ed25519,
36 | 'Cannot convert non-Ed25519 public key to X25519',
37 | );
38 |
39 | final pkc = Uint8List(publicKeyLength);
40 | TweetNaClExt.crypto_sign_ed25519_pk_to_x25519_pk(
41 | pkc,
42 | Uint8List.fromList(await getBytes()),
43 | );
44 |
45 | return OmemoPublicKey(
46 | SimplePublicKey(List.from(pkc), type: KeyPairType.x25519),
47 | );
48 | }
49 |
50 | SimplePublicKey asPublicKey() => _pubkey;
51 |
52 | @visibleForTesting
53 | Future equals(OmemoPublicKey key) async {
54 | return type == key.type &&
55 | listsEqual(
56 | await getBytes(),
57 | await key.getBytes(),
58 | );
59 | }
60 | }
61 |
62 | class OmemoPrivateKey {
63 | const OmemoPrivateKey(this._privkey, this.type);
64 | final List _privkey;
65 | final KeyPairType type;
66 |
67 | Future> getBytes() async => _privkey;
68 |
69 | Future toCurve25519() async {
70 | assert(
71 | type == KeyPairType.ed25519,
72 | 'Cannot convert non-Ed25519 private key to X25519',
73 | );
74 |
75 | final skc = Uint8List(privateKeyLength);
76 | TweetNaClExt.crypto_sign_ed25519_sk_to_x25519_sk(
77 | skc,
78 | Uint8List.fromList(await getBytes()),
79 | );
80 |
81 | return OmemoPrivateKey(List.from(skc), KeyPairType.x25519);
82 | }
83 |
84 | @visibleForTesting
85 | Future equals(OmemoPrivateKey key) async {
86 | return type == key.type &&
87 | listsEqual(
88 | await getBytes(),
89 | await key.getBytes(),
90 | );
91 | }
92 | }
93 |
94 | /// A generic wrapper class for both Ed25519 and X25519 keypairs
95 | class OmemoKeyPair {
96 | const OmemoKeyPair(this.pk, this.sk, this.type);
97 |
98 | /// Create an OmemoKeyPair just from a [type] and the bytes of the private and public
99 | /// key.
100 | factory OmemoKeyPair.fromBytes(
101 | List publicKey,
102 | List privateKey,
103 | KeyPairType type,
104 | ) {
105 | return OmemoKeyPair(
106 | OmemoPublicKey.fromBytes(
107 | publicKey,
108 | type,
109 | ),
110 | OmemoPrivateKey(
111 | privateKey,
112 | type,
113 | ),
114 | type,
115 | );
116 | }
117 |
118 | /// Generate a completely new random OmemoKeyPair of type [type]. [type] must be either
119 | /// KeyPairType.ed25519 or KeyPairType.x25519.
120 | static Future generateNewPair(KeyPairType type) async {
121 | assert(
122 | type == KeyPairType.ed25519 || type == KeyPairType.x25519,
123 | 'Keypair must be either Ed25519 or X25519',
124 | );
125 |
126 | SimpleKeyPair kp;
127 | if (type == KeyPairType.ed25519) {
128 | final ed = Ed25519();
129 | kp = await ed.newKeyPair();
130 | } else if (type == KeyPairType.x25519) {
131 | final x = Cryptography.instance.x25519();
132 | kp = await x.newKeyPair();
133 | } else {
134 | // Should never happen
135 | throw Exception();
136 | }
137 |
138 | final kpd = await kp.extract();
139 |
140 | return OmemoKeyPair(
141 | OmemoPublicKey(await kp.extractPublicKey()),
142 | OmemoPrivateKey(await kpd.extractPrivateKeyBytes(), type),
143 | type,
144 | );
145 | }
146 |
147 | final KeyPairType type;
148 | final OmemoPublicKey pk;
149 | final OmemoPrivateKey sk;
150 |
151 | /// Return the bytes that comprise the public key.
152 | Future toCurve25519() async {
153 | assert(
154 | type == KeyPairType.ed25519,
155 | 'Cannot convert non-Ed25519 keypair to X25519',
156 | );
157 |
158 | return OmemoKeyPair(
159 | await pk.toCurve25519(),
160 | await sk.toCurve25519(),
161 | KeyPairType.x25519,
162 | );
163 | }
164 |
165 | Future asKeyPair() async {
166 | return SimpleKeyPairData(
167 | await sk.getBytes(),
168 | publicKey: pk.asPublicKey(),
169 | type: type,
170 | );
171 | }
172 |
173 | @visibleForTesting
174 | Future equals(OmemoKeyPair pair) async {
175 | return type == pair.type &&
176 | await pk.equals(pair.pk) &&
177 | await sk.equals(pair.sk);
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/lib/src/omemo/bundle.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:cryptography/cryptography.dart';
3 | import 'package:hex/hex.dart';
4 | import 'package:omemo_dart/src/keys.dart';
5 |
6 | class OmemoBundle {
7 | const OmemoBundle(
8 | this.jid,
9 | this.id,
10 | this.spkEncoded,
11 | this.spkId,
12 | this.spkSignatureEncoded,
13 | this.ikEncoded,
14 | this.opksEncoded,
15 | );
16 |
17 | /// The bare Jid the Bundle belongs to
18 | final String jid;
19 |
20 | /// The device Id
21 | final int id;
22 |
23 | /// The SPK but base64 encoded
24 | final String spkEncoded;
25 | final int spkId;
26 |
27 | /// The SPK signature but base64 encoded
28 | final String spkSignatureEncoded;
29 |
30 | /// The IK but base64 encoded
31 | final String ikEncoded;
32 |
33 | /// The mapping of a OPK's id to the base64 encoded data
34 | final Map opksEncoded;
35 |
36 | OmemoPublicKey get spk {
37 | final data = base64Decode(spkEncoded);
38 | return OmemoPublicKey.fromBytes(data, KeyPairType.x25519);
39 | }
40 |
41 | OmemoPublicKey get ik {
42 | final data = base64Decode(ikEncoded);
43 | return OmemoPublicKey.fromBytes(data, KeyPairType.ed25519);
44 | }
45 |
46 | OmemoPublicKey getOpk(int id) {
47 | final data = base64Decode(opksEncoded[id]!);
48 | return OmemoPublicKey.fromBytes(data, KeyPairType.x25519);
49 | }
50 |
51 | List get spkSignature => base64Decode(spkSignatureEncoded);
52 |
53 | /// Calculates the fingerprint of the bundle (See
54 | /// https://xmpp.org/extensions/xep-0384.html#security § 2).
55 | Future getFingerprint() async {
56 | return HEX.encode(await ik.getBytes());
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/src/omemo/decryption_result.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 | import 'package:omemo_dart/src/errors.dart';
3 |
4 | @immutable
5 | class DecryptionResult {
6 | const DecryptionResult(
7 | this.payload,
8 | this.usedOpkId,
9 | this.newRatchets,
10 | this.replacedRatchets,
11 | this.error,
12 | );
13 |
14 | /// The decrypted payload or null, if it was an empty OMEMO message.
15 | final String? payload;
16 |
17 | /// In case a key exchange has been performed: The id of the used OPK. Useful for
18 | /// replacing the OPK after a message catch-up.
19 | final int? usedOpkId;
20 |
21 | /// Mapping of JIDs to a list of device ids for which we created a new ratchet session.
22 | final Map> newRatchets;
23 |
24 | /// Similar to [newRatchets], but the ratchets listed in [replacedRatchets] where also existent before
25 | /// and replaced with the new ratchet.
26 | final Map> replacedRatchets;
27 |
28 | /// The error that occurred during decryption or null, if no error occurred.
29 | final OmemoError? error;
30 | }
31 |
--------------------------------------------------------------------------------
/lib/src/omemo/device.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'package:cryptography/cryptography.dart';
3 | import 'package:hex/hex.dart';
4 | import 'package:meta/meta.dart';
5 | import 'package:omemo_dart/src/helpers.dart';
6 | import 'package:omemo_dart/src/keys.dart';
7 | import 'package:omemo_dart/src/omemo/bundle.dart';
8 | import 'package:omemo_dart/src/x3dh/x3dh.dart';
9 |
10 | /// This class represents an OmemoBundle but with all keypairs belonging to the keys
11 | @immutable
12 | class OmemoDevice {
13 | const OmemoDevice(
14 | this.jid,
15 | this.id,
16 | this.ik,
17 | this.spk,
18 | this.spkId,
19 | this.spkSignature,
20 | this.oldSpk,
21 | this.oldSpkId,
22 | this.opks,
23 | );
24 |
25 | /// Generate a completely new device, i.e. cryptographic identity.
26 | static Future generateNewDevice(
27 | String jid, {
28 | int opkAmount = 100,
29 | }) async {
30 | final id = generateRandom32BitNumber();
31 | final ik = await OmemoKeyPair.generateNewPair(KeyPairType.ed25519);
32 | final spk = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
33 | final spkId = generateRandom32BitNumber();
34 | final signature = await sig(ik, await spk.pk.getBytes());
35 |
36 | final opks = {};
37 | for (var i = 0; i < opkAmount; i++) {
38 | // Generate unique ids for each key
39 | while (true) {
40 | final opkId = generateRandom32BitNumber();
41 | if (opks.containsKey(opkId)) {
42 | continue;
43 | }
44 |
45 | opks[opkId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
46 | break;
47 | }
48 | }
49 |
50 | return OmemoDevice(jid, id, ik, spk, spkId, signature, null, null, opks);
51 | }
52 |
53 | /// Our bare Jid
54 | final String jid;
55 |
56 | /// The device Id
57 | final int id;
58 |
59 | /// The identity key
60 | final OmemoKeyPair ik;
61 |
62 | /// The signed prekey...
63 | final OmemoKeyPair spk;
64 |
65 | /// ...its Id, ...
66 | final int spkId;
67 |
68 | /// ...and its signature
69 | final List spkSignature;
70 |
71 | /// The old Signed Prekey...
72 | final OmemoKeyPair? oldSpk;
73 |
74 | /// ...and its Id
75 | final int? oldSpkId;
76 |
77 | /// Map of an id to the associated Onetime-Prekey
78 | final Map opks;
79 |
80 | /// This replaces the Onetime-Prekey with id [id] with a completely new one. Returns
81 | /// a new Device object that copies over everything but replaces said key.
82 | @internal
83 | Future replaceOnetimePrekey(int id) async {
84 | opks.remove(id);
85 |
86 | // Generate a new unique id for the OPK.
87 | while (true) {
88 | final newId = generateRandom32BitNumber();
89 | if (opks.containsKey(newId)) {
90 | continue;
91 | }
92 |
93 | opks[newId] = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
94 | break;
95 | }
96 |
97 | return OmemoDevice(
98 | jid,
99 | this.id,
100 | ik,
101 | spk,
102 | spkId,
103 | spkSignature,
104 | oldSpk,
105 | oldSpkId,
106 | opks,
107 | );
108 | }
109 |
110 | /// This replaces the Signed-Prekey with a completely new one. Returns a new Device object
111 | /// that copies over everything but replaces the Signed-Prekey and its signature.
112 | @internal
113 | Future replaceSignedPrekey() async {
114 | final newSpk = await OmemoKeyPair.generateNewPair(KeyPairType.x25519);
115 | final newSpkId = generateRandom32BitNumber();
116 | final newSignature = await sig(ik, await newSpk.pk.getBytes());
117 |
118 | return OmemoDevice(
119 | jid,
120 | id,
121 | ik,
122 | newSpk,
123 | newSpkId,
124 | newSignature,
125 | spk,
126 | spkId,
127 | opks,
128 | );
129 | }
130 |
131 | /// Returns a new device that is equal to this one with the exception that the new
132 | /// device's id is a new number between 0 and 2**32 - 1.
133 | @internal
134 | OmemoDevice withNewId() {
135 | return OmemoDevice(
136 | jid,
137 | generateRandom32BitNumber(),
138 | ik,
139 | spk,
140 | spkId,
141 | spkSignature,
142 | oldSpk,
143 | oldSpkId,
144 | opks,
145 | );
146 | }
147 |
148 | /// Converts this device into an OmemoBundle that could be used for publishing.
149 | Future toBundle() async {
150 | final encodedOpks = {};
151 |
152 | for (final opkKey in opks.keys) {
153 | encodedOpks[opkKey] = base64.encode(await opks[opkKey]!.pk.getBytes());
154 | }
155 |
156 | return OmemoBundle(
157 | jid,
158 | id,
159 | base64.encode(await spk.pk.getBytes()),
160 | spkId,
161 | base64.encode(spkSignature),
162 | base64.encode(await ik.pk.getBytes()),
163 | encodedOpks,
164 | );
165 | }
166 |
167 | /// Returns the fingerprint of the current device
168 | Future getFingerprint() async {
169 | // Since the local key is Ed25519, we must convert it to Curve25519 first
170 | final curveKey = await ik.pk.toCurve25519();
171 | return HEX.encode(await curveKey.getBytes());
172 | }
173 |
174 | @visibleForTesting
175 | Future equals(OmemoDevice other) async {
176 | var opksMatch = true;
177 | if (opks.length != other.opks.length) {
178 | opksMatch = false;
179 | } else {
180 | for (final entry in opks.entries) {
181 | // ignore: invalid_use_of_visible_for_testing_member
182 | final matches =
183 | // ignore: invalid_use_of_visible_for_testing_member
184 | await other.opks[entry.key]?.equals(entry.value) ?? false;
185 | if (!matches) {
186 | opksMatch = false;
187 | }
188 | }
189 | }
190 |
191 | // ignore: invalid_use_of_visible_for_testing_member
192 | final ikMatch = await ik.equals(other.ik);
193 | // ignore: invalid_use_of_visible_for_testing_member
194 | final spkMatch = await spk.equals(other.spk);
195 | // ignore: invalid_use_of_visible_for_testing_member
196 | final oldSpkMatch = oldSpk != null
197 | // ignore: invalid_use_of_visible_for_testing_member
198 | ? await oldSpk!.equals(other.oldSpk!)
199 | : other.oldSpk == null;
200 | return id == other.id &&
201 | ikMatch &&
202 | spkMatch &&
203 | oldSpkMatch &&
204 | jid == other.jid &&
205 | listsEqual(spkSignature, other.spkSignature) &&
206 | spkId == other.spkId &&
207 | oldSpkId == other.oldSpkId &&
208 | opksMatch;
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/lib/src/omemo/encrypted_key.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:meta/meta.dart';
4 |
5 | /// EncryptedKey is the intermediary format of a element in the OMEMO message's
6 | /// header.
7 | @immutable
8 | class EncryptedKey {
9 | const EncryptedKey(this.rid, this.value, this.kex);
10 |
11 | /// The id of the device the key is encrypted for.
12 | final int rid;
13 |
14 | /// The base64-encoded payload.
15 | final String value;
16 |
17 | /// Flag indicating whether the payload is a OMEMOKeyExchange (true) or
18 | /// an OMEMOAuthenticatedMessage (false).
19 | final bool kex;
20 |
21 | /// The base64-decoded payload.
22 | List get data => base64Decode(value);
23 | }
24 |
--------------------------------------------------------------------------------
/lib/src/omemo/encryption_result.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 | import 'package:omemo_dart/src/omemo/encrypted_key.dart';
3 | import 'package:omemo_dart/src/omemo/errors.dart';
4 |
5 | @immutable
6 | class EncryptionResult {
7 | const EncryptionResult(
8 | this.ciphertext,
9 | this.encryptedKeys,
10 | this.deviceEncryptionErrors,
11 | this.newRatchets,
12 | this.replacedRatchets,
13 | this.canSend,
14 | );
15 |
16 | /// The actual message that was encrypted.
17 | final List? ciphertext;
18 |
19 | /// Mapping of the device Id to the key for decrypting ciphertext, encrypted
20 | /// for the ratchet with said device Id.
21 | final Map> encryptedKeys;
22 |
23 | /// Mapping of a JID to
24 | final Map> deviceEncryptionErrors;
25 |
26 | /// Mapping of JIDs to a list of device ids for which we created a new ratchet session.
27 | final Map> newRatchets;
28 |
29 | /// Similar to [newRatchets], but the ratchets listed in [replacedRatchets] where also existent before
30 | /// and replaced with the new ratchet.
31 | final Map> replacedRatchets;
32 |
33 | /// A flag indicating that the message could be sent like that, i.e. we were able
34 | /// to encrypt to at-least one device per recipient.
35 | final bool canSend;
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/omemo/errors.dart:
--------------------------------------------------------------------------------
1 | import 'package:omemo_dart/src/errors.dart';
2 |
3 | /// Returned on encryption, if encryption failed for some reason.
4 | class EncryptToJidError extends OmemoError {
5 | EncryptToJidError(this.device, this.error);
6 |
7 | /// The device the error occurred with
8 | final int? device;
9 |
10 | /// The actual error.
11 | final OmemoError error;
12 | }
13 |
--------------------------------------------------------------------------------
/lib/src/omemo/fingerprint.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 |
3 | @immutable
4 | class DeviceFingerprint {
5 | const DeviceFingerprint(this.deviceId, this.fingerprint);
6 | final String fingerprint;
7 | final int deviceId;
8 |
9 | @override
10 | bool operator ==(Object other) {
11 | return other is DeviceFingerprint &&
12 | fingerprint == other.fingerprint &&
13 | deviceId == other.deviceId;
14 | }
15 |
16 | @override
17 | int get hashCode => fingerprint.hashCode ^ deviceId.hashCode;
18 | }
19 |
--------------------------------------------------------------------------------
/lib/src/omemo/omemo.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 | import 'package:collection/collection.dart';
4 | import 'package:cryptography/cryptography.dart';
5 | import 'package:logging/logging.dart';
6 | import 'package:meta/meta.dart';
7 | import 'package:moxlib/moxlib.dart';
8 | import 'package:omemo_dart/src/common/constants.dart';
9 | import 'package:omemo_dart/src/crypto.dart';
10 | import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
11 | import 'package:omemo_dart/src/errors.dart';
12 | import 'package:omemo_dart/src/helpers.dart';
13 | import 'package:omemo_dart/src/keys.dart';
14 | import 'package:omemo_dart/src/omemo/bundle.dart';
15 | import 'package:omemo_dart/src/omemo/decryption_result.dart';
16 | import 'package:omemo_dart/src/omemo/device.dart';
17 | import 'package:omemo_dart/src/omemo/encrypted_key.dart';
18 | import 'package:omemo_dart/src/omemo/encryption_result.dart';
19 | import 'package:omemo_dart/src/omemo/errors.dart';
20 | import 'package:omemo_dart/src/omemo/fingerprint.dart';
21 | import 'package:omemo_dart/src/omemo/queue.dart';
22 | import 'package:omemo_dart/src/omemo/ratchet_data.dart';
23 | import 'package:omemo_dart/src/omemo/ratchet_map_key.dart';
24 | import 'package:omemo_dart/src/omemo/stanza.dart';
25 | import 'package:omemo_dart/src/protobuf/schema.pb.dart';
26 | import 'package:omemo_dart/src/trust/base.dart';
27 | import 'package:omemo_dart/src/x3dh/x3dh.dart';
28 | import 'package:synchronized/synchronized.dart';
29 |
30 | class OmemoDataPackage {
31 | const OmemoDataPackage(this.devices, this.ratchets);
32 |
33 | /// The device list for the given JID.
34 | final List devices;
35 |
36 | /// The ratchets for the JID.
37 | final Map ratchets;
38 | }
39 |
40 | /// Callback type definitions
41 |
42 | /// Directly "package" [result] into an OMEMO message and send it to [recipientJid].
43 | typedef SendEmptyOmemoMessageFunction = Future Function(
44 | EncryptionResult result,
45 | String recipientJid,
46 | );
47 |
48 | /// Fetches the device list for [jid]. If no device list could be fetched, returns null.
49 | typedef FetchDeviceListFunction = Future?> Function(String jid);
50 |
51 | /// Fetch the device bundle for the device with id @id of jid. If it cannot be fetched, return null.
52 | typedef FetchDeviceBundleFunction = Future Function(
53 | String jid,
54 | int id,
55 | );
56 |
57 | /// Subscribes to the device list node of [jid].
58 | typedef DeviceListSubscribeFunction = Future Function(String jid);
59 |
60 | /// Publishes the device bundle on our own PEP node.
61 | typedef PublishDeviceBundleFunction = Future Function(OmemoDevice device);
62 |
63 | /// Commits the device list [devices] for [jid] to persistent storage.
64 | typedef CommitDeviceListCallback = Future Function(
65 | String jid,
66 | List devices,
67 | );
68 |
69 | /// A stub implementation of [CommitDeviceListCallback].
70 | Future commitDeviceListStub(
71 | String _,
72 | List __,
73 | ) async {}
74 |
75 | /// Commits the mapping of the (new) ratchets in [ratchets] to persistent storage.
76 | typedef CommitRatchetsCallback = Future Function(
77 | List ratchets,
78 | );
79 |
80 | /// A stub implementation of [CommitRatchetsCallback];
81 | Future commitRatchetsStub(List _) async {}
82 |
83 | /// Commits the device [device] to persistent storage.
84 | typedef CommitDeviceCallback = Future Function(OmemoDevice device);
85 |
86 | /// A stub implementation of [CommitDeviceCallback].
87 | Future commitDeviceStub(OmemoDevice device) async {}
88 |
89 | /// Removes the ratchets identified by their keys in [ratchets] from persistent storage.
90 | typedef RemoveRatchetsFunction = Future Function(
91 | List ratchets,
92 | );
93 |
94 | /// A stub implementation of [RemoveRatchetsFunction].
95 | Future removeRatchetsStub(List ratchets) async {}
96 |
97 | /// Loads all the required data for the ratchets of [jid].
98 | typedef LoadRatchetsCallback = Future Function(String jid);
99 |
100 | /// A stub implementation of [LoadRatchetsCallback].
101 | Future loadRatchetsStub(String _) async => null;
102 |
103 | class OmemoManager {
104 | OmemoManager(
105 | this._device,
106 | this._trustManager,
107 | this.sendEmptyOmemoMessageImpl,
108 | this.fetchDeviceListImpl,
109 | this.fetchDeviceBundleImpl,
110 | this.subscribeToDeviceListNodeImpl,
111 | this.publishDeviceBundle, {
112 | this.commitRatchets = commitRatchetsStub,
113 | this.commitDeviceList = commitDeviceListStub,
114 | this.commitDevice = commitDeviceStub,
115 | this.removeRatchets = removeRatchetsStub,
116 | this.loadRatchets = loadRatchetsStub,
117 | });
118 |
119 | final Logger _log = Logger('OmemoManager');
120 |
121 | /// Functions for connecting with the OMEMO library
122 |
123 | /// Send an empty OMEMO:2 message using the encrypted payload @result to
124 | /// @recipientJid.
125 | final SendEmptyOmemoMessageFunction sendEmptyOmemoMessageImpl;
126 |
127 | /// Fetch the list of device ids associated with @jid. If the device list cannot be
128 | /// fetched, return null.
129 | final FetchDeviceListFunction fetchDeviceListImpl;
130 |
131 | /// Fetch the device bundle for the device with id @id of jid. If it cannot be fetched, return null.
132 | final FetchDeviceBundleFunction fetchDeviceBundleImpl;
133 |
134 | /// Subscribe to the device list PEP node of @jid.
135 | final DeviceListSubscribeFunction subscribeToDeviceListNodeImpl;
136 |
137 | /// Publishes the device bundle on the PEP node.
138 | final PublishDeviceBundleFunction publishDeviceBundle;
139 |
140 | /// Callback to commit the ratchet to persistent storage.
141 | final CommitRatchetsCallback commitRatchets;
142 |
143 | /// Callback to commit the device list to persistent storage.
144 | final CommitDeviceListCallback commitDeviceList;
145 |
146 | /// Callback to commit the device to persistent storage.
147 | final CommitDeviceCallback commitDevice;
148 |
149 | /// Callback to remove ratchets from persistent storage.
150 | final RemoveRatchetsFunction removeRatchets;
151 |
152 | /// Callback to load ratchets from persistent storage.
153 | final LoadRatchetsCallback loadRatchets;
154 |
155 | /// Map bare JID to its known devices
156 | final Map> _deviceList = {};
157 |
158 | /// Map bare JIDs to whether we already requested the device list once
159 | final Map _deviceListRequested = {};
160 |
161 | /// Map bare a ratchet key to its ratchet. Note that this is also locked by
162 | /// _ratchetCriticalSectionLock.
163 | final Map _ratchetMap = {};
164 |
165 | /// Map bare JID to whether we already tried to subscribe to the device list node.
166 | final Map _subscriptionMap = {};
167 |
168 | /// List of JIDs for which we cached trust data, the device list, and the ratchets.
169 | final List _cachedJids = [];
170 |
171 | /// For preventing a race condition in encryption/decryption
172 | final RatchetAccessQueue _ratchetQueue = RatchetAccessQueue();
173 |
174 | /// The OmemoManager's trust management
175 | final TrustManager _trustManager;
176 |
177 | /// Our own keys...
178 | final Lock _deviceLock = Lock();
179 | // ignore: prefer_final_fields
180 | OmemoDevice _device;
181 |
182 | Future _cacheJidsIfNeccessary(List jids) async {
183 | for (final jid in jids) {
184 | await _cacheJidIfNeccessary(jid);
185 | }
186 | }
187 |
188 | Future _cacheJidIfNeccessary(String jid) async {
189 | // JID is already cached. We don't have to do anything.
190 | if (_cachedJids.contains(jid)) {
191 | return;
192 | }
193 |
194 | _cachedJids.add(jid);
195 | final result = await loadRatchets(jid);
196 | if (result == null) {
197 | _log.fine('Did not load ratchet data for $jid. Assuming there is none.');
198 | return;
199 | }
200 |
201 | // Cache the data
202 | _deviceList[jid] = result.devices;
203 | _ratchetMap.addAll(result.ratchets);
204 |
205 | // Load trust data
206 | await _trustManager.loadTrustData(jid);
207 | }
208 |
209 | Future> _decryptAndVerifyHmac(
210 | List? ciphertext,
211 | List keyAndHmac,
212 | ) async {
213 | // Empty OMEMO messages should just have the key decrypted and/or session set up.
214 | if (ciphertext == null) {
215 | return const Result(null);
216 | }
217 |
218 | final key = keyAndHmac.sublist(0, 32);
219 | final hmac = keyAndHmac.sublist(32, 48);
220 | final derivedKeys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
221 | final computedHmac =
222 | await truncatedHmac(ciphertext, derivedKeys.authenticationKey);
223 | if (!listsEqual(hmac, computedHmac)) {
224 | return Result(InvalidMessageHMACError());
225 | }
226 |
227 | final result = await aes256CbcDecrypt(
228 | ciphertext,
229 | derivedKeys.encryptionKey,
230 | derivedKeys.iv,
231 | );
232 | if (result.isType()) {
233 | return Result(
234 | result.get(),
235 | );
236 | }
237 |
238 | return Result(
239 | utf8.decode(
240 | result.get>(),
241 | ),
242 | );
243 | }
244 |
245 | /// Fetches the device list from the server for [jid] and downloads OMEMO bundles
246 | /// for devices we have no session with.
247 | ///
248 | /// Returns a list of new bundles, that may be empty.
249 | Future> _fetchNewOmemoBundles(String jid) async {
250 | // Do we have to request the device list or are we already up-to-date?
251 | if (!_deviceListRequested.containsKey(jid) ||
252 | !_deviceList.containsKey(jid)) {
253 | final newDeviceList = await fetchDeviceListImpl(jid);
254 | if (newDeviceList != null) {
255 | // Figure out what bundles we must fetch
256 | _deviceList[jid] = newDeviceList;
257 | _deviceListRequested[jid] = true;
258 |
259 | await commitDeviceList(
260 | jid,
261 | newDeviceList,
262 | );
263 | }
264 | }
265 |
266 | // Check that we have the device list
267 | if (!_deviceList.containsKey(jid)) {
268 | _log.warning('$jid not tracked in device list.');
269 | return [];
270 | }
271 |
272 | final ownDevice = await getDevice();
273 | final bundlesToFetch = _deviceList[jid]!.where((device) {
274 | // Do not include our current device, if we request bundles for our own JID.
275 | if (ownDevice.jid == jid && device == ownDevice.id) {
276 | return false;
277 | }
278 |
279 | return !_ratchetMap.containsKey(RatchetMapKey(jid, device));
280 | });
281 | if (bundlesToFetch.isEmpty) {
282 | return [];
283 | }
284 |
285 | // Fetch the new bundles
286 | _log.finest('Fetching bundles $bundlesToFetch for $jid');
287 | final bundles = [];
288 | for (final device in bundlesToFetch) {
289 | final bundle = await fetchDeviceBundleImpl(jid, device);
290 | if (bundle != null) {
291 | bundles.add(bundle);
292 | } else {
293 | _log.warning('Failed to fetch bundle $jid:$device');
294 | }
295 | }
296 |
297 | return bundles;
298 | }
299 |
300 | Future _maybeSendEmptyMessage(
301 | RatchetMapKey key,
302 | bool created,
303 | bool replaced,
304 | ) async {
305 | final ratchet = _ratchetMap[key]!;
306 | if (ratchet.acknowledged) {
307 | // The ratchet is acknowledged
308 | _log.finest(
309 | 'Checking whether to heartbeat to ${key.jid}, ratchet.nr (${ratchet.nr}) >= 53: ${ratchet.nr >= 53}, created: $created, replaced: $replaced',
310 | );
311 | if (ratchet.nr >= 53 || created || replaced) {
312 | await sendEmptyOmemoMessageImpl(
313 | await _onOutgoingStanzaImpl(
314 | OmemoOutgoingStanza(
315 | [key.jid],
316 | null,
317 | ),
318 | ),
319 | key.jid,
320 | );
321 | }
322 | } else {
323 | // Ratchet is not acknowledged
324 | _log.finest('Sending acknowledgement heartbeat to ${key.jid}');
325 | await _ratchetAcknowledged(key.jid, key.deviceId);
326 | await sendEmptyOmemoMessageImpl(
327 | await _onOutgoingStanzaImpl(
328 | OmemoOutgoingStanza(
329 | [key.jid],
330 | null,
331 | ),
332 | ),
333 | key.jid,
334 | );
335 | }
336 | }
337 |
338 | ///
339 | Future onIncomingStanza(OmemoIncomingStanza stanza) async {
340 | return _ratchetQueue.synchronized(
341 | [stanza.bareSenderJid],
342 | () => _onIncomingStanzaImpl(stanza),
343 | );
344 | }
345 |
346 | Future _onIncomingStanzaImpl(
347 | OmemoIncomingStanza stanza,
348 | ) async {
349 | // Populate the cache
350 | await _cacheJidIfNeccessary(stanza.bareSenderJid);
351 |
352 | // Find the correct key for our device
353 | final deviceId = await getDeviceId();
354 | final key = stanza.keys.firstWhereOrNull((key) => key.rid == deviceId);
355 | if (key == null) {
356 | return DecryptionResult(
357 | null,
358 | null,
359 | const {},
360 | const {},
361 | NotEncryptedForDeviceError(),
362 | );
363 | }
364 |
365 | // Protobuf will happily parse this and return bogus data.
366 | if (key.value.isEmpty) {
367 | return DecryptionResult(
368 | null,
369 | null,
370 | const {},
371 | const {},
372 | MalformedEncryptedKeyError(),
373 | );
374 | }
375 |
376 | // Check how we should process the message
377 | final ratchetKey =
378 | RatchetMapKey(stanza.bareSenderJid, stanza.senderDeviceId);
379 | var processAsKex = key.kex;
380 | final ratchetAlreadyExists = _ratchetMap.containsKey(ratchetKey);
381 | if (key.kex && ratchetAlreadyExists) {
382 | final ratchet = _ratchetMap[ratchetKey]!;
383 | final kexMessage = OMEMOKeyExchange.fromBuffer(key.data);
384 | final ratchetEk = await ratchet.kex.ek.getBytes();
385 | final sameEk = listsEqual(kexMessage.ek, ratchetEk);
386 |
387 | if (sameEk) {
388 | processAsKex = false;
389 | } else {
390 | processAsKex = true;
391 | }
392 | _log.finest('kexMessage.ek == ratchetEk: $sameEk');
393 | }
394 |
395 | // Process the message
396 | if (processAsKex) {
397 | _log.finest('Decoding message as OMEMOKeyExchange');
398 | final kexMessage = OMEMOKeyExchange.fromBuffer(key.data);
399 |
400 | // Find the correct SPK
401 | final device = await getDevice();
402 | OmemoKeyPair spk;
403 | if (kexMessage.spkId == device.spkId) {
404 | spk = device.spk;
405 | } else if (kexMessage.spkId == device.oldSpkId) {
406 | spk = device.oldSpk!;
407 | } else {
408 | return DecryptionResult(
409 | null,
410 | null,
411 | const {},
412 | const {},
413 | UnknownSignedPrekeyError(),
414 | );
415 | }
416 |
417 | // Build the new ratchet session
418 | final kexIk = OmemoPublicKey.fromBytes(
419 | kexMessage.ik,
420 | KeyPairType.ed25519,
421 | );
422 | final kexEk = OmemoPublicKey.fromBytes(
423 | kexMessage.ek,
424 | KeyPairType.x25519,
425 | );
426 | final kex = await x3dhFromInitialMessage(
427 | X3DHMessage(
428 | kexIk,
429 | kexEk,
430 | kexMessage.pkId,
431 | ),
432 | spk,
433 | device.opks[kexMessage.pkId]!,
434 | device.ik,
435 | );
436 | final ratchet = await OmemoDoubleRatchet.acceptNewSession(
437 | spk,
438 | kexMessage.spkId,
439 | kexIk,
440 | kexMessage.pkId,
441 | kexEk,
442 | kex.sk,
443 | kex.ad,
444 | );
445 |
446 | final keyAndHmac = await ratchet.ratchetDecrypt(
447 | kexMessage.message,
448 | );
449 | if (keyAndHmac.isType()) {
450 | final error = keyAndHmac.get();
451 | _log.warning('Failed to decrypt symmetric key: $error');
452 |
453 | return DecryptionResult(
454 | null,
455 | null,
456 | const {},
457 | const {},
458 | error,
459 | );
460 | }
461 |
462 | Result result;
463 | if (stanza.payload != null) {
464 | result = await _decryptAndVerifyHmac(
465 | stanza.payload?.fromBase64(),
466 | keyAndHmac.get>(),
467 | );
468 | if (result.isType()) {
469 | final error = result.get();
470 | _log.warning('Decrypting payload failed: $error');
471 |
472 | return DecryptionResult(
473 | null,
474 | null,
475 | const {},
476 | const {},
477 | error,
478 | );
479 | }
480 | } else {
481 | result = const Result(null);
482 | }
483 |
484 | // Notify the trust manager
485 | await _trustManager.onNewSession(
486 | stanza.bareSenderJid,
487 | stanza.senderDeviceId,
488 | );
489 |
490 | // If we received an empty OMEMO message, mark the ratchet as acknowledged
491 | if (result.get() == null) {
492 | if (!ratchet.acknowledged) {
493 | ratchet.acknowledged = true;
494 | }
495 | }
496 |
497 | // Commit the ratchet
498 | _ratchetMap[ratchetKey] = ratchet;
499 | _deviceList.appendOrCreate(stanza.bareSenderJid, stanza.senderDeviceId);
500 | await commitRatchets([
501 | OmemoRatchetData(
502 | stanza.bareSenderJid,
503 | stanza.senderDeviceId,
504 | ratchet,
505 | ),
506 | ]);
507 |
508 | // Replace the OPK if we're not doing a catchup.
509 | if (!stanza.isCatchup) {
510 | await _deviceLock.synchronized(() async {
511 | await _device.replaceOnetimePrekey(kexMessage.pkId);
512 | await commitDevice(_device);
513 |
514 | // Publish the device bundle
515 | unawaited(
516 | publishDeviceBundle(_device),
517 | );
518 | });
519 | }
520 |
521 | // Send the hearbeat, if we have to
522 | await _maybeSendEmptyMessage(
523 | ratchetKey,
524 | true,
525 | _ratchetMap.containsKey(ratchetKey),
526 | );
527 |
528 | final newlyCreatedDevice = {
529 | stanza.bareSenderJid: [stanza.senderDeviceId],
530 | };
531 | return DecryptionResult(
532 | result.get(),
533 | kexMessage.pkId,
534 | ratchetAlreadyExists ? {} : newlyCreatedDevice,
535 | ratchetAlreadyExists ? newlyCreatedDevice : {},
536 | null,
537 | );
538 | } else {
539 | // Check if we even have a ratchet
540 | if (!_ratchetMap.containsKey(ratchetKey)) {
541 | // TODO(Unknown): Check if we recently failed to build a session with the device
542 | // This causes omemo_dart to build a session with the device.
543 | if (!_deviceList[stanza.bareSenderJid]!
544 | .contains(stanza.senderDeviceId)) {
545 | _deviceList[stanza.bareSenderJid]!.add(stanza.senderDeviceId);
546 | }
547 | final emptyResult = await _sendOmemoHeartbeat(stanza.bareSenderJid);
548 |
549 | return DecryptionResult(
550 | null,
551 | null,
552 | emptyResult.newRatchets,
553 | emptyResult.replacedRatchets,
554 | NoSessionWithDeviceError(),
555 | );
556 | }
557 |
558 | _log.finest('Decoding message as OMEMOAuthenticatedMessage');
559 | final ratchet = _ratchetMap[ratchetKey]!.clone();
560 |
561 | // Correctly decode the message
562 | OMEMOAuthenticatedMessage authMessage;
563 | if (key.kex) {
564 | _log.finest(
565 | 'Extracting OMEMOAuthenticatedMessage from OMEMOKeyExchange',
566 | );
567 | authMessage = OMEMOKeyExchange.fromBuffer(key.data).message;
568 | } else {
569 | authMessage = OMEMOAuthenticatedMessage.fromBuffer(key.data);
570 | }
571 |
572 | final keyAndHmac = await ratchet.ratchetDecrypt(authMessage);
573 | if (keyAndHmac.isType()) {
574 | final error = keyAndHmac.get();
575 | _log.warning('Failed to decrypt symmetric key: $error');
576 | return DecryptionResult(
577 | null,
578 | null,
579 | const {},
580 | const {},
581 | error,
582 | );
583 | }
584 |
585 | Result result;
586 | if (stanza.payload != null) {
587 | result = await _decryptAndVerifyHmac(
588 | stanza.payload?.fromBase64(),
589 | keyAndHmac.get>(),
590 | );
591 | if (result.isType()) {
592 | final error = result.get();
593 | _log.warning('Failed to decrypt message: $error');
594 | return DecryptionResult(
595 | null,
596 | null,
597 | const {},
598 | const {},
599 | error,
600 | );
601 | }
602 | } else {
603 | result = const Result(null);
604 | }
605 |
606 | // If we received an empty OMEMO message, mark the ratchet as acknowledged
607 | if (result.get() == null) {
608 | if (!ratchet.acknowledged) {
609 | ratchet.acknowledged = true;
610 | }
611 | }
612 |
613 | // Message was successfully decrypted, so commit the ratchet
614 | _ratchetMap[ratchetKey] = ratchet;
615 | await commitRatchets([
616 | OmemoRatchetData(
617 | stanza.bareSenderJid,
618 | stanza.senderDeviceId,
619 | ratchet,
620 | ),
621 | ]);
622 |
623 | // Send a heartbeat, if required.
624 | await _maybeSendEmptyMessage(ratchetKey, false, false);
625 |
626 | return DecryptionResult(
627 | result.get(),
628 | null,
629 | const {},
630 | const {},
631 | null,
632 | );
633 | }
634 | }
635 |
636 | Future onOutgoingStanza(OmemoOutgoingStanza stanza) async {
637 | return _ratchetQueue.synchronized(
638 | stanza.recipientJids,
639 | () => _onOutgoingStanzaImpl(stanza),
640 | );
641 | }
642 |
643 | Future _onOutgoingStanzaImpl(
644 | OmemoOutgoingStanza stanza,
645 | ) async {
646 | // Populate the cache
647 | await _cacheJidsIfNeccessary(stanza.recipientJids);
648 |
649 | // Encrypt the payload, if we have any
650 | final List payloadKey;
651 | final List? ciphertext;
652 | if (stanza.payload != null) {
653 | // Generate the key and encrypt the plaintext
654 | final rawKey = generateRandomBytes(32);
655 | final keys = await deriveEncryptionKeys(rawKey, omemoPayloadInfoString);
656 | ciphertext = await aes256CbcEncrypt(
657 | utf8.encode(stanza.payload!),
658 | keys.encryptionKey,
659 | keys.iv,
660 | );
661 | final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
662 | payloadKey = concat([rawKey, hmac]);
663 | } else {
664 | payloadKey = List.filled(32, 0x0);
665 | ciphertext = null;
666 | }
667 |
668 | final successfulEncryptions = Map.fromEntries(
669 | stanza.recipientJids.map((jid) => MapEntry(jid, 0)),
670 | );
671 | final encryptionErrors = >{};
672 | final addedRatchetKeys = List.empty(growable: true);
673 | final newRatchets = >{};
674 | final replacedRatchets = >{};
675 | final kex = {};
676 | for (final jid in stanza.recipientJids) {
677 | final newBundles = await _fetchNewOmemoBundles(jid);
678 | if (newBundles.isEmpty) {
679 | continue;
680 | }
681 |
682 | for (final bundle in newBundles) {
683 | _log.finest('Building new ratchet $jid:${bundle.id}');
684 | final ratchetKey = RatchetMapKey(jid, bundle.id);
685 | final ownDevice = await getDevice();
686 | final kexResultRaw = await x3dhFromBundle(
687 | bundle,
688 | ownDevice.ik,
689 | );
690 | // TODO(Unknown): Track the failure and do not attempt to encrypt to this device
691 | // on every send.
692 | if (kexResultRaw.isType()) {
693 | encryptionErrors.appendOrCreate(
694 | jid,
695 | EncryptToJidError(
696 | bundle.id,
697 | kexResultRaw.get(),
698 | ),
699 | );
700 | continue;
701 | }
702 |
703 | final kexResult = kexResultRaw.get();
704 | final newRatchet = await OmemoDoubleRatchet.initiateNewSession(
705 | bundle.spk,
706 | bundle.spkId,
707 | bundle.ik,
708 | ownDevice.ik.pk,
709 | kexResult.ek.pk,
710 | kexResult.sk,
711 | kexResult.ad,
712 | kexResult.opkId,
713 | );
714 |
715 | // Track the ratchet
716 | if (_ratchetMap.containsKey(ratchetKey)) {
717 | replacedRatchets.appendOrCreate(ratchetKey.jid, ratchetKey.deviceId);
718 | } else {
719 | newRatchets.appendOrCreate(ratchetKey.jid, ratchetKey.deviceId);
720 | }
721 | _ratchetMap[ratchetKey] = newRatchet;
722 | addedRatchetKeys.add(ratchetKey);
723 |
724 | // Initiate trust
725 | await _trustManager.onNewSession(jid, bundle.id);
726 |
727 | // Track the KEX for later
728 | final ik = await ownDevice.ik.pk.getBytes();
729 | final ek = await kexResult.ek.pk.getBytes();
730 | kex[ratchetKey] = OMEMOKeyExchange()
731 | ..pkId = newRatchet.kex.pkId
732 | ..spkId = newRatchet.kex.spkId
733 | ..ik = ik
734 | ..ek = ek;
735 | }
736 | }
737 |
738 | // Commit the newly created ratchets, if we created any.
739 | if (addedRatchetKeys.isNotEmpty) {
740 | await commitRatchets(
741 | addedRatchetKeys.map((key) {
742 | return OmemoRatchetData(
743 | key.jid,
744 | key.deviceId,
745 | _ratchetMap[key]!,
746 | );
747 | }).toList(),
748 | );
749 | }
750 |
751 | // Encrypt the symmetric key for all devices.
752 | final encryptedKeys = >{};
753 | for (final jid in stanza.recipientJids) {
754 | // Check if we know about any devices to use
755 | final devices = _deviceList[jid];
756 | if (devices == null) {
757 | _log.info('No devices for $jid known. Skipping in encryption');
758 | encryptionErrors.appendOrCreate(
759 | jid,
760 | EncryptToJidError(
761 | null,
762 | NoKeyMaterialAvailableError(),
763 | ),
764 | );
765 | continue;
766 | }
767 |
768 | // Check if we have to subscribe to the device list
769 | if (!_subscriptionMap.containsKey(jid)) {
770 | unawaited(subscribeToDeviceListNodeImpl(jid));
771 | _subscriptionMap[jid] = true;
772 | }
773 |
774 | for (final device in devices) {
775 | // Check if we should encrypt for this device
776 | // NOTE: Empty OMEMO messages are allowed to bypass trust decisions
777 | if (stanza.payload != null) {
778 | // Only encrypt to devices that are trusted
779 | if (!(await _trustManager.isTrusted(jid, device))) continue;
780 |
781 | // Only encrypt to devices that are enabled
782 | if (!(await _trustManager.isEnabled(jid, device))) continue;
783 | }
784 |
785 | // Check if the ratchet exists
786 | final ratchetKey = RatchetMapKey(jid, device);
787 | if (!_ratchetMap.containsKey(ratchetKey)) {
788 | // NOTE: The earlier loop should have created a new ratchet
789 | _log.warning('No ratchet for $jid:$device found.');
790 | encryptionErrors.appendOrCreate(
791 | jid,
792 | EncryptToJidError(
793 | device,
794 | NoSessionWithDeviceError(),
795 | ),
796 | );
797 | continue;
798 | }
799 |
800 | // Encrypt
801 | final ratchet = _ratchetMap[ratchetKey]!;
802 | final authMessage = await ratchet.ratchetEncrypt(payloadKey);
803 |
804 | // Package
805 | if (kex.containsKey(ratchetKey)) {
806 | final kexMessage = kex[ratchetKey]!..message = authMessage;
807 | encryptedKeys.appendOrCreate(
808 | jid,
809 | EncryptedKey(
810 | device,
811 | base64Encode(kexMessage.writeToBuffer()),
812 | true,
813 | ),
814 | );
815 | successfulEncryptions[jid] = successfulEncryptions[jid]! + 1;
816 | } else if (!ratchet.acknowledged) {
817 | // The ratchet as not yet been acked.
818 | // Keep sending the old KEX
819 | _log.finest('Using old KEX data for OMEMOKeyExchange');
820 | final kexMessage = OMEMOKeyExchange()
821 | ..pkId = ratchet.kex.pkId
822 | ..spkId = ratchet.kex.spkId
823 | ..ik = await ratchet.kex.ik.getBytes()
824 | ..ek = await ratchet.kex.ek.getBytes()
825 | ..message = authMessage;
826 |
827 | encryptedKeys.appendOrCreate(
828 | jid,
829 | EncryptedKey(
830 | device,
831 | base64Encode(kexMessage.writeToBuffer()),
832 | true,
833 | ),
834 | );
835 | successfulEncryptions[jid] = successfulEncryptions[jid]! + 1;
836 | } else {
837 | // The ratchet exists and is acked
838 | encryptedKeys.appendOrCreate(
839 | jid,
840 | EncryptedKey(
841 | device,
842 | base64Encode(authMessage.writeToBuffer()),
843 | false,
844 | ),
845 | );
846 | successfulEncryptions[jid] = successfulEncryptions[jid]! + 1;
847 | }
848 | }
849 | }
850 |
851 | return EncryptionResult(
852 | ciphertext,
853 | encryptedKeys,
854 | encryptionErrors,
855 | newRatchets,
856 | replacedRatchets,
857 | successfulEncryptions.values.every((n) => n > 0),
858 | );
859 | }
860 |
861 | /// Sends an empty OMEMO message (heartbeat) to [jid].
862 | Future sendOmemoHeartbeat(String jid) async {
863 | await _ratchetQueue.synchronized(
864 | [jid],
865 | () => _sendOmemoHeartbeat(jid),
866 | );
867 | }
868 |
869 | /// Like [sendOmemoHeartbeat], but does not acquire the lock for [jid].
870 | Future _sendOmemoHeartbeat(String jid) async {
871 | final result = await _onOutgoingStanzaImpl(
872 | OmemoOutgoingStanza(
873 | [jid],
874 | null,
875 | ),
876 | );
877 | await sendEmptyOmemoMessageImpl(result, jid);
878 | return result;
879 | }
880 |
881 | /// Removes all ratchets associated with [jid].
882 | Future removeAllRatchets(String jid) async {
883 | await _ratchetQueue.synchronized(
884 | [jid],
885 | () async {
886 | // Remove the ratchet and commit
887 | final keys = (_deviceList[jid] ?? [])
888 | .map((device) => RatchetMapKey(jid, device));
889 | for (final key in keys) {
890 | _ratchetMap.remove(key);
891 | }
892 | await removeRatchets(keys.toList());
893 |
894 | // Tell the trust manager
895 | await _trustManager.removeTrustDecisionsForJid(jid);
896 |
897 | // Clear the device list
898 | await commitDeviceList(
899 | jid,
900 | [],
901 | );
902 | _deviceList.remove(jid);
903 | _deviceListRequested.remove(jid);
904 | },
905 | );
906 | }
907 |
908 | /// To be called when a update to the device list of [jid] is returned.
909 | /// [devices] is the list of device identifiers contained in the update.
910 | Future onDeviceListUpdate(String jid, List devices) async {
911 | await _ratchetQueue.synchronized(
912 | [jid],
913 | () async {
914 | // Update our state
915 | _deviceList[jid] = devices;
916 | _deviceListRequested[jid] = true;
917 |
918 | // Commit the device list
919 | await commitDeviceList(jid, devices);
920 | },
921 | );
922 | }
923 |
924 | /// To be called when a new connection is made, i.e. when the previous stream could
925 | /// previous stream could not be resumed using XEP-0198.
926 | Future onNewConnection() async {
927 | _deviceListRequested.clear();
928 | _subscriptionMap.clear();
929 | }
930 |
931 | // Mark the ratchet [jid]:[device] as acknowledged.
932 | Future ratchetAcknowledged(String jid, int device) async {
933 | await _ratchetQueue.synchronized(
934 | [jid],
935 | () => _ratchetAcknowledged(jid, device),
936 | );
937 | }
938 |
939 | /// Like [ratchetAcknowledged], but does not acquire the lock for [jid].
940 | Future _ratchetAcknowledged(String jid, int device) async {
941 | final ratchetKey = RatchetMapKey(jid, device);
942 | if (!_ratchetMap.containsKey(ratchetKey)) {
943 | _log.warning(
944 | 'Cannot mark $jid:$device as acknowledged as the ratchet does not exist',
945 | );
946 | } else {
947 | // Commit
948 | final ratchet = _ratchetMap[ratchetKey]!..acknowledged = true;
949 | await commitRatchets([
950 | OmemoRatchetData(
951 | jid,
952 | device,
953 | ratchet,
954 | ),
955 | ]);
956 | }
957 | }
958 |
959 | /// If ratchets with [jid] exists, returns a list of fingerprints for each
960 | /// ratchet.
961 | ///
962 | /// If not ratchets exists, returns null.
963 | Future?> getFingerprintsForJid(String jid) async {
964 | return _ratchetQueue.synchronized(
965 | [jid],
966 | () => _getFingerprintsForJidImpl(jid),
967 | );
968 | }
969 |
970 | /// Same as [getFingerprintsForJid], but without acquiring the lock for [jid].
971 | Future?> _getFingerprintsForJidImpl(
972 | String jid,
973 | ) async {
974 | // Check if we know of the JID.
975 | if (!_deviceList.containsKey(jid)) {
976 | return null;
977 | }
978 |
979 | final devices = _deviceList[jid]!;
980 | final fingerprints = List.empty(growable: true);
981 | for (final device in devices) {
982 | final ratchet = _ratchetMap[RatchetMapKey(jid, device)];
983 | if (ratchet == null) {
984 | _log.warning('getFingerprintsForJid: Ratchet $jid:$device not found.');
985 | continue;
986 | }
987 |
988 | fingerprints.add(
989 | DeviceFingerprint(
990 | device,
991 | await ratchet.fingerprint,
992 | ),
993 | );
994 | }
995 |
996 | return fingerprints;
997 | }
998 |
999 | /// Returns the device used for encryption and decryption.
1000 | Future getDevice() => _deviceLock.synchronized(() => _device);
1001 |
1002 | /// Returns the id of the device used for encryption and decryption.
1003 | Future getDeviceId() async => (await getDevice()).id;
1004 |
1005 | @visibleForTesting
1006 | OmemoDoubleRatchet? getRatchet(RatchetMapKey key) => _ratchetMap[key];
1007 |
1008 | /// Replaces the OPK with id [opkId] and commits the new device to storage. This
1009 | /// function should not be called. It's only useful for rotating OPKs after message
1010 | /// catch-up, because in that case the OPKs are not rotated automatically.
1011 | Future replaceOnetimePrekey(int opkId) async {
1012 | await _deviceLock.synchronized(() async {
1013 | // Replace OPK
1014 | await _device.replaceOnetimePrekey(opkId);
1015 |
1016 | // Commit the device
1017 | await commitDevice(_device);
1018 |
1019 | // Publish
1020 | unawaited(
1021 | publishDeviceBundle(_device),
1022 | );
1023 | });
1024 | }
1025 |
1026 | /// Replaces the SPK of our device and commits it to storage.
1027 | Future replaceSignedPrekey() async {
1028 | await _deviceLock.synchronized(() async {
1029 | // Replace SPK
1030 | await _device.replaceSignedPrekey();
1031 |
1032 | // Commit the device
1033 | await commitDevice(_device);
1034 | });
1035 | }
1036 |
1037 | /// Generates a completely new device to use.
1038 | Future regenerateDevice() async {
1039 | // Generate the new device
1040 | final oldDevice = await getDevice();
1041 | final newDevice = await OmemoDevice.generateNewDevice(
1042 | oldDevice.jid,
1043 | opkAmount: oldDevice.opks.length,
1044 | );
1045 |
1046 | await _deviceLock.synchronized(() async {
1047 | // Replace the old device
1048 | _device = newDevice;
1049 |
1050 | // Commit
1051 | await commitDevice(newDevice);
1052 |
1053 | // Publish
1054 | unawaited(
1055 | publishDeviceBundle(newDevice),
1056 | );
1057 | });
1058 |
1059 | return newDevice;
1060 | }
1061 |
1062 | /// Acquire a lock for interacting with the trust manager for modifying the trust
1063 | /// state of [jid]. [callback] is called from within the critical section with the
1064 | /// trust manager as its parameter.
1065 | Future withTrustManager(
1066 | String jid,
1067 | Future Function(TrustManager) callback,
1068 | ) async {
1069 | await _ratchetQueue.synchronized(
1070 | [jid],
1071 | () => callback(_trustManager),
1072 | );
1073 | }
1074 | }
1075 |
--------------------------------------------------------------------------------
/lib/src/omemo/queue.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:collection';
3 |
4 | import 'package:meta/meta.dart';
5 | import 'package:synchronized/synchronized.dart';
6 |
7 | extension UtilAllMethodsList on List {
8 | void removeAll(List values) {
9 | for (final value in values) {
10 | remove(value);
11 | }
12 | }
13 |
14 | bool containsAll(List values) {
15 | for (final value in values) {
16 | if (!contains(value)) {
17 | return false;
18 | }
19 | }
20 |
21 | return true;
22 | }
23 | }
24 |
25 | class _RatchetAccessQueueEntry {
26 | _RatchetAccessQueueEntry(
27 | this.jids,
28 | this.completer,
29 | );
30 |
31 | final List jids;
32 | final Completer completer;
33 | }
34 |
35 | class RatchetAccessQueue {
36 | final Queue<_RatchetAccessQueueEntry> _queue = Queue();
37 |
38 | @visibleForTesting
39 | final List runningOperations = List.empty(growable: true);
40 |
41 | final Lock lock = Lock();
42 |
43 | bool canBypass(List jids) {
44 | for (final jid in jids) {
45 | if (runningOperations.contains(jid)) {
46 | return false;
47 | }
48 | }
49 |
50 | return true;
51 | }
52 |
53 | Future enterCriticalSection(List jids) async {
54 | final completer = await lock.synchronized?>(() {
55 | if (canBypass(jids)) {
56 | runningOperations.addAll(jids);
57 | return null;
58 | }
59 |
60 | final completer = Completer();
61 | _queue.add(
62 | _RatchetAccessQueueEntry(
63 | jids,
64 | completer,
65 | ),
66 | );
67 |
68 | return completer;
69 | });
70 |
71 | await completer?.future;
72 | }
73 |
74 | Future leaveCriticalSection(List jids) async {
75 | await lock.synchronized(() {
76 | runningOperations.removeAll(jids);
77 |
78 | while (_queue.isNotEmpty) {
79 | if (canBypass(_queue.first.jids)) {
80 | final head = _queue.removeFirst();
81 | runningOperations.addAll(head.jids);
82 | head.completer.complete();
83 | } else {
84 | break;
85 | }
86 | }
87 | });
88 | }
89 |
90 | Future synchronized(
91 | List jids,
92 | Future Function() function,
93 | ) async {
94 | await enterCriticalSection(jids);
95 | final result = await function();
96 | await leaveCriticalSection(jids);
97 |
98 | return result;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib/src/omemo/ratchet_data.dart:
--------------------------------------------------------------------------------
1 | import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
2 |
3 | class OmemoRatchetData {
4 | const OmemoRatchetData(
5 | this.jid,
6 | this.id,
7 | this.ratchet,
8 | );
9 |
10 | /// The JID we have the ratchet with.
11 | final String jid;
12 |
13 | /// The device id we have the ratchet with.
14 | final int id;
15 |
16 | /// The actual double ratchet to commit.
17 | final OmemoDoubleRatchet ratchet;
18 | }
19 |
--------------------------------------------------------------------------------
/lib/src/omemo/ratchet_map_key.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 |
3 | @immutable
4 | class RatchetMapKey {
5 | const RatchetMapKey(this.jid, this.deviceId);
6 |
7 | factory RatchetMapKey.fromJsonKey(String key) {
8 | final parts = key.split(':');
9 | final deviceId = int.parse(parts.first);
10 |
11 | return RatchetMapKey(
12 | parts.sublist(1).join(':'),
13 | deviceId,
14 | );
15 | }
16 |
17 | final String jid;
18 | final int deviceId;
19 |
20 | String toJsonKey() {
21 | return '$deviceId:$jid';
22 | }
23 |
24 | @override
25 | bool operator ==(Object other) {
26 | return other is RatchetMapKey &&
27 | jid == other.jid &&
28 | deviceId == other.deviceId;
29 | }
30 |
31 | @override
32 | int get hashCode => jid.hashCode ^ deviceId.hashCode;
33 | }
34 |
--------------------------------------------------------------------------------
/lib/src/omemo/stanza.dart:
--------------------------------------------------------------------------------
1 | import 'package:omemo_dart/src/omemo/encrypted_key.dart';
2 |
3 | /// Describes a stanza that was received by the underlying XMPP library.
4 | class OmemoIncomingStanza {
5 | const OmemoIncomingStanza(
6 | this.bareSenderJid,
7 | this.senderDeviceId,
8 | this.keys,
9 | this.payload,
10 | this.isCatchup,
11 | );
12 |
13 | /// The bare JID of the sender of the stanza.
14 | final String bareSenderJid;
15 |
16 | /// The device ID of the sender.
17 | final int senderDeviceId;
18 |
19 | /// The included encrypted keys for our own JID
20 | final List keys;
21 |
22 | /// The string payload included in the element.
23 | final String? payload;
24 |
25 | /// Flag indicating whether the message was received due to a catchup.
26 | final bool isCatchup;
27 | }
28 |
29 | /// Describes a stanza that is to be sent out
30 | class OmemoOutgoingStanza {
31 | const OmemoOutgoingStanza(
32 | this.recipientJids,
33 | this.payload,
34 | );
35 |
36 | /// The JIDs the stanza will be sent to.
37 | final List recipientJids;
38 |
39 | /// The serialised XML data that should be encrypted.
40 | final String? payload;
41 | }
42 |
--------------------------------------------------------------------------------
/lib/src/protobuf/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PapaTutuWawa/omemo_dart/124c997fa3f0792fa50ff66b80f43c3b71382f89/lib/src/protobuf/.gitkeep
--------------------------------------------------------------------------------
/lib/src/protobuf/schema.pb.dart:
--------------------------------------------------------------------------------
1 | ///
2 | // Generated code. Do not modify.
3 | // source: schema.proto
4 | //
5 | // @dart = 2.12
6 | // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
7 |
8 | import 'dart:core' as $core;
9 |
10 | import 'package:protobuf/protobuf.dart' as $pb;
11 |
12 | class OMEMOMessage extends $pb.GeneratedMessage {
13 | static final $pb.BuilderInfo _i = $pb.BuilderInfo(
14 | const $core.bool.fromEnvironment('protobuf.omit_message_names')
15 | ? ''
16 | : 'OMEMOMessage',
17 | createEmptyInstance: create)
18 | ..a<$core.int>(
19 | 1,
20 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
21 | ? ''
22 | : 'n',
23 | $pb.PbFieldType.QU3)
24 | ..a<$core.int>(
25 | 2,
26 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
27 | ? ''
28 | : 'pn',
29 | $pb.PbFieldType.QU3)
30 | ..a<$core.List<$core.int>>(
31 | 3,
32 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
33 | ? ''
34 | : 'dhPub',
35 | $pb.PbFieldType.QY)
36 | ..a<$core.List<$core.int>>(
37 | 4,
38 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
39 | ? ''
40 | : 'ciphertext',
41 | $pb.PbFieldType.OY);
42 |
43 | OMEMOMessage._() : super();
44 | factory OMEMOMessage({
45 | $core.int? n,
46 | $core.int? pn,
47 | $core.List<$core.int>? dhPub,
48 | $core.List<$core.int>? ciphertext,
49 | }) {
50 | final _result = create();
51 | if (n != null) {
52 | _result.n = n;
53 | }
54 | if (pn != null) {
55 | _result.pn = pn;
56 | }
57 | if (dhPub != null) {
58 | _result.dhPub = dhPub;
59 | }
60 | if (ciphertext != null) {
61 | _result.ciphertext = ciphertext;
62 | }
63 | return _result;
64 | }
65 | factory OMEMOMessage.fromBuffer($core.List<$core.int> i,
66 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
67 | create()..mergeFromBuffer(i, r);
68 | factory OMEMOMessage.fromJson($core.String i,
69 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
70 | create()..mergeFromJson(i, r);
71 | @$core.Deprecated('Using this can add significant overhead to your binary. '
72 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
73 | 'Will be removed in next major version')
74 | OMEMOMessage clone() => OMEMOMessage()..mergeFromMessage(this);
75 | @$core.Deprecated('Using this can add significant overhead to your binary. '
76 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
77 | 'Will be removed in next major version')
78 | OMEMOMessage copyWith(void Function(OMEMOMessage) updates) =>
79 | super.copyWith((message) => updates(message as OMEMOMessage))
80 | as OMEMOMessage; // ignore: deprecated_member_use
81 | $pb.BuilderInfo get info_ => _i;
82 | @$core.pragma('dart2js:noInline')
83 | static OMEMOMessage create() => OMEMOMessage._();
84 | OMEMOMessage createEmptyInstance() => create();
85 | static $pb.PbList createRepeated() =>
86 | $pb.PbList();
87 | @$core.pragma('dart2js:noInline')
88 | static OMEMOMessage getDefault() => _defaultInstance ??=
89 | $pb.GeneratedMessage.$_defaultFor(create);
90 | static OMEMOMessage? _defaultInstance;
91 |
92 | @$pb.TagNumber(1)
93 | $core.int get n => $_getIZ(0);
94 | @$pb.TagNumber(1)
95 | set n($core.int v) {
96 | $_setUnsignedInt32(0, v);
97 | }
98 |
99 | @$pb.TagNumber(1)
100 | $core.bool hasN() => $_has(0);
101 | @$pb.TagNumber(1)
102 | void clearN() => clearField(1);
103 |
104 | @$pb.TagNumber(2)
105 | $core.int get pn => $_getIZ(1);
106 | @$pb.TagNumber(2)
107 | set pn($core.int v) {
108 | $_setUnsignedInt32(1, v);
109 | }
110 |
111 | @$pb.TagNumber(2)
112 | $core.bool hasPn() => $_has(1);
113 | @$pb.TagNumber(2)
114 | void clearPn() => clearField(2);
115 |
116 | @$pb.TagNumber(3)
117 | $core.List<$core.int> get dhPub => $_getN(2);
118 | @$pb.TagNumber(3)
119 | set dhPub($core.List<$core.int> v) {
120 | $_setBytes(2, v);
121 | }
122 |
123 | @$pb.TagNumber(3)
124 | $core.bool hasDhPub() => $_has(2);
125 | @$pb.TagNumber(3)
126 | void clearDhPub() => clearField(3);
127 |
128 | @$pb.TagNumber(4)
129 | $core.List<$core.int> get ciphertext => $_getN(3);
130 | @$pb.TagNumber(4)
131 | set ciphertext($core.List<$core.int> v) {
132 | $_setBytes(3, v);
133 | }
134 |
135 | @$pb.TagNumber(4)
136 | $core.bool hasCiphertext() => $_has(3);
137 | @$pb.TagNumber(4)
138 | void clearCiphertext() => clearField(4);
139 | }
140 |
141 | class OMEMOAuthenticatedMessage extends $pb.GeneratedMessage {
142 | static final $pb.BuilderInfo _i = $pb.BuilderInfo(
143 | const $core.bool.fromEnvironment('protobuf.omit_message_names')
144 | ? ''
145 | : 'OMEMOAuthenticatedMessage',
146 | createEmptyInstance: create)
147 | ..a<$core.List<$core.int>>(
148 | 1,
149 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
150 | ? ''
151 | : 'mac',
152 | $pb.PbFieldType.QY)
153 | ..a<$core.List<$core.int>>(
154 | 2,
155 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
156 | ? ''
157 | : 'message',
158 | $pb.PbFieldType.QY);
159 |
160 | OMEMOAuthenticatedMessage._() : super();
161 | factory OMEMOAuthenticatedMessage({
162 | $core.List<$core.int>? mac,
163 | $core.List<$core.int>? message,
164 | }) {
165 | final _result = create();
166 | if (mac != null) {
167 | _result.mac = mac;
168 | }
169 | if (message != null) {
170 | _result.message = message;
171 | }
172 | return _result;
173 | }
174 | factory OMEMOAuthenticatedMessage.fromBuffer($core.List<$core.int> i,
175 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
176 | create()..mergeFromBuffer(i, r);
177 | factory OMEMOAuthenticatedMessage.fromJson($core.String i,
178 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
179 | create()..mergeFromJson(i, r);
180 | @$core.Deprecated('Using this can add significant overhead to your binary. '
181 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
182 | 'Will be removed in next major version')
183 | OMEMOAuthenticatedMessage clone() =>
184 | OMEMOAuthenticatedMessage()..mergeFromMessage(this);
185 | @$core.Deprecated('Using this can add significant overhead to your binary. '
186 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
187 | 'Will be removed in next major version')
188 | OMEMOAuthenticatedMessage copyWith(
189 | void Function(OMEMOAuthenticatedMessage) updates) =>
190 | super.copyWith((message) => updates(message as OMEMOAuthenticatedMessage))
191 | as OMEMOAuthenticatedMessage; // ignore: deprecated_member_use
192 | $pb.BuilderInfo get info_ => _i;
193 | @$core.pragma('dart2js:noInline')
194 | static OMEMOAuthenticatedMessage create() => OMEMOAuthenticatedMessage._();
195 | OMEMOAuthenticatedMessage createEmptyInstance() => create();
196 | static $pb.PbList createRepeated() =>
197 | $pb.PbList();
198 | @$core.pragma('dart2js:noInline')
199 | static OMEMOAuthenticatedMessage getDefault() => _defaultInstance ??=
200 | $pb.GeneratedMessage.$_defaultFor(create);
201 | static OMEMOAuthenticatedMessage? _defaultInstance;
202 |
203 | @$pb.TagNumber(1)
204 | $core.List<$core.int> get mac => $_getN(0);
205 | @$pb.TagNumber(1)
206 | set mac($core.List<$core.int> v) {
207 | $_setBytes(0, v);
208 | }
209 |
210 | @$pb.TagNumber(1)
211 | $core.bool hasMac() => $_has(0);
212 | @$pb.TagNumber(1)
213 | void clearMac() => clearField(1);
214 |
215 | @$pb.TagNumber(2)
216 | $core.List<$core.int> get message => $_getN(1);
217 | @$pb.TagNumber(2)
218 | set message($core.List<$core.int> v) {
219 | $_setBytes(1, v);
220 | }
221 |
222 | @$pb.TagNumber(2)
223 | $core.bool hasMessage() => $_has(1);
224 | @$pb.TagNumber(2)
225 | void clearMessage() => clearField(2);
226 | }
227 |
228 | class OMEMOKeyExchange extends $pb.GeneratedMessage {
229 | static final $pb.BuilderInfo _i = $pb.BuilderInfo(
230 | const $core.bool.fromEnvironment('protobuf.omit_message_names')
231 | ? ''
232 | : 'OMEMOKeyExchange',
233 | createEmptyInstance: create)
234 | ..a<$core.int>(
235 | 1,
236 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
237 | ? ''
238 | : 'pkId',
239 | $pb.PbFieldType.QU3)
240 | ..a<$core.int>(
241 | 2,
242 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
243 | ? ''
244 | : 'spkId',
245 | $pb.PbFieldType.QU3)
246 | ..a<$core.List<$core.int>>(
247 | 3,
248 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
249 | ? ''
250 | : 'ik',
251 | $pb.PbFieldType.QY)
252 | ..a<$core.List<$core.int>>(
253 | 4,
254 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
255 | ? ''
256 | : 'ek',
257 | $pb.PbFieldType.QY)
258 | ..aQM(
259 | 5,
260 | const $core.bool.fromEnvironment('protobuf.omit_field_names')
261 | ? ''
262 | : 'message',
263 | subBuilder: OMEMOAuthenticatedMessage.create);
264 |
265 | OMEMOKeyExchange._() : super();
266 | factory OMEMOKeyExchange({
267 | $core.int? pkId,
268 | $core.int? spkId,
269 | $core.List<$core.int>? ik,
270 | $core.List<$core.int>? ek,
271 | OMEMOAuthenticatedMessage? message,
272 | }) {
273 | final _result = create();
274 | if (pkId != null) {
275 | _result.pkId = pkId;
276 | }
277 | if (spkId != null) {
278 | _result.spkId = spkId;
279 | }
280 | if (ik != null) {
281 | _result.ik = ik;
282 | }
283 | if (ek != null) {
284 | _result.ek = ek;
285 | }
286 | if (message != null) {
287 | _result.message = message;
288 | }
289 | return _result;
290 | }
291 | factory OMEMOKeyExchange.fromBuffer($core.List<$core.int> i,
292 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
293 | create()..mergeFromBuffer(i, r);
294 | factory OMEMOKeyExchange.fromJson($core.String i,
295 | [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) =>
296 | create()..mergeFromJson(i, r);
297 | @$core.Deprecated('Using this can add significant overhead to your binary. '
298 | 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. '
299 | 'Will be removed in next major version')
300 | OMEMOKeyExchange clone() => OMEMOKeyExchange()..mergeFromMessage(this);
301 | @$core.Deprecated('Using this can add significant overhead to your binary. '
302 | 'Use [GeneratedMessageGenericExtensions.rebuild] instead. '
303 | 'Will be removed in next major version')
304 | OMEMOKeyExchange copyWith(void Function(OMEMOKeyExchange) updates) =>
305 | super.copyWith((message) => updates(message as OMEMOKeyExchange))
306 | as OMEMOKeyExchange; // ignore: deprecated_member_use
307 | $pb.BuilderInfo get info_ => _i;
308 | @$core.pragma('dart2js:noInline')
309 | static OMEMOKeyExchange create() => OMEMOKeyExchange._();
310 | OMEMOKeyExchange createEmptyInstance() => create();
311 | static $pb.PbList createRepeated() =>
312 | $pb.PbList();
313 | @$core.pragma('dart2js:noInline')
314 | static OMEMOKeyExchange getDefault() => _defaultInstance ??=
315 | $pb.GeneratedMessage.$_defaultFor(create);
316 | static OMEMOKeyExchange? _defaultInstance;
317 |
318 | @$pb.TagNumber(1)
319 | $core.int get pkId => $_getIZ(0);
320 | @$pb.TagNumber(1)
321 | set pkId($core.int v) {
322 | $_setUnsignedInt32(0, v);
323 | }
324 |
325 | @$pb.TagNumber(1)
326 | $core.bool hasPkId() => $_has(0);
327 | @$pb.TagNumber(1)
328 | void clearPkId() => clearField(1);
329 |
330 | @$pb.TagNumber(2)
331 | $core.int get spkId => $_getIZ(1);
332 | @$pb.TagNumber(2)
333 | set spkId($core.int v) {
334 | $_setUnsignedInt32(1, v);
335 | }
336 |
337 | @$pb.TagNumber(2)
338 | $core.bool hasSpkId() => $_has(1);
339 | @$pb.TagNumber(2)
340 | void clearSpkId() => clearField(2);
341 |
342 | @$pb.TagNumber(3)
343 | $core.List<$core.int> get ik => $_getN(2);
344 | @$pb.TagNumber(3)
345 | set ik($core.List<$core.int> v) {
346 | $_setBytes(2, v);
347 | }
348 |
349 | @$pb.TagNumber(3)
350 | $core.bool hasIk() => $_has(2);
351 | @$pb.TagNumber(3)
352 | void clearIk() => clearField(3);
353 |
354 | @$pb.TagNumber(4)
355 | $core.List<$core.int> get ek => $_getN(3);
356 | @$pb.TagNumber(4)
357 | set ek($core.List<$core.int> v) {
358 | $_setBytes(3, v);
359 | }
360 |
361 | @$pb.TagNumber(4)
362 | $core.bool hasEk() => $_has(3);
363 | @$pb.TagNumber(4)
364 | void clearEk() => clearField(4);
365 |
366 | @$pb.TagNumber(5)
367 | OMEMOAuthenticatedMessage get message => $_getN(4);
368 | @$pb.TagNumber(5)
369 | set message(OMEMOAuthenticatedMessage v) {
370 | setField(5, v);
371 | }
372 |
373 | @$pb.TagNumber(5)
374 | $core.bool hasMessage() => $_has(4);
375 | @$pb.TagNumber(5)
376 | void clearMessage() => clearField(5);
377 | @$pb.TagNumber(5)
378 | OMEMOAuthenticatedMessage ensureMessage() => $_ensure(4);
379 | }
380 |
--------------------------------------------------------------------------------
/lib/src/protobuf/schema.pbenum.dart:
--------------------------------------------------------------------------------
1 | ///
2 | // Generated code. Do not modify.
3 | // source: schema.proto
4 | //
5 | // @dart = 2.12
6 | // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
7 |
--------------------------------------------------------------------------------
/lib/src/protobuf/schema.pbjson.dart:
--------------------------------------------------------------------------------
1 | ///
2 | // Generated code. Do not modify.
3 | // source: schema.proto
4 | //
5 | // @dart = 2.12
6 | // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
7 |
8 | import 'dart:core' as $core;
9 | import 'dart:convert' as $convert;
10 | import 'dart:typed_data' as $typed_data;
11 |
12 | @$core.Deprecated('Use oMEMOMessageDescriptor instead')
13 | const OMEMOMessage$json = {
14 | '1': 'OMEMOMessage',
15 | '2': [
16 | {'1': 'n', '3': 1, '4': 2, '5': 13, '10': 'n'},
17 | {'1': 'pn', '3': 2, '4': 2, '5': 13, '10': 'pn'},
18 | {'1': 'dh_pub', '3': 3, '4': 2, '5': 12, '10': 'dhPub'},
19 | {'1': 'ciphertext', '3': 4, '4': 1, '5': 12, '10': 'ciphertext'},
20 | ],
21 | };
22 |
23 | /// Descriptor for `OMEMOMessage`. Decode as a `google.protobuf.DescriptorProto`.
24 | final $typed_data.Uint8List oMEMOMessageDescriptor = $convert.base64Decode(
25 | 'CgxPTUVNT01lc3NhZ2USDAoBbhgBIAIoDVIBbhIOCgJwbhgCIAIoDVICcG4SFQoGZGhfcHViGAMgAigMUgVkaFB1YhIeCgpjaXBoZXJ0ZXh0GAQgASgMUgpjaXBoZXJ0ZXh0');
26 | @$core.Deprecated('Use oMEMOAuthenticatedMessageDescriptor instead')
27 | const OMEMOAuthenticatedMessage$json = {
28 | '1': 'OMEMOAuthenticatedMessage',
29 | '2': [
30 | {'1': 'mac', '3': 1, '4': 2, '5': 12, '10': 'mac'},
31 | {'1': 'message', '3': 2, '4': 2, '5': 12, '10': 'message'},
32 | ],
33 | };
34 |
35 | /// Descriptor for `OMEMOAuthenticatedMessage`. Decode as a `google.protobuf.DescriptorProto`.
36 | final $typed_data.Uint8List oMEMOAuthenticatedMessageDescriptor =
37 | $convert.base64Decode(
38 | 'ChlPTUVNT0F1dGhlbnRpY2F0ZWRNZXNzYWdlEhAKA21hYxgBIAIoDFIDbWFjEhgKB21lc3NhZ2UYAiACKAxSB21lc3NhZ2U=');
39 | @$core.Deprecated('Use oMEMOKeyExchangeDescriptor instead')
40 | const OMEMOKeyExchange$json = {
41 | '1': 'OMEMOKeyExchange',
42 | '2': [
43 | {'1': 'pk_id', '3': 1, '4': 2, '5': 13, '10': 'pkId'},
44 | {'1': 'spk_id', '3': 2, '4': 2, '5': 13, '10': 'spkId'},
45 | {'1': 'ik', '3': 3, '4': 2, '5': 12, '10': 'ik'},
46 | {'1': 'ek', '3': 4, '4': 2, '5': 12, '10': 'ek'},
47 | {
48 | '1': 'message',
49 | '3': 5,
50 | '4': 2,
51 | '5': 11,
52 | '6': '.OMEMOAuthenticatedMessage',
53 | '10': 'message'
54 | },
55 | ],
56 | };
57 |
58 | /// Descriptor for `OMEMOKeyExchange`. Decode as a `google.protobuf.DescriptorProto`.
59 | final $typed_data.Uint8List oMEMOKeyExchangeDescriptor = $convert.base64Decode(
60 | 'ChBPTUVNT0tleUV4Y2hhbmdlEhMKBXBrX2lkGAEgAigNUgRwa0lkEhUKBnNwa19pZBgCIAIoDVIFc3BrSWQSDgoCaWsYAyACKAxSAmlrEg4KAmVrGAQgAigMUgJlaxI0CgdtZXNzYWdlGAUgAigLMhouT01FTU9BdXRoZW50aWNhdGVkTWVzc2FnZVIHbWVzc2FnZQ==');
61 |
--------------------------------------------------------------------------------
/lib/src/protobuf/schema.pbserver.dart:
--------------------------------------------------------------------------------
1 | ///
2 | // Generated code. Do not modify.
3 | // source: schema.proto
4 | //
5 | // @dart = 2.12
6 | // ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
7 |
8 | export 'schema.pb.dart';
9 |
--------------------------------------------------------------------------------
/lib/src/trust/always.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 | import 'package:omemo_dart/src/trust/base.dart';
3 |
4 | /// Only use for testing!
5 | /// An implementation of TrustManager that always trusts every device and thus
6 | /// has no internal state.
7 | @visibleForTesting
8 | class AlwaysTrustingTrustManager extends TrustManager {
9 | @override
10 | Future isTrusted(String jid, int deviceId) async => true;
11 |
12 | @override
13 | Future onNewSession(String jid, int deviceId) async {}
14 |
15 | @override
16 | Future isEnabled(String jid, int deviceId) async => true;
17 |
18 | @override
19 | Future setEnabled(String jid, int deviceId, bool enabled) async {}
20 |
21 | @override
22 | Future removeTrustDecisionsForJid(String jid) async {}
23 |
24 | @override
25 | Future loadTrustData(String jid) async {}
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/trust/base.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 |
3 | /// The base class for managing trust in OMEMO sessions.
4 | // ignore: one_member_abstracts
5 | abstract class TrustManager {
6 | /// Return true when the device with id [deviceId] of Jid [jid] is trusted, i.e. if an
7 | /// encrypted message should be sent to this device. If not, return false.
8 | Future isTrusted(String jid, int deviceId);
9 |
10 | /// Called by the OmemoSessionManager when a new session has been built. Should set
11 | /// a default trust state to [jid]'s device with identifier [deviceId].
12 | @internal
13 | Future onNewSession(String jid, int deviceId);
14 |
15 | /// Return true if the device with id [deviceId] of Jid [jid] should be used for encryption.
16 | /// If not, return false.
17 | Future isEnabled(String jid, int deviceId);
18 |
19 | /// Mark the device with id [deviceId] of Jid [jid] as enabled if [enabled] is true or as disabled
20 | /// if [enabled] is false.
21 | Future setEnabled(String jid, int deviceId, bool enabled);
22 |
23 | /// Removes all trust decisions for [jid].
24 | @internal
25 | Future removeTrustDecisionsForJid(String jid);
26 |
27 | // ignore: comment_references
28 | /// Called from within the [OmemoManager].
29 | /// Loads the trust data for the JID [jid] from persistent storage
30 | /// into the internal cache, if applicable.
31 | @internal
32 | Future loadTrustData(String jid);
33 | }
34 |
--------------------------------------------------------------------------------
/lib/src/trust/btbv.dart:
--------------------------------------------------------------------------------
1 | import 'package:meta/meta.dart';
2 | import 'package:omemo_dart/src/helpers.dart';
3 | import 'package:omemo_dart/src/omemo/ratchet_map_key.dart';
4 | import 'package:omemo_dart/src/trust/base.dart';
5 |
6 | @immutable
7 | class BTBVTrustData {
8 | const BTBVTrustData(
9 | this.jid,
10 | this.device,
11 | this.state,
12 | this.enabled,
13 | this.trusted,
14 | );
15 |
16 | /// The JID in question.
17 | final String jid;
18 |
19 | /// The device (ratchet) in question.
20 | final int device;
21 |
22 | /// The trust state of the ratchet.
23 | final BTBVTrustState state;
24 |
25 | /// Flag indicating whether the ratchet is enabled (true) or not (false).
26 | final bool enabled;
27 |
28 | /// Flag indicating whether the ratchet is trusted. For loading and commiting a ratchet, this field
29 | /// contains an arbitrary value.
30 | /// When using [BlindTrustBeforeVerificationTrustManager.getDevicesTrust], this flag will be true if
31 | /// the ratchet is trusted and false if not.
32 | final bool trusted;
33 | }
34 |
35 | /// A callback for when a trust decision is to be commited to persistent storage.
36 | typedef BTBVTrustCommitCallback = Future Function(BTBVTrustData data);
37 |
38 | /// A stub-implementation of [BTBVTrustCommitCallback].
39 | Future btbvCommitStub(BTBVTrustData _) async {}
40 |
41 | /// A callback for when all trust decisions for a JID should be removed from persistent storage.
42 | typedef BTBVRemoveTrustForJidCallback = Future Function(String jid);
43 |
44 | /// A stub-implementation of [BTBVRemoveTrustForJidCallback].
45 | Future btbvRemoveTrustStub(String _) async {}
46 |
47 | /// A callback for when trust data should be loaded.
48 | typedef BTBVLoadDataCallback = Future> Function(String jid);
49 |
50 | /// A stub-implementation for [BTBVLoadDataCallback].
51 | Future> btbvLoadDataStub(String _) async => [];
52 |
53 | /// Every device is in either of those two trust states:
54 | /// - notTrusted: The device is absolutely not trusted
55 | /// - blindTrust: The fingerprint is not verified using OOB means
56 | /// - verified: The fingerprint has been verified using OOB means
57 | enum BTBVTrustState {
58 | notTrusted(1),
59 | blindTrust(2),
60 | verified(3);
61 |
62 | const BTBVTrustState(this.value);
63 |
64 | factory BTBVTrustState.fromInt(int value) {
65 | switch (value) {
66 | case 1:
67 | return BTBVTrustState.notTrusted;
68 | case 2:
69 | return BTBVTrustState.blindTrust;
70 | case 3:
71 | return BTBVTrustState.verified;
72 | // TODO(Unknown): Should we handle this better?
73 | default:
74 | return BTBVTrustState.notTrusted;
75 | }
76 | }
77 |
78 | /// The value backing the trust state.
79 | final int value;
80 | }
81 |
82 | /// A TrustManager that implements the idea of Blind Trust Before Verification.
83 | /// See https://gultsch.de/trust.html for more details.
84 | class BlindTrustBeforeVerificationTrustManager extends TrustManager {
85 | BlindTrustBeforeVerificationTrustManager({
86 | this.loadData = btbvLoadDataStub,
87 | this.commit = btbvCommitStub,
88 | this.removeTrust = btbvRemoveTrustStub,
89 | });
90 |
91 | /// The cache for mapping a RatchetMapKey to its trust state
92 | @visibleForTesting
93 | @protected
94 | final Map trustCache = {};
95 |
96 | /// The cache for mapping a RatchetMapKey to whether it is enabled or not
97 | @visibleForTesting
98 | @protected
99 | final Map enablementCache = {};
100 |
101 | /// Mapping of Jids to their device identifiers
102 | @visibleForTesting
103 | @protected
104 | final Map> devices = {};
105 |
106 | /// Callback for loading trust data.
107 | final BTBVLoadDataCallback loadData;
108 |
109 | /// Callback for commiting trust data to persistent storage.
110 | final BTBVTrustCommitCallback commit;
111 |
112 | /// Callback for removing trust data for a JID.
113 | final BTBVRemoveTrustForJidCallback removeTrust;
114 |
115 | /// Returns true if [jid] has at least one device that is verified. If not, returns false.
116 | /// Note that this function accesses devices and trustCache, which requires that the
117 | /// lock for those two maps (_lock) has been aquired before calling.
118 | bool _hasAtLeastOneVerifiedDevice(String jid) {
119 | if (!devices.containsKey(jid)) return false;
120 |
121 | return devices[jid]!.any((id) {
122 | return trustCache[RatchetMapKey(jid, id)]! == BTBVTrustState.verified;
123 | });
124 | }
125 |
126 | @override
127 | Future isTrusted(String jid, int deviceId) async {
128 | final trustCacheValue = trustCache[RatchetMapKey(jid, deviceId)];
129 | if (trustCacheValue == BTBVTrustState.notTrusted) {
130 | return false;
131 | } else if (trustCacheValue == BTBVTrustState.verified) {
132 | // The key is verified, so it's safe.
133 | return true;
134 | } else {
135 | if (_hasAtLeastOneVerifiedDevice(jid)) {
136 | // Do not trust if there is at least one device with full trust
137 | return false;
138 | } else {
139 | // We have not verified a key from [jid], so it is blind trust all the way.
140 | return true;
141 | }
142 | }
143 | }
144 |
145 | @override
146 | Future