async-matrix 1.1.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/async_matrix_e2ee/Cargo.toml +28 -0
- data/ext/async_matrix_e2ee/extconf.rb +8 -0
- data/ext/async_matrix_e2ee/src/lib.rs +328 -0
- data/lib/async/matrix/e2ee.rb +85 -0
- data/lib/async/matrix/version.rb +1 -1
- metadata +35 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a0c3eee197effa06e2abe173a328734d5c0e2db5c033c26a24030813d7ad69b
|
|
4
|
+
data.tar.gz: 597a3fdae49b1fdf17584bdb181067c27f3fc6e2877e56ec4607f60bff242c06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d596428929e71d223463ac9bd28f78941cd9ced8e6dde5ccb9450f9c5fd7e0e527ae2953c5592a6d3ffa93b7a5557f74935a58fddee6de687d466d7540c7b426
|
|
7
|
+
data.tar.gz: 87faa2828aba3103c88feb145a0b53bb1bbfb2f05adaa795e05f7020c6cc15aa1bc81b51a260e5485b986eb2d4196b18d82e954309cdbbae2b56a99a73c1bd12
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "async_matrix_e2ee"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
name = "async_matrix_e2ee"
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
# magnus 0.8 is required for Ruby 4.0 C-API compatibility (0.7 fails to compile
|
|
13
|
+
# against Ruby 4.0: rb_fiber_raise pointer mutability + RTypedData.typed_flag).
|
|
14
|
+
magnus = "0.8"
|
|
15
|
+
|
|
16
|
+
# vodozemac 0.10.0 (2026-04-13), default features (libolm-compat).
|
|
17
|
+
#
|
|
18
|
+
# - 0.10 is the release that fixes the all-zero / non-contributory DH issue from
|
|
19
|
+
# Soatok's review (the proposed patch was deprecated `since = "0.10.0"`), so we
|
|
20
|
+
# pin >= 0.10 rather than 0.9.
|
|
21
|
+
# - There is no `strict-signatures` feature in 0.10 (it existed pre-0.10; the
|
|
22
|
+
# related signature-verification hardening was resolved upstream).
|
|
23
|
+
# - There is no `fuzzing` Cargo feature: vodozemac's MAC/signature bypass is
|
|
24
|
+
# gated behind `#[cfg(fuzzing)]`, only set by `cargo fuzz`
|
|
25
|
+
# (RUSTFLAGS=--cfg fuzzing). A normal `cargo build`/`rake compile` never
|
|
26
|
+
# enables it. Never build production artifacts with `cargo fuzz`.
|
|
27
|
+
[dependencies.vodozemac]
|
|
28
|
+
version = "0.10"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "rb_sys/mkmf"
|
|
5
|
+
|
|
6
|
+
# Builds the Rust crate in this directory and produces async_matrix_e2ee.so.
|
|
7
|
+
# The crate's #[magnus::init] defines the classes under Async::Matrix::E2EE.
|
|
8
|
+
create_rust_makefile("async_matrix_e2ee")
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//! Ruby bindings for vodozemac (Matrix Olm/Megolm), via magnus.
|
|
2
|
+
//!
|
|
3
|
+
//! This is the Ruby analogue of matrix-nio/vodozemac-python (which uses PyO3).
|
|
4
|
+
//! It exposes vodozemac's Account / Session (Olm) and GroupSession /
|
|
5
|
+
//! InboundGroupSession (Megolm) under the `Async::Matrix::E2EE` module.
|
|
6
|
+
//!
|
|
7
|
+
//! The API is deliberately base64-string / integer centric rather than passing
|
|
8
|
+
//! wrapped key objects around: Matrix moves keys and ciphertext as base64
|
|
9
|
+
//! strings inside JSON, so a string API drops straight into the protocol layer
|
|
10
|
+
//! and keeps the magnus surface small.
|
|
11
|
+
|
|
12
|
+
use std::cell::RefCell;
|
|
13
|
+
use std::collections::HashMap;
|
|
14
|
+
|
|
15
|
+
use magnus::{function, method, prelude::*, Error, Ruby};
|
|
16
|
+
|
|
17
|
+
use vodozemac::{base64_decode, base64_encode, Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature};
|
|
18
|
+
use vodozemac::olm::{
|
|
19
|
+
Account as OlmAccount, AccountPickle, OlmMessage, PreKeyMessage, Session as OlmSession,
|
|
20
|
+
SessionConfig as OlmSessionConfig, SessionPickle,
|
|
21
|
+
};
|
|
22
|
+
use vodozemac::megolm::{
|
|
23
|
+
GroupSession as MegolmGroupSession, GroupSessionPickle, InboundGroupSession as MegolmInbound,
|
|
24
|
+
InboundGroupSessionPickle, MegolmMessage, SessionConfig as MegolmSessionConfig, SessionKey,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// --- helpers ---------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
fn rt_err<E: std::fmt::Display>(e: E) -> Error {
|
|
30
|
+
// Safe: every caller runs inside a Ruby method invocation (GVL held).
|
|
31
|
+
let ruby = Ruby::get().expect("rt_err called outside the Ruby VM");
|
|
32
|
+
Error::new(ruby.exception_runtime_error(), e.to_string())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn key32(label: &str, bytes: &[u8]) -> Result<[u8; 32], Error> {
|
|
36
|
+
bytes
|
|
37
|
+
.try_into()
|
|
38
|
+
.map_err(|_| rt_err(format!("{label} must be 32 bytes, got {}", bytes.len())))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Account ---------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
#[magnus::wrap(class = "Async::Matrix::E2EE::Account", free_immediately, size)]
|
|
44
|
+
struct Account(RefCell<OlmAccount>);
|
|
45
|
+
|
|
46
|
+
impl Account {
|
|
47
|
+
fn new() -> Self {
|
|
48
|
+
Account(RefCell::new(OlmAccount::new()))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
|
|
52
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
53
|
+
let p = AccountPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
|
|
54
|
+
Ok(Account(RefCell::new(OlmAccount::from_pickle(p))))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn pickle(&self, pickle_key: String) -> Result<String, Error> {
|
|
58
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
59
|
+
Ok(self.0.borrow().pickle().encrypt(&key))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn ed25519_key(&self) -> String {
|
|
63
|
+
self.0.borrow().ed25519_key().to_base64()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn curve25519_key(&self) -> String {
|
|
67
|
+
self.0.borrow().curve25519_key().to_base64()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn sign(&self, message: String) -> String {
|
|
71
|
+
self.0.borrow().sign(message.as_bytes()).to_base64()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn one_time_keys(&self) -> HashMap<String, String> {
|
|
75
|
+
self.0
|
|
76
|
+
.borrow()
|
|
77
|
+
.one_time_keys()
|
|
78
|
+
.into_iter()
|
|
79
|
+
.map(|(k, v)| (k.to_base64(), v.to_base64()))
|
|
80
|
+
.collect()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn max_number_of_one_time_keys(&self) -> usize {
|
|
84
|
+
self.0.borrow().max_number_of_one_time_keys()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn generate_one_time_keys(&self, count: usize) {
|
|
88
|
+
self.0.borrow_mut().generate_one_time_keys(count);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn fallback_key(&self) -> HashMap<String, String> {
|
|
92
|
+
self.0
|
|
93
|
+
.borrow()
|
|
94
|
+
.fallback_key()
|
|
95
|
+
.into_iter()
|
|
96
|
+
.map(|(k, v)| (k.to_base64(), v.to_base64()))
|
|
97
|
+
.collect()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn generate_fallback_key(&self) {
|
|
101
|
+
self.0.borrow_mut().generate_fallback_key();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn mark_keys_as_published(&self) {
|
|
105
|
+
self.0.borrow_mut().mark_keys_as_published();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Returns a Session for sending to a peer identified by their identity and
|
|
109
|
+
/// one-time keys (both base64).
|
|
110
|
+
fn create_outbound_session(
|
|
111
|
+
&self,
|
|
112
|
+
identity_key: String,
|
|
113
|
+
one_time_key: String,
|
|
114
|
+
) -> Result<Session, Error> {
|
|
115
|
+
let ik = Curve25519PublicKey::from_base64(&identity_key).map_err(rt_err)?;
|
|
116
|
+
let otk = Curve25519PublicKey::from_base64(&one_time_key).map_err(rt_err)?;
|
|
117
|
+
let session = self
|
|
118
|
+
.0
|
|
119
|
+
.borrow()
|
|
120
|
+
.create_outbound_session(OlmSessionConfig::version_1(), ik, otk)
|
|
121
|
+
.map_err(rt_err)?;
|
|
122
|
+
Ok(Session(RefCell::new(session)))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Establishes an inbound Session from a received pre-key message (base64
|
|
126
|
+
/// body, as carried in an m.room.encrypted olm `body`). Returns
|
|
127
|
+
/// `[session, plaintext]`.
|
|
128
|
+
fn create_inbound_session(
|
|
129
|
+
&self,
|
|
130
|
+
identity_key: String,
|
|
131
|
+
prekey_message_body: String,
|
|
132
|
+
) -> Result<(Session, String), Error> {
|
|
133
|
+
let ik = Curve25519PublicKey::from_base64(&identity_key).map_err(rt_err)?;
|
|
134
|
+
let prekey = PreKeyMessage::from_base64(&prekey_message_body).map_err(rt_err)?;
|
|
135
|
+
let result = self
|
|
136
|
+
.0
|
|
137
|
+
.borrow_mut()
|
|
138
|
+
.create_inbound_session(OlmSessionConfig::version_1(), ik, &prekey)
|
|
139
|
+
.map_err(rt_err)?;
|
|
140
|
+
let plaintext = String::from_utf8(result.plaintext).map_err(rt_err)?;
|
|
141
|
+
Ok((Session(RefCell::new(result.session)), plaintext))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Session (Olm 1:1) -----------------------------------------------------
|
|
146
|
+
|
|
147
|
+
#[magnus::wrap(class = "Async::Matrix::E2EE::Session", free_immediately, size)]
|
|
148
|
+
struct Session(RefCell<OlmSession>);
|
|
149
|
+
|
|
150
|
+
impl Session {
|
|
151
|
+
fn session_id(&self) -> String {
|
|
152
|
+
self.0.borrow().session_id()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn pickle(&self, pickle_key: String) -> Result<String, Error> {
|
|
156
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
157
|
+
Ok(self.0.borrow().pickle().encrypt(&key))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
|
|
161
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
162
|
+
let p = SessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
|
|
163
|
+
Ok(Session(RefCell::new(OlmSession::from_pickle(p))))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Encrypts plaintext; returns `[message_type, body_base64]` matching the
|
|
167
|
+
/// Matrix olm ciphertext shape.
|
|
168
|
+
fn encrypt(&self, plaintext: String) -> Result<(usize, String), Error> {
|
|
169
|
+
let message = self.0.borrow_mut().encrypt(plaintext.as_bytes()).map_err(rt_err)?;
|
|
170
|
+
let (mtype, ciphertext) = message.to_parts();
|
|
171
|
+
Ok((mtype, base64_encode(ciphertext)))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Decrypts an olm message given its type (0 = pre-key, 1 = normal) and
|
|
175
|
+
/// base64 body.
|
|
176
|
+
fn decrypt(&self, message_type: usize, body: String) -> Result<String, Error> {
|
|
177
|
+
let ciphertext = base64_decode(&body).map_err(rt_err)?;
|
|
178
|
+
let message = OlmMessage::from_parts(message_type, &ciphertext).map_err(rt_err)?;
|
|
179
|
+
let plaintext = self.0.borrow_mut().decrypt(&message).map_err(rt_err)?;
|
|
180
|
+
String::from_utf8(plaintext).map_err(rt_err)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- GroupSession (Megolm outbound) ---------------------------------------
|
|
185
|
+
|
|
186
|
+
#[magnus::wrap(class = "Async::Matrix::E2EE::GroupSession", free_immediately, size)]
|
|
187
|
+
struct GroupSession(RefCell<MegolmGroupSession>);
|
|
188
|
+
|
|
189
|
+
impl GroupSession {
|
|
190
|
+
fn new() -> Self {
|
|
191
|
+
GroupSession(RefCell::new(MegolmGroupSession::new(MegolmSessionConfig::version_1())))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn session_id(&self) -> String {
|
|
195
|
+
self.0.borrow().session_id()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fn message_index(&self) -> u32 {
|
|
199
|
+
self.0.borrow().message_index()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// The session key (base64) to share with recipients via m.room_key.
|
|
203
|
+
fn session_key(&self) -> String {
|
|
204
|
+
self.0.borrow().session_key().to_base64()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Encrypts plaintext, returning the megolm message as base64.
|
|
208
|
+
fn encrypt(&self, plaintext: String) -> String {
|
|
209
|
+
self.0.borrow_mut().encrypt(plaintext.as_bytes()).to_base64()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn pickle(&self, pickle_key: String) -> Result<String, Error> {
|
|
213
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
214
|
+
Ok(self.0.borrow().pickle().encrypt(&key))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
|
|
218
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
219
|
+
let p = GroupSessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
|
|
220
|
+
Ok(GroupSession(RefCell::new(MegolmGroupSession::from_pickle(p))))
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --- InboundGroupSession (Megolm inbound) ---------------------------------
|
|
225
|
+
|
|
226
|
+
#[magnus::wrap(class = "Async::Matrix::E2EE::InboundGroupSession", free_immediately, size)]
|
|
227
|
+
struct InboundGroupSession(RefCell<MegolmInbound>);
|
|
228
|
+
|
|
229
|
+
impl InboundGroupSession {
|
|
230
|
+
/// Build from a base64 session key received in an m.room_key event.
|
|
231
|
+
fn new(session_key: String) -> Result<Self, Error> {
|
|
232
|
+
let key = SessionKey::from_base64(&session_key).map_err(rt_err)?;
|
|
233
|
+
Ok(InboundGroupSession(RefCell::new(MegolmInbound::new(
|
|
234
|
+
&key,
|
|
235
|
+
MegolmSessionConfig::version_1(),
|
|
236
|
+
))))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn session_id(&self) -> String {
|
|
240
|
+
self.0.borrow().session_id()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn first_known_index(&self) -> u32 {
|
|
244
|
+
self.0.borrow().first_known_index()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Decrypts a base64 megolm message; returns `[plaintext, message_index]`.
|
|
248
|
+
fn decrypt(&self, message: String) -> Result<(String, u32), Error> {
|
|
249
|
+
let msg = MegolmMessage::from_base64(&message).map_err(rt_err)?;
|
|
250
|
+
let decrypted = self.0.borrow_mut().decrypt(&msg).map_err(rt_err)?;
|
|
251
|
+
let plaintext = String::from_utf8(decrypted.plaintext).map_err(rt_err)?;
|
|
252
|
+
Ok((plaintext, decrypted.message_index))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fn pickle(&self, pickle_key: String) -> Result<String, Error> {
|
|
256
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
257
|
+
Ok(self.0.borrow().pickle().encrypt(&key))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
|
|
261
|
+
let key = key32("pickle_key", pickle_key.as_bytes())?;
|
|
262
|
+
let p = InboundGroupSessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
|
|
263
|
+
Ok(InboundGroupSession(RefCell::new(MegolmInbound::from_pickle(p))))
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- module-level functions ------------------------------------------------
|
|
268
|
+
|
|
269
|
+
/// Verify an ed25519 signature. `key` and `signature` are base64; returns bool.
|
|
270
|
+
fn verify_signature(key: String, message: String, signature: String) -> Result<bool, Error> {
|
|
271
|
+
let public_key = Ed25519PublicKey::from_base64(&key).map_err(rt_err)?;
|
|
272
|
+
let sig = Ed25519Signature::from_base64(&signature).map_err(rt_err)?;
|
|
273
|
+
Ok(public_key.verify(message.as_bytes(), &sig).is_ok())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// --- init ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
#[magnus::init]
|
|
279
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
280
|
+
let namespace = ruby.define_module("Async")?.define_module("Matrix")?.define_module("E2EE")?;
|
|
281
|
+
|
|
282
|
+
let account = namespace.define_class("Account", ruby.class_object())?;
|
|
283
|
+
account.define_singleton_method("new", function!(Account::new, 0))?;
|
|
284
|
+
account.define_singleton_method("from_pickle", function!(Account::from_pickle, 2))?;
|
|
285
|
+
account.define_method("pickle", method!(Account::pickle, 1))?;
|
|
286
|
+
account.define_method("ed25519_key", method!(Account::ed25519_key, 0))?;
|
|
287
|
+
account.define_method("curve25519_key", method!(Account::curve25519_key, 0))?;
|
|
288
|
+
account.define_method("sign", method!(Account::sign, 1))?;
|
|
289
|
+
account.define_method("one_time_keys", method!(Account::one_time_keys, 0))?;
|
|
290
|
+
account.define_method(
|
|
291
|
+
"max_number_of_one_time_keys",
|
|
292
|
+
method!(Account::max_number_of_one_time_keys, 0),
|
|
293
|
+
)?;
|
|
294
|
+
account.define_method("generate_one_time_keys", method!(Account::generate_one_time_keys, 1))?;
|
|
295
|
+
account.define_method("fallback_key", method!(Account::fallback_key, 0))?;
|
|
296
|
+
account.define_method("generate_fallback_key", method!(Account::generate_fallback_key, 0))?;
|
|
297
|
+
account.define_method("mark_keys_as_published", method!(Account::mark_keys_as_published, 0))?;
|
|
298
|
+
account.define_method("create_outbound_session", method!(Account::create_outbound_session, 2))?;
|
|
299
|
+
account.define_method("create_inbound_session", method!(Account::create_inbound_session, 2))?;
|
|
300
|
+
|
|
301
|
+
let session = namespace.define_class("Session", ruby.class_object())?;
|
|
302
|
+
session.define_singleton_method("from_pickle", function!(Session::from_pickle, 2))?;
|
|
303
|
+
session.define_method("session_id", method!(Session::session_id, 0))?;
|
|
304
|
+
session.define_method("pickle", method!(Session::pickle, 1))?;
|
|
305
|
+
session.define_method("encrypt", method!(Session::encrypt, 1))?;
|
|
306
|
+
session.define_method("decrypt", method!(Session::decrypt, 2))?;
|
|
307
|
+
|
|
308
|
+
let group = namespace.define_class("GroupSession", ruby.class_object())?;
|
|
309
|
+
group.define_singleton_method("new", function!(GroupSession::new, 0))?;
|
|
310
|
+
group.define_singleton_method("from_pickle", function!(GroupSession::from_pickle, 2))?;
|
|
311
|
+
group.define_method("session_id", method!(GroupSession::session_id, 0))?;
|
|
312
|
+
group.define_method("message_index", method!(GroupSession::message_index, 0))?;
|
|
313
|
+
group.define_method("session_key", method!(GroupSession::session_key, 0))?;
|
|
314
|
+
group.define_method("encrypt", method!(GroupSession::encrypt, 1))?;
|
|
315
|
+
group.define_method("pickle", method!(GroupSession::pickle, 1))?;
|
|
316
|
+
|
|
317
|
+
let inbound = namespace.define_class("InboundGroupSession", ruby.class_object())?;
|
|
318
|
+
inbound.define_singleton_method("new", function!(InboundGroupSession::new, 1))?;
|
|
319
|
+
inbound.define_singleton_method("from_pickle", function!(InboundGroupSession::from_pickle, 2))?;
|
|
320
|
+
inbound.define_method("session_id", method!(InboundGroupSession::session_id, 0))?;
|
|
321
|
+
inbound.define_method("first_known_index", method!(InboundGroupSession::first_known_index, 0))?;
|
|
322
|
+
inbound.define_method("decrypt", method!(InboundGroupSession::decrypt, 1))?;
|
|
323
|
+
inbound.define_method("pickle", method!(InboundGroupSession::pickle, 1))?;
|
|
324
|
+
|
|
325
|
+
namespace.define_module_function("verify_signature", function!(verify_signature, 3))?;
|
|
326
|
+
|
|
327
|
+
Ok(())
|
|
328
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the Apache License, Version 2.0.
|
|
4
|
+
# Copyright, 2026, by General Intelligence Systems.
|
|
5
|
+
|
|
6
|
+
# Loads the native vodozemac binding (ext/async_matrix_e2ee) and namespaces the
|
|
7
|
+
# Olm/Megolm primitives under Async::Matrix::E2EE.
|
|
8
|
+
#
|
|
9
|
+
# The native classes (Account, Session, GroupSession, InboundGroupSession) and
|
|
10
|
+
# the module function verify_signature are defined by the Rust #[magnus::init].
|
|
11
|
+
# This file only locates and requires the compiled object.
|
|
12
|
+
#
|
|
13
|
+
# account = Async::Matrix::E2EE::Account.new
|
|
14
|
+
# account.curve25519_key # => base64 string
|
|
15
|
+
# group = Async::Matrix::E2EE::GroupSession.new
|
|
16
|
+
# msg = group.encrypt("hello") # => base64 megolm message
|
|
17
|
+
# inbound = Async::Matrix::E2EE::InboundGroupSession.new(group.session_key)
|
|
18
|
+
# inbound.decrypt(msg) # => ["hello", 0]
|
|
19
|
+
|
|
20
|
+
module Async
|
|
21
|
+
module Matrix
|
|
22
|
+
module E2EE
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rake-compiler installs the shared object alongside this file.
|
|
28
|
+
require_relative "async_matrix_e2ee"
|
|
29
|
+
|
|
30
|
+
test do
|
|
31
|
+
describe "Async::Matrix::E2EE" do
|
|
32
|
+
it "exposes account identity keys as base64" do
|
|
33
|
+
account = Async::Matrix::E2EE::Account.new
|
|
34
|
+
account.curve25519_key.should.be.kind_of String
|
|
35
|
+
account.ed25519_key.should.be.kind_of String
|
|
36
|
+
account.curve25519_key.length.should.be > 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "round-trips a megolm group message" do
|
|
40
|
+
group = Async::Matrix::E2EE::GroupSession.new
|
|
41
|
+
inbound = Async::Matrix::E2EE::InboundGroupSession.new(group.session_key)
|
|
42
|
+
|
|
43
|
+
message = group.encrypt("hello world")
|
|
44
|
+
plaintext, index = inbound.decrypt(message)
|
|
45
|
+
|
|
46
|
+
plaintext.should == "hello world"
|
|
47
|
+
index.should == 0
|
|
48
|
+
inbound.session_id.should == group.session_id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "round-trips an olm 1:1 message" do
|
|
52
|
+
alice = Async::Matrix::E2EE::Account.new
|
|
53
|
+
bob = Async::Matrix::E2EE::Account.new
|
|
54
|
+
bob.generate_one_time_keys(1)
|
|
55
|
+
bob_otk = bob.one_time_keys.values.first
|
|
56
|
+
|
|
57
|
+
outbound = alice.create_outbound_session(bob.curve25519_key, bob_otk)
|
|
58
|
+
type, body = outbound.encrypt("yo g")
|
|
59
|
+
type.should == 0 # pre-key message
|
|
60
|
+
|
|
61
|
+
session, plaintext = bob.create_inbound_session(alice.curve25519_key, body)
|
|
62
|
+
plaintext.should == "yo g"
|
|
63
|
+
|
|
64
|
+
reply_type, reply_body = session.encrypt("hello back")
|
|
65
|
+
outbound.decrypt(reply_type, reply_body).should == "hello back"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "verifies a valid ed25519 signature and rejects a bad one" do
|
|
69
|
+
account = Async::Matrix::E2EE::Account.new
|
|
70
|
+
signature = account.sign("payload")
|
|
71
|
+
|
|
72
|
+
Async::Matrix::E2EE.verify_signature(account.ed25519_key, "payload", signature).should == true
|
|
73
|
+
Async::Matrix::E2EE.verify_signature(account.ed25519_key, "tampered", signature).should == false
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "survives a pickle round-trip" do
|
|
77
|
+
key = "0" * 32
|
|
78
|
+
group = Async::Matrix::E2EE::GroupSession.new
|
|
79
|
+
pickled = group.pickle(key)
|
|
80
|
+
|
|
81
|
+
restored = Async::Matrix::E2EE::GroupSession.from_pickle(pickled, key)
|
|
82
|
+
restored.session_id.should == group.session_id
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
data/lib/async/matrix/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: async-matrix
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Kidd
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '1.2'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rb_sys
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0.9'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0.9'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: falcon
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -149,12 +163,27 @@ dependencies:
|
|
|
149
163
|
- - ">="
|
|
150
164
|
- !ruby/object:Gem::Version
|
|
151
165
|
version: '0'
|
|
166
|
+
- !ruby/object:Gem::Dependency
|
|
167
|
+
name: rake-compiler
|
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
|
169
|
+
requirements:
|
|
170
|
+
- - "~>"
|
|
171
|
+
- !ruby/object:Gem::Version
|
|
172
|
+
version: '1.2'
|
|
173
|
+
type: :development
|
|
174
|
+
prerelease: false
|
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - "~>"
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '1.2'
|
|
152
180
|
description: Async-native Matrix protocol primitives built on the Socketry async ecosystem.
|
|
153
181
|
Provides well-known discovery, event notification, and application service models.
|
|
154
182
|
email:
|
|
155
183
|
- nathankidd@hey.com
|
|
156
184
|
executables: []
|
|
157
|
-
extensions:
|
|
185
|
+
extensions:
|
|
186
|
+
- ext/async_matrix_e2ee/extconf.rb
|
|
158
187
|
extra_rdoc_files: []
|
|
159
188
|
files:
|
|
160
189
|
- LICENSE
|
|
@@ -455,6 +484,9 @@ files:
|
|
|
455
484
|
- data/matrix-spec/event-schemas/schema/m.sticker.yaml
|
|
456
485
|
- data/matrix-spec/event-schemas/schema/m.tag.yaml
|
|
457
486
|
- data/matrix-spec/event-schemas/schema/m.typing.yaml
|
|
487
|
+
- ext/async_matrix_e2ee/Cargo.toml
|
|
488
|
+
- ext/async_matrix_e2ee/extconf.rb
|
|
489
|
+
- ext/async_matrix_e2ee/src/lib.rs
|
|
458
490
|
- lib/async/discord.rb
|
|
459
491
|
- lib/async/discord/api.rb
|
|
460
492
|
- lib/async/discord/api/path_tree.rb
|
|
@@ -511,6 +543,7 @@ files:
|
|
|
511
543
|
- lib/async/matrix/client.rb
|
|
512
544
|
- lib/async/matrix/connection.rb
|
|
513
545
|
- lib/async/matrix/double_puppet_client.rb
|
|
546
|
+
- lib/async/matrix/e2ee.rb
|
|
514
547
|
- lib/async/matrix/endpoint.rb
|
|
515
548
|
- lib/async/matrix/error.rb
|
|
516
549
|
- lib/async/matrix/media_client.rb
|