vodozemac 0.1.0

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.
@@ -0,0 +1,900 @@
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), GroupSession /
5
+ //! InboundGroupSession (Megolm), Sas, the ed25519 and curve25519 key types and
6
+ //! PkEncryption / PkDecryption, under the `Vodozemac` module.
7
+ //!
8
+ //! The API is deliberately base64-string / integer centric rather than passing
9
+ //! wrapped key objects around: Matrix moves keys and ciphertext as base64
10
+ //! strings inside JSON, so a string API drops straight into the protocol layer
11
+ //! and keeps the magnus surface small. The key types are bound as classes too,
12
+ //! for the caller which wants to parse one before handing it on, but every
13
+ //! method takes and returns base64.
14
+ //!
15
+ //! # Provenance
16
+ //!
17
+ //! matrix-rust-sdk is the reference for how these primitives are driven, and it
18
+ //! is licensed Apache-2.0, as this project is. Logic may therefore be ported
19
+ //! from it directly: section 4(b) of the licence asks that a derived file carry
20
+ //! a prominent notice of the change, so a method which follows its logic says
21
+ //! so where it appears, and the root NOTICE records the attribution.
22
+ //!
23
+ //! Fractal is worth reading for how a client sequences these calls, but it is
24
+ //! GPL-3.0-or-later. Nothing may be copied from it into this file.
25
+ //!
26
+ //! - <https://github.com/matrix-org/matrix-rust-sdk> (Apache-2.0)
27
+ //! - <https://github.com/matrix-org/vodozemac> (Apache-2.0)
28
+ //! - <https://gitlab.gnome.org/World/fractal> (GPL-3.0-or-later; read only)
29
+
30
+ use std::cell::RefCell;
31
+ use std::collections::HashMap;
32
+
33
+ use magnus::{function, method, prelude::*, Error, Ruby};
34
+
35
+ use vodozemac_core::{
36
+ base64_decode, base64_encode, Curve25519PublicKey as Curve25519Public,
37
+ Curve25519SecretKey as Curve25519Secret, Ed25519Keypair as Ed25519Pair,
38
+ Ed25519PublicKey as Ed25519Public, Ed25519SecretKey as Ed25519Secret,
39
+ Ed25519Signature as Ed25519Sig,
40
+ };
41
+ use vodozemac_core::olm::{
42
+ Account as OlmAccount, AccountPickle, OlmMessage, PreKeyMessage, Session as OlmSession,
43
+ SessionConfig as OlmSessionConfig, SessionPickle,
44
+ };
45
+ use vodozemac_core::megolm::{
46
+ ExportedSessionKey, GroupSession as MegolmGroupSession, GroupSessionPickle,
47
+ InboundGroupSession as MegolmInbound, InboundGroupSessionPickle, MegolmMessage,
48
+ SessionConfig as MegolmSessionConfig, SessionKey,
49
+ };
50
+ use vodozemac_core::pk_encryption::{
51
+ Message as PkMessage, PkDecryption as PkDecrypt, PkEncryption as PkEncrypt,
52
+ };
53
+
54
+ // --- helpers ---------------------------------------------------------------
55
+
56
+ fn rt_err<E: std::fmt::Display>(e: E) -> Error {
57
+ // Safe: every caller runs inside a Ruby method invocation (GVL held).
58
+ let ruby = Ruby::get().expect("rt_err called outside the Ruby VM");
59
+ Error::new(ruby.exception_runtime_error(), e.to_string())
60
+ }
61
+
62
+ fn key32(label: &str, bytes: &[u8]) -> Result<[u8; 32], Error> {
63
+ bytes
64
+ .try_into()
65
+ .map_err(|_| rt_err(format!("{label} must be 32 bytes, got {}", bytes.len())))
66
+ }
67
+
68
+ /// Decode a base64 string into the 32 bytes a raw key is.
69
+ ///
70
+ /// The key types vodozemac builds from raw bytes take `&[u8; 32]`, and the ones
71
+ /// which parse base64 themselves do their own length checking. This is for the
72
+ /// former, so that a short string is refused by name rather than panicking.
73
+ fn secret32(label: &str, encoded: &str) -> Result<[u8; 32], Error> {
74
+ let bytes = base64_decode(encoded).map_err(rt_err)?;
75
+
76
+ key32(label, &bytes)
77
+ }
78
+
79
+ // --- Account ---------------------------------------------------------------
80
+
81
+ #[magnus::wrap(class = "Vodozemac::Account", free_immediately, size)]
82
+ struct Account(RefCell<OlmAccount>);
83
+
84
+ impl Account {
85
+ fn new() -> Self {
86
+ Account(RefCell::new(OlmAccount::new()))
87
+ }
88
+
89
+ fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
90
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
91
+ let p = AccountPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
92
+ Ok(Account(RefCell::new(OlmAccount::from_pickle(p))))
93
+ }
94
+
95
+ fn pickle(&self, pickle_key: String) -> Result<String, Error> {
96
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
97
+ Ok(self.0.borrow().pickle().encrypt(&key))
98
+ }
99
+
100
+ fn ed25519_key(&self) -> String {
101
+ self.0.borrow().ed25519_key().to_base64()
102
+ }
103
+
104
+ fn curve25519_key(&self) -> String {
105
+ self.0.borrow().curve25519_key().to_base64()
106
+ }
107
+
108
+ fn sign(&self, message: String) -> String {
109
+ self.0.borrow().sign(message.as_bytes()).to_base64()
110
+ }
111
+
112
+ fn one_time_keys(&self) -> HashMap<String, String> {
113
+ self.0
114
+ .borrow()
115
+ .one_time_keys()
116
+ .into_iter()
117
+ .map(|(k, v)| (k.to_base64(), v.to_base64()))
118
+ .collect()
119
+ }
120
+
121
+ fn max_number_of_one_time_keys(&self) -> usize {
122
+ self.0.borrow().max_number_of_one_time_keys()
123
+ }
124
+
125
+ /// How many one-time keys this account is holding, published or not.
126
+ fn stored_one_time_key_count(&self) -> usize {
127
+ self.0.borrow().stored_one_time_key_count()
128
+ }
129
+
130
+ fn generate_one_time_keys(&self, count: usize) {
131
+ self.0.borrow_mut().generate_one_time_keys(count);
132
+ }
133
+
134
+
135
+ fn fallback_key(&self) -> HashMap<String, String> {
136
+ self.0
137
+ .borrow()
138
+ .fallback_key()
139
+ .into_iter()
140
+ .map(|(k, v)| (k.to_base64(), v.to_base64()))
141
+ .collect()
142
+ }
143
+
144
+ fn generate_fallback_key(&self) {
145
+ self.0.borrow_mut().generate_fallback_key();
146
+ }
147
+
148
+ fn mark_keys_as_published(&self) {
149
+ self.0.borrow_mut().mark_keys_as_published();
150
+ }
151
+
152
+ /// Drops the private half of the previous fallback key.
153
+ ///
154
+ /// The spec says to keep at most two fallback private keys — the current one
155
+ /// and the one before it — and to drop the older once it is reasonably
156
+ /// certain no more messages will arrive using it, because a fallback key is
157
+ /// not consumed by use and so keeps decrypting replays indefinitely.
158
+ /// Returns whether there was one to forget.
159
+ fn forget_fallback_key(&self) -> bool {
160
+ self.0.borrow_mut().forget_fallback_key()
161
+ }
162
+
163
+ /// Returns a Session for sending to a peer identified by their identity and
164
+ /// one-time keys (both base64).
165
+ fn create_outbound_session(
166
+ &self,
167
+ identity_key: String,
168
+ one_time_key: String,
169
+ ) -> Result<Session, Error> {
170
+ let ik = Curve25519Public::from_base64(&identity_key).map_err(rt_err)?;
171
+ let otk = Curve25519Public::from_base64(&one_time_key).map_err(rt_err)?;
172
+ let session = self
173
+ .0
174
+ .borrow()
175
+ .create_outbound_session(OlmSessionConfig::version_1(), ik, otk)
176
+ .map_err(rt_err)?;
177
+ Ok(Session(RefCell::new(session)))
178
+ }
179
+
180
+ /// Establishes an inbound Session from a received pre-key message (base64
181
+ /// body, as carried in an m.room.encrypted olm `body`). Returns
182
+ /// `[session, plaintext]`.
183
+ fn create_inbound_session(
184
+ &self,
185
+ identity_key: String,
186
+ prekey_message_body: String,
187
+ ) -> Result<(Session, String), Error> {
188
+ let ik = Curve25519Public::from_base64(&identity_key).map_err(rt_err)?;
189
+ let prekey = PreKeyMessage::from_base64(&prekey_message_body).map_err(rt_err)?;
190
+ let result = self
191
+ .0
192
+ .borrow_mut()
193
+ .create_inbound_session(OlmSessionConfig::version_1(), ik, &prekey)
194
+ .map_err(rt_err)?;
195
+ let plaintext = String::from_utf8(result.plaintext).map_err(rt_err)?;
196
+ Ok((Session(RefCell::new(result.session)), plaintext))
197
+ }
198
+ }
199
+
200
+ // --- Session (Olm 1:1) -----------------------------------------------------
201
+
202
+ #[magnus::wrap(class = "Vodozemac::Session", free_immediately, size)]
203
+ struct Session(RefCell<OlmSession>);
204
+
205
+ impl Session {
206
+ fn session_id(&self) -> String {
207
+ self.0.borrow().session_id()
208
+ }
209
+
210
+ /// Whether this session has ever decrypted a message from the other side.
211
+ ///
212
+ /// Until it has, every message it encrypts is a pre-key message, because the
213
+ /// peer may not have the session yet. This is what says which.
214
+ fn has_received_message(&self) -> bool {
215
+ self.0.borrow().has_received_message()
216
+ }
217
+
218
+ fn pickle(&self, pickle_key: String) -> Result<String, Error> {
219
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
220
+ Ok(self.0.borrow().pickle().encrypt(&key))
221
+ }
222
+
223
+ fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
224
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
225
+ let p = SessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
226
+ Ok(Session(RefCell::new(OlmSession::from_pickle(p))))
227
+ }
228
+
229
+ /// Encrypts plaintext; returns `[message_type, body_base64]` matching the
230
+ /// Matrix olm ciphertext shape.
231
+ fn encrypt(&self, plaintext: String) -> Result<(usize, String), Error> {
232
+ let message = self.0.borrow_mut().encrypt(plaintext.as_bytes()).map_err(rt_err)?;
233
+ let (mtype, ciphertext) = message.to_parts();
234
+ Ok((mtype, base64_encode(ciphertext)))
235
+ }
236
+
237
+ /// Decrypts an olm message given its type (0 = pre-key, 1 = normal) and
238
+ /// base64 body.
239
+ fn decrypt(&self, message_type: usize, body: String) -> Result<String, Error> {
240
+ let ciphertext = base64_decode(&body).map_err(rt_err)?;
241
+ let message = OlmMessage::from_parts(message_type, &ciphertext).map_err(rt_err)?;
242
+ let plaintext = self.0.borrow_mut().decrypt(&message).map_err(rt_err)?;
243
+ String::from_utf8(plaintext).map_err(rt_err)
244
+ }
245
+ }
246
+
247
+ // --- GroupSession (Megolm outbound) ---------------------------------------
248
+
249
+ #[magnus::wrap(class = "Vodozemac::GroupSession", free_immediately, size)]
250
+ struct GroupSession(RefCell<MegolmGroupSession>);
251
+
252
+ impl GroupSession {
253
+ fn new() -> Self {
254
+ GroupSession(RefCell::new(MegolmGroupSession::new(MegolmSessionConfig::version_1())))
255
+ }
256
+
257
+ fn session_id(&self) -> String {
258
+ self.0.borrow().session_id()
259
+ }
260
+
261
+ fn message_index(&self) -> u32 {
262
+ self.0.borrow().message_index()
263
+ }
264
+
265
+ /// The session key (base64) to share with recipients via m.room_key.
266
+ fn session_key(&self) -> String {
267
+ self.0.borrow().session_key().to_base64()
268
+ }
269
+
270
+ /// Encrypts plaintext, returning the megolm message as base64.
271
+ fn encrypt(&self, plaintext: String) -> String {
272
+ self.0.borrow_mut().encrypt(plaintext.as_bytes()).to_base64()
273
+ }
274
+
275
+ fn pickle(&self, pickle_key: String) -> Result<String, Error> {
276
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
277
+ Ok(self.0.borrow().pickle().encrypt(&key))
278
+ }
279
+
280
+ fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
281
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
282
+ let p = GroupSessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
283
+ Ok(GroupSession(RefCell::new(MegolmGroupSession::from_pickle(p))))
284
+ }
285
+ }
286
+
287
+ // --- InboundGroupSession (Megolm inbound) ---------------------------------
288
+
289
+ #[magnus::wrap(class = "Vodozemac::InboundGroupSession", free_immediately, size)]
290
+ struct InboundGroupSession(RefCell<MegolmInbound>);
291
+
292
+ impl InboundGroupSession {
293
+ /// Build from a base64 session key received in an m.room_key event.
294
+ fn new(session_key: String) -> Result<Self, Error> {
295
+ let key = SessionKey::from_base64(&session_key).map_err(rt_err)?;
296
+ Ok(InboundGroupSession(RefCell::new(MegolmInbound::new(
297
+ &key,
298
+ MegolmSessionConfig::version_1(),
299
+ ))))
300
+ }
301
+
302
+ /// Build from a base64 **exported** session key.
303
+ ///
304
+ /// This is the format room keys travel in outside of `m.room_key`: server-side
305
+ /// key backup (`m.megolm_backup.v1.curve25519-aes-sha2`) and
306
+ /// `m.forwarded_room_key` both carry an `ExportedSessionKey`, which is
307
+ /// version 1 and carries no signature — so `new` (which requires a signed
308
+ /// version 2 `SessionKey`) cannot accept them. Without this, a client can
309
+ /// never read history it did not receive a live `m.room_key` for.
310
+ ///
311
+ /// The resulting session is NOT signature-verified, because an exported key
312
+ /// has no signature to verify. That is inherent to the format: the sender
313
+ /// identity of such a session is only as trustworthy as whoever handed it
314
+ /// over, which is why the spec treats backup-restored keys as unverified.
315
+ fn import(exported_key: String) -> Result<Self, Error> {
316
+ let key = ExportedSessionKey::from_base64(&exported_key).map_err(rt_err)?;
317
+ Ok(InboundGroupSession(RefCell::new(MegolmInbound::import(
318
+ &key,
319
+ MegolmSessionConfig::version_1(),
320
+ ))))
321
+ }
322
+
323
+ fn session_id(&self) -> String {
324
+ self.0.borrow().session_id()
325
+ }
326
+
327
+ fn first_known_index(&self) -> u32 {
328
+ self.0.borrow().first_known_index()
329
+ }
330
+
331
+ /// Whether the two sessions are the same megolm session, by their ratchets.
332
+ ///
333
+ /// Two copies of one session may have been received separately — one live,
334
+ /// one from backup — and reached different indices. This says they belong
335
+ /// together without saying which is ahead.
336
+ fn connected(&self, other: &InboundGroupSession) -> bool {
337
+ self.0.borrow_mut().connected(&mut other.0.borrow_mut())
338
+ }
339
+
340
+ /// Which of the two sessions holds more of the ratchet.
341
+ ///
342
+ /// Returns one of `:equal`, `:better`, `:worse` or `:unconnected` — the last
343
+ /// meaning they are not the same session at all. This is how a client
344
+ /// decides whether a key it has just been handed is worth keeping over the
345
+ /// one it already has.
346
+ fn compare(&self, other: &InboundGroupSession) -> Result<magnus::Symbol, Error> {
347
+ use vodozemac_core::megolm::SessionOrdering;
348
+
349
+ let ruby = Ruby::get().map_err(rt_err)?;
350
+
351
+ let name = match self.0.borrow_mut().compare(&mut other.0.borrow_mut()) {
352
+ SessionOrdering::Equal => "equal",
353
+ SessionOrdering::Better => "better",
354
+ SessionOrdering::Worse => "worse",
355
+ SessionOrdering::Unconnected => "unconnected",
356
+ };
357
+
358
+ Ok(ruby.to_symbol(name))
359
+ }
360
+
361
+ /// Move the ratchet forward to `index`, discarding the keys before it.
362
+ ///
363
+ /// Returns whether it moved. This is how a session is trimmed to the history
364
+ /// a room's visibility rules allow a member to read.
365
+ fn advance_to(&self, index: u32) -> bool {
366
+ self.0.borrow_mut().advance_to(index)
367
+ }
368
+
369
+ /// Export this session at `index`, base64, for uploading to key backup or
370
+ /// forwarding to another device. Returns nil when the ratchet has already
371
+ /// moved past `index` and can no longer produce a key for it — megolm
372
+ /// ratchets forward only, so an earlier index is unrecoverable by design.
373
+ fn export_at(&self, index: u32) -> Option<String> {
374
+ self.0.borrow_mut().export_at(index).map(|key| key.to_base64())
375
+ }
376
+
377
+ /// Export at the earliest index this session can still reach.
378
+ fn export_at_first_known_index(&self) -> String {
379
+ self.0.borrow().export_at_first_known_index().to_base64()
380
+ }
381
+
382
+ /// Decrypts a base64 megolm message; returns `[plaintext, message_index]`.
383
+ fn decrypt(&self, message: String) -> Result<(String, u32), Error> {
384
+ let msg = MegolmMessage::from_base64(&message).map_err(rt_err)?;
385
+ let decrypted = self.0.borrow_mut().decrypt(&msg).map_err(rt_err)?;
386
+ let plaintext = String::from_utf8(decrypted.plaintext).map_err(rt_err)?;
387
+ Ok((plaintext, decrypted.message_index))
388
+ }
389
+
390
+ fn pickle(&self, pickle_key: String) -> Result<String, Error> {
391
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
392
+ Ok(self.0.borrow().pickle().encrypt(&key))
393
+ }
394
+
395
+ fn from_pickle(pickle: String, pickle_key: String) -> Result<Self, Error> {
396
+ let key = key32("pickle_key", pickle_key.as_bytes())?;
397
+ let p = InboundGroupSessionPickle::from_encrypted(&pickle, &key).map_err(rt_err)?;
398
+ Ok(InboundGroupSession(RefCell::new(MegolmInbound::from_pickle(p))))
399
+ }
400
+ }
401
+
402
+ // --- Sas (short authentication string) -------------------------------------
403
+
404
+ /// One half of an SAS verification, before the Diffie-Hellman.
405
+ ///
406
+ /// A `Sas` establishes exactly one shared secret and is consumed doing so,
407
+ /// which is why `diffie_hellman` takes it out of the cell: vodozemac models that
408
+ /// with an owned `self`, and a second attempt is a programming error rather than
409
+ /// something to be silently allowed.
410
+ #[magnus::wrap(class = "Vodozemac::Sas", free_immediately, size)]
411
+ struct Sas {
412
+ sas: RefCell<Option<vodozemac_core::sas::Sas>>,
413
+ // Kept separately so that it survives the Diffie-Hellman: the SAS and MAC
414
+ // info strings are built from BOTH public keys, so a caller still needs its
415
+ // own after the exchange has consumed the Sas itself.
416
+ public_key: String,
417
+ }
418
+
419
+ impl Sas {
420
+ fn new() -> Self {
421
+ let sas = vodozemac_core::sas::Sas::new();
422
+ let public_key = sas.public_key().to_base64();
423
+
424
+ Sas {
425
+ sas: RefCell::new(Some(sas)),
426
+ public_key,
427
+ }
428
+ }
429
+
430
+ /// The ephemeral public key to send in `m.key.verification.key`.
431
+ fn public_key(&self) -> String {
432
+ self.public_key.clone()
433
+ }
434
+
435
+ fn diffie_hellman(&self, their_public_key: String) -> Result<EstablishedSas, Error> {
436
+ let key = Curve25519Public::from_base64(&their_public_key).map_err(rt_err)?;
437
+ let sas = self
438
+ .sas
439
+ .borrow_mut()
440
+ .take()
441
+ .ok_or_else(|| rt_err("this Sas has already been established"))?;
442
+
443
+ Ok(EstablishedSas {
444
+ sas: sas.diffie_hellman(key).map_err(rt_err)?,
445
+ our_public_key: self.public_key.clone(),
446
+ // Re-encoded from the parsed key rather than kept as given, so that
447
+ // the pair is canonical base64 on both sides: the info strings are
448
+ // built by concatenating these, and must match byte for byte.
449
+ their_public_key: key.to_base64(),
450
+ })
451
+ }
452
+ }
453
+
454
+ /// An established SAS channel, which carries both public keys.
455
+ ///
456
+ /// vodozemac drops them at this point, but every info string the spec defines
457
+ /// is built from both, so carrying them here is what lets a caller construct
458
+ /// one without tracking the keys itself.
459
+ #[magnus::wrap(class = "Vodozemac::EstablishedSas", free_immediately, size)]
460
+ struct EstablishedSas {
461
+ sas: vodozemac_core::sas::EstablishedSas,
462
+ our_public_key: String,
463
+ their_public_key: String,
464
+ }
465
+
466
+ impl EstablishedSas {
467
+ /// Our own ephemeral public key, as sent in `m.key.verification.key`.
468
+ fn our_public_key(&self) -> String {
469
+ self.our_public_key.clone()
470
+ }
471
+
472
+ /// The other device's ephemeral public key, as received.
473
+ fn their_public_key(&self) -> String {
474
+ self.their_public_key.clone()
475
+ }
476
+
477
+ /// The three numbers of the `decimal` SAS method, already offset by 1000.
478
+ ///
479
+ /// This is the method to offer. Decimal is required of every client, so it
480
+ /// always agrees, and MSC4405 deprecates the alternative.
481
+ fn decimals(&self, info: String) -> (u16, u16, u16) {
482
+ self.sas.bytes(&info).decimals()
483
+ }
484
+
485
+ /// The seven emoji indices of the `emoji` SAS method, as table offsets.
486
+ ///
487
+ /// Deprecated by MSC4405, because each client names the emoji in its own
488
+ /// language and two users can therefore hold different words for the same
489
+ /// picture. Kept so that a verification with a client which offers nothing
490
+ /// else can still be shown.
491
+ fn emoji_indices(&self, info: String) -> Vec<u8> {
492
+ self.sas.bytes(&info).emoji_indices().to_vec()
493
+ }
494
+
495
+ /// The raw bytes, for a SAS method this crate does not implement.
496
+ fn bytes(&self, info: String) -> Vec<u8> {
497
+ self.sas.bytes(&info).as_bytes().to_vec()
498
+ }
499
+
500
+ /// As many raw bytes as a method asks for, rather than the six `bytes` gives.
501
+ ///
502
+ /// Refused above 8160 bytes, which is the most HKDF-SHA256 will expand to.
503
+ fn bytes_raw(&self, info: String, count: usize) -> Result<Vec<u8>, Error> {
504
+ self.sas.bytes_raw(&info, count).map_err(|_| {
505
+ rt_err(format!("cannot expand {count} bytes of SAS material"))
506
+ })
507
+ }
508
+
509
+ fn calculate_mac(&self, input: String, info: String) -> String {
510
+ self.sas.calculate_mac(&input, &info).to_base64()
511
+ }
512
+
513
+ /// The `hkdf-hmac-sha256` MAC, which libolm encoded incorrectly.
514
+ ///
515
+ /// Deprecated by the spec in favour of `hkdf-hmac-sha256.v2`, and kept only
516
+ /// so that a verification with an older client can still be read.
517
+ fn calculate_mac_invalid_base64(&self, input: String, info: String) -> String {
518
+ self.sas.calculate_mac_invalid_base64(&input, &info)
519
+ }
520
+
521
+ fn verify_mac(&self, input: String, info: String, tag: String) -> Result<bool, Error> {
522
+ let mac = vodozemac_core::sas::Mac::from_base64(&tag).map_err(rt_err)?;
523
+ Ok(self.sas.verify_mac(&input, &info, &mac).is_ok())
524
+ }
525
+ }
526
+
527
+ // --- ed25519 ---------------------------------------------------------------
528
+
529
+ /// A long-term ed25519 signing key, which can be written down and read back.
530
+ ///
531
+ /// This is the type a Matrix cross-signing key is: the private half goes into
532
+ /// secret storage as base64, and the public half names the key. `Ed25519Keypair`
533
+ /// is the faster one for signing in bulk, but it has no serialisation, so an
534
+ /// identity which has to survive a restart is this one.
535
+ #[magnus::wrap(class = "Vodozemac::Ed25519SecretKey", free_immediately, size)]
536
+ struct Ed25519SecretKey(Ed25519Secret);
537
+
538
+ impl Ed25519SecretKey {
539
+ fn new() -> Self {
540
+ Ed25519SecretKey(Ed25519Secret::new())
541
+ }
542
+
543
+ fn from_base64(encoded: String) -> Result<Self, Error> {
544
+ Ok(Ed25519SecretKey(Ed25519Secret::from_base64(&encoded).map_err(rt_err)?))
545
+ }
546
+
547
+ /// The private half, base64. This belongs in secret storage and nowhere else.
548
+ fn to_base64(&self) -> String {
549
+ self.0.to_base64()
550
+ }
551
+
552
+ /// The public half, base64, which is also how the key is named.
553
+ fn public_key(&self) -> String {
554
+ self.0.public_key().to_base64()
555
+ }
556
+
557
+ /// Sign a message, returning the signature as base64.
558
+ fn sign(&self, message: String) -> String {
559
+ self.0.sign(message.as_bytes()).to_base64()
560
+ }
561
+ }
562
+
563
+ /// An ephemeral ed25519 keypair, which signs faster and cannot be written down.
564
+ ///
565
+ /// vodozemac keeps its pickle private, so there is no way to serialise one: this
566
+ /// is for a key which lives as long as the process does. An Account holds one of
567
+ /// these for its own device signature.
568
+ #[magnus::wrap(class = "Vodozemac::Ed25519Keypair", free_immediately, size)]
569
+ struct Ed25519Keypair(Ed25519Pair);
570
+
571
+ impl Ed25519Keypair {
572
+ fn new() -> Self {
573
+ Ed25519Keypair(Ed25519Pair::new())
574
+ }
575
+
576
+ /// The public half, base64, which is also how the key is named.
577
+ fn public_key(&self) -> String {
578
+ self.0.public_key().to_base64()
579
+ }
580
+
581
+ /// Sign a message, returning the signature as base64.
582
+ fn sign(&self, message: String) -> String {
583
+ self.0.sign(message.as_bytes()).to_base64()
584
+ }
585
+ }
586
+
587
+ /// The public half of an ed25519 key, which is what a signature is checked against.
588
+ #[magnus::wrap(class = "Vodozemac::Ed25519PublicKey", free_immediately, size)]
589
+ struct Ed25519PublicKey(Ed25519Public);
590
+
591
+ impl Ed25519PublicKey {
592
+ fn from_base64(encoded: String) -> Result<Self, Error> {
593
+ Ok(Ed25519PublicKey(Ed25519Public::from_base64(&encoded).map_err(rt_err)?))
594
+ }
595
+
596
+ fn to_base64(&self) -> String {
597
+ self.0.to_base64()
598
+ }
599
+
600
+ /// Whether this key signed the message, given the base64 signature.
601
+ fn verify(&self, message: String, signature: String) -> Result<bool, Error> {
602
+ let sig = Ed25519Sig::from_base64(&signature).map_err(rt_err)?;
603
+
604
+ Ok(self.0.verify(message.as_bytes(), &sig).is_ok())
605
+ }
606
+ }
607
+
608
+ /// An ed25519 signature, for the caller which wants one parsed before sending it.
609
+ ///
610
+ /// Every method here takes and returns a signature as base64, because that is
611
+ /// how Matrix carries one. This is the type for checking that a string is a
612
+ /// well-formed signature at all.
613
+ #[magnus::wrap(class = "Vodozemac::Ed25519Signature", free_immediately, size)]
614
+ struct Ed25519Signature(Ed25519Sig);
615
+
616
+ impl Ed25519Signature {
617
+ fn from_base64(encoded: String) -> Result<Self, Error> {
618
+ Ok(Ed25519Signature(Ed25519Sig::from_base64(&encoded).map_err(rt_err)?))
619
+ }
620
+
621
+ fn to_base64(&self) -> String {
622
+ self.0.to_base64()
623
+ }
624
+ }
625
+
626
+ // --- curve25519 ------------------------------------------------------------
627
+
628
+ /// The private half of a curve25519 key, which agrees a secret with a public one.
629
+ #[magnus::wrap(class = "Vodozemac::Curve25519SecretKey", free_immediately, size)]
630
+ struct Curve25519SecretKey(Curve25519Secret);
631
+
632
+ impl Curve25519SecretKey {
633
+ fn new() -> Self {
634
+ Curve25519SecretKey(Curve25519Secret::new())
635
+ }
636
+
637
+ fn from_base64(encoded: String) -> Result<Self, Error> {
638
+ let bytes = secret32("secret key", &encoded)?;
639
+
640
+ Ok(Curve25519SecretKey(Curve25519Secret::from_slice(&bytes)))
641
+ }
642
+
643
+ fn to_base64(&self) -> String {
644
+ base64_encode(self.0.to_bytes().as_slice())
645
+ }
646
+
647
+ fn public_key(&self) -> String {
648
+ Curve25519Public::from(&self.0).to_base64()
649
+ }
650
+
651
+ /// The raw shared secret with another key, base64.
652
+ ///
653
+ /// Nothing in Matrix wants this: Sas and PkEncryption do their own exchange
654
+ /// and derive from it. It is bound because vodozemac exposes it, and a raw
655
+ /// secret which has not been through a KDF is not a key — derive one before
656
+ /// it encrypts anything. Nil when the peer's key is non-contributory.
657
+ fn diffie_hellman(&self, their_public_key: String) -> Result<Option<String>, Error> {
658
+ let key = Curve25519Public::from_base64(&their_public_key).map_err(rt_err)?;
659
+
660
+ Ok(self.0.diffie_hellman(&key).map(|shared| base64_encode(shared.as_bytes())))
661
+ }
662
+ }
663
+
664
+ /// The public half of a curve25519 key, which is how a device is addressed.
665
+ #[magnus::wrap(class = "Vodozemac::Curve25519PublicKey", free_immediately, size)]
666
+ struct Curve25519PublicKey(Curve25519Public);
667
+
668
+ impl Curve25519PublicKey {
669
+ fn from_base64(encoded: String) -> Result<Self, Error> {
670
+ Ok(Curve25519PublicKey(Curve25519Public::from_base64(&encoded).map_err(rt_err)?))
671
+ }
672
+
673
+ fn to_base64(&self) -> String {
674
+ self.0.to_base64()
675
+ }
676
+ }
677
+
678
+ // --- PkEncryption / PkDecryption -------------------------------------------
679
+
680
+ /// The private half of a PkEncryption key, which reads what was sealed to it.
681
+ ///
682
+ /// This is libolm's PkDecryption, which is the primitive Matrix server-side key
683
+ /// backup is built on: `m.megolm_backup.v1.curve25519-aes-sha2` is an ECDH to
684
+ /// this key, HKDF-SHA256, AES-256-CBC and a truncated HMAC.
685
+ ///
686
+ /// The MAC authenticates nothing — libolm computed it over the empty string
687
+ /// rather than the ciphertext, and every implementation has reproduced the bug
688
+ /// ever since for compatibility. So a message which verifies has only been shown
689
+ /// to come from somebody who knew the public key, which is everybody. See the
690
+ /// warning in the specification, and MSC4048 for the version which fixes it.
691
+ #[magnus::wrap(class = "Vodozemac::PkDecryption", free_immediately, size)]
692
+ struct PkDecryption(PkDecrypt);
693
+
694
+ impl PkDecryption {
695
+ fn new() -> Self {
696
+ PkDecryption(PkDecrypt::new())
697
+ }
698
+
699
+ /// The key a stored private half describes, which is how a backup is reopened.
700
+ fn from_base64(secret_key: String) -> Result<Self, Error> {
701
+ let bytes = secret32("secret key", &secret_key)?;
702
+
703
+ Ok(PkDecryption(PkDecrypt::from_key(Curve25519Secret::from_slice(&bytes))))
704
+ }
705
+
706
+ /// The public half, base64, which anybody may seal to.
707
+ fn public_key(&self) -> String {
708
+ self.0.public_key().to_base64()
709
+ }
710
+
711
+ /// The private half, base64, which belongs in secret storage and nowhere else.
712
+ fn secret_key(&self) -> String {
713
+ base64_encode(self.0.secret_key().to_bytes().as_slice())
714
+ }
715
+
716
+ /// Open a message, given its three base64 parts as the specification names them.
717
+ fn decrypt(
718
+ &self,
719
+ ciphertext: String,
720
+ mac: String,
721
+ ephemeral_key: String,
722
+ ) -> Result<String, Error> {
723
+ let message = PkMessage::from_base64(&ciphertext, &mac, &ephemeral_key).map_err(rt_err)?;
724
+ let plaintext = self.0.decrypt(&message).map_err(rt_err)?;
725
+
726
+ String::from_utf8(plaintext).map_err(rt_err)
727
+ }
728
+ }
729
+
730
+ /// The public half of a PkEncryption key, which seals to it.
731
+ ///
732
+ /// Holding this is enough to add to a backup and not enough to read one, which
733
+ /// is what lets a device upload history it cannot itself recover.
734
+ #[magnus::wrap(class = "Vodozemac::PkEncryption", free_immediately, size)]
735
+ struct PkEncryption(PkEncrypt);
736
+
737
+ impl PkEncryption {
738
+ fn from_base64(public_key: String) -> Result<Self, Error> {
739
+ let key = Curve25519Public::from_base64(&public_key).map_err(rt_err)?;
740
+
741
+ Ok(PkEncryption(PkEncrypt::from_key(key)))
742
+ }
743
+
744
+ /// Seal a message; returns `[ciphertext, mac, ephemeral_key]`, each base64.
745
+ fn encrypt(&self, plaintext: String) -> Result<(String, String, String), Error> {
746
+ let message = self.0.encrypt(plaintext.as_bytes()).map_err(rt_err)?;
747
+
748
+ Ok((
749
+ base64_encode(&message.ciphertext),
750
+ base64_encode(&message.mac),
751
+ message.ephemeral_key.to_base64(),
752
+ ))
753
+ }
754
+ }
755
+
756
+ // --- module-level functions ------------------------------------------------
757
+
758
+ /// Verify an ed25519 signature. `key` and `signature` are base64; returns bool.
759
+ fn verify_signature(key: String, message: String, signature: String) -> Result<bool, Error> {
760
+ let public_key = Ed25519Public::from_base64(&key).map_err(rt_err)?;
761
+ let sig = Ed25519Sig::from_base64(&signature).map_err(rt_err)?;
762
+ Ok(public_key.verify(message.as_bytes(), &sig).is_ok())
763
+ }
764
+
765
+ // --- init ------------------------------------------------------------------
766
+
767
+ #[magnus::init]
768
+ fn init(ruby: &Ruby) -> Result<(), Error> {
769
+ let namespace = ruby.define_module("Vodozemac")?;
770
+
771
+ // Which vodozemac this was built against, as against Vodozemac::VERSION, which is the gem's.
772
+ namespace.const_set("NATIVE_VERSION", vodozemac_core::VERSION)?;
773
+
774
+ let account = namespace.define_class("Account", ruby.class_object())?;
775
+ account.define_singleton_method("new", function!(Account::new, 0))?;
776
+ account.define_singleton_method("from_pickle", function!(Account::from_pickle, 2))?;
777
+ account.define_method("pickle", method!(Account::pickle, 1))?;
778
+ account.define_method("ed25519_key", method!(Account::ed25519_key, 0))?;
779
+ account.define_method("curve25519_key", method!(Account::curve25519_key, 0))?;
780
+ account.define_method("sign", method!(Account::sign, 1))?;
781
+ account.define_method("one_time_keys", method!(Account::one_time_keys, 0))?;
782
+ account.define_method(
783
+ "max_number_of_one_time_keys",
784
+ method!(Account::max_number_of_one_time_keys, 0),
785
+ )?;
786
+ account.define_method(
787
+ "stored_one_time_key_count",
788
+ method!(Account::stored_one_time_key_count, 0),
789
+ )?;
790
+ account.define_method("generate_one_time_keys", method!(Account::generate_one_time_keys, 1))?;
791
+ account.define_method("fallback_key", method!(Account::fallback_key, 0))?;
792
+ account.define_method("generate_fallback_key", method!(Account::generate_fallback_key, 0))?;
793
+ account.define_method("mark_keys_as_published", method!(Account::mark_keys_as_published, 0))?;
794
+ account.define_method("forget_fallback_key", method!(Account::forget_fallback_key, 0))?;
795
+ account.define_method("create_outbound_session", method!(Account::create_outbound_session, 2))?;
796
+ account.define_method("create_inbound_session", method!(Account::create_inbound_session, 2))?;
797
+
798
+ let session = namespace.define_class("Session", ruby.class_object())?;
799
+ session.define_singleton_method("from_pickle", function!(Session::from_pickle, 2))?;
800
+ session.define_method("session_id", method!(Session::session_id, 0))?;
801
+ session.define_method("has_received_message", method!(Session::has_received_message, 0))?;
802
+ session.define_method("pickle", method!(Session::pickle, 1))?;
803
+ session.define_method("encrypt", method!(Session::encrypt, 1))?;
804
+ session.define_method("decrypt", method!(Session::decrypt, 2))?;
805
+
806
+ let group = namespace.define_class("GroupSession", ruby.class_object())?;
807
+ group.define_singleton_method("new", function!(GroupSession::new, 0))?;
808
+ group.define_singleton_method("from_pickle", function!(GroupSession::from_pickle, 2))?;
809
+ group.define_method("session_id", method!(GroupSession::session_id, 0))?;
810
+ group.define_method("message_index", method!(GroupSession::message_index, 0))?;
811
+ group.define_method("session_key", method!(GroupSession::session_key, 0))?;
812
+ group.define_method("encrypt", method!(GroupSession::encrypt, 1))?;
813
+ group.define_method("pickle", method!(GroupSession::pickle, 1))?;
814
+
815
+ let inbound = namespace.define_class("InboundGroupSession", ruby.class_object())?;
816
+ inbound.define_singleton_method("new", function!(InboundGroupSession::new, 1))?;
817
+ inbound.define_singleton_method("import", function!(InboundGroupSession::import, 1))?;
818
+ inbound.define_singleton_method("from_pickle", function!(InboundGroupSession::from_pickle, 2))?;
819
+ inbound.define_method("session_id", method!(InboundGroupSession::session_id, 0))?;
820
+ inbound.define_method("first_known_index", method!(InboundGroupSession::first_known_index, 0))?;
821
+ inbound.define_method("connected", method!(InboundGroupSession::connected, 1))?;
822
+ inbound.define_method("compare", method!(InboundGroupSession::compare, 1))?;
823
+ inbound.define_method("advance_to", method!(InboundGroupSession::advance_to, 1))?;
824
+ inbound.define_method("decrypt", method!(InboundGroupSession::decrypt, 1))?;
825
+ inbound.define_method("pickle", method!(InboundGroupSession::pickle, 1))?;
826
+ inbound.define_method("export_at", method!(InboundGroupSession::export_at, 1))?;
827
+ inbound.define_method(
828
+ "export_at_first_known_index",
829
+ method!(InboundGroupSession::export_at_first_known_index, 0),
830
+ )?;
831
+
832
+ let sas = namespace.define_class("Sas", ruby.class_object())?;
833
+ sas.define_singleton_method("new", function!(Sas::new, 0))?;
834
+ sas.define_method("public_key", method!(Sas::public_key, 0))?;
835
+ sas.define_method("diffie_hellman", method!(Sas::diffie_hellman, 1))?;
836
+
837
+ let established = namespace.define_class("EstablishedSas", ruby.class_object())?;
838
+ established.define_method("our_public_key", method!(EstablishedSas::our_public_key, 0))?;
839
+ established.define_method("their_public_key", method!(EstablishedSas::their_public_key, 0))?;
840
+ established.define_method("emoji_indices", method!(EstablishedSas::emoji_indices, 1))?;
841
+ established.define_method("decimals", method!(EstablishedSas::decimals, 1))?;
842
+ established.define_method("bytes", method!(EstablishedSas::bytes, 1))?;
843
+ established.define_method("bytes_raw", method!(EstablishedSas::bytes_raw, 2))?;
844
+ established.define_method("calculate_mac", method!(EstablishedSas::calculate_mac, 2))?;
845
+ established.define_method(
846
+ "calculate_mac_invalid_base64",
847
+ method!(EstablishedSas::calculate_mac_invalid_base64, 2),
848
+ )?;
849
+ established.define_method("verify_mac", method!(EstablishedSas::verify_mac, 3))?;
850
+
851
+ let secret = namespace.define_class("Ed25519SecretKey", ruby.class_object())?;
852
+ secret.define_singleton_method("new", function!(Ed25519SecretKey::new, 0))?;
853
+ secret.define_singleton_method("from_base64", function!(Ed25519SecretKey::from_base64, 1))?;
854
+ secret.define_method("to_base64", method!(Ed25519SecretKey::to_base64, 0))?;
855
+ secret.define_method("public_key", method!(Ed25519SecretKey::public_key, 0))?;
856
+ secret.define_method("sign", method!(Ed25519SecretKey::sign, 1))?;
857
+
858
+ let keypair = namespace.define_class("Ed25519Keypair", ruby.class_object())?;
859
+ keypair.define_singleton_method("new", function!(Ed25519Keypair::new, 0))?;
860
+ keypair.define_method("public_key", method!(Ed25519Keypair::public_key, 0))?;
861
+ keypair.define_method("sign", method!(Ed25519Keypair::sign, 1))?;
862
+
863
+ let public = namespace.define_class("Ed25519PublicKey", ruby.class_object())?;
864
+ public.define_singleton_method("from_base64", function!(Ed25519PublicKey::from_base64, 1))?;
865
+ public.define_method("to_base64", method!(Ed25519PublicKey::to_base64, 0))?;
866
+ public.define_method("verify", method!(Ed25519PublicKey::verify, 2))?;
867
+
868
+ let signature = namespace.define_class("Ed25519Signature", ruby.class_object())?;
869
+ signature.define_singleton_method("from_base64", function!(Ed25519Signature::from_base64, 1))?;
870
+ signature.define_method("to_base64", method!(Ed25519Signature::to_base64, 0))?;
871
+
872
+ let curve_secret = namespace.define_class("Curve25519SecretKey", ruby.class_object())?;
873
+ curve_secret.define_singleton_method("new", function!(Curve25519SecretKey::new, 0))?;
874
+ curve_secret
875
+ .define_singleton_method("from_base64", function!(Curve25519SecretKey::from_base64, 1))?;
876
+ curve_secret.define_method("to_base64", method!(Curve25519SecretKey::to_base64, 0))?;
877
+ curve_secret.define_method("public_key", method!(Curve25519SecretKey::public_key, 0))?;
878
+ curve_secret
879
+ .define_method("diffie_hellman", method!(Curve25519SecretKey::diffie_hellman, 1))?;
880
+
881
+ let curve_public = namespace.define_class("Curve25519PublicKey", ruby.class_object())?;
882
+ curve_public
883
+ .define_singleton_method("from_base64", function!(Curve25519PublicKey::from_base64, 1))?;
884
+ curve_public.define_method("to_base64", method!(Curve25519PublicKey::to_base64, 0))?;
885
+
886
+ let decryption = namespace.define_class("PkDecryption", ruby.class_object())?;
887
+ decryption.define_singleton_method("new", function!(PkDecryption::new, 0))?;
888
+ decryption.define_singleton_method("from_base64", function!(PkDecryption::from_base64, 1))?;
889
+ decryption.define_method("public_key", method!(PkDecryption::public_key, 0))?;
890
+ decryption.define_method("secret_key", method!(PkDecryption::secret_key, 0))?;
891
+ decryption.define_method("decrypt", method!(PkDecryption::decrypt, 3))?;
892
+
893
+ let encryption = namespace.define_class("PkEncryption", ruby.class_object())?;
894
+ encryption.define_singleton_method("from_base64", function!(PkEncryption::from_base64, 1))?;
895
+ encryption.define_method("encrypt", method!(PkEncryption::encrypt, 1))?;
896
+
897
+ namespace.define_module_function("verify_signature", function!(verify_signature, 3))?;
898
+
899
+ Ok(())
900
+ }