Skip to content

Commit 2e4a71b

Browse files
committed
Expose the ECIES support from vodozemac
1 parent b364407 commit 2e4a71b

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# UNRELEASED
22

3+
- Expose the vodozemac ECIES support, which can be used to establish the secure
4+
channel required for QR code login described in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).
5+
36
- Add `QrCodeData` and `QrCodeMode` classes which can be used to parse or
47
generate QR codes intended for the QR code login mechanism described in
58
[MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).

src/vodozemac/ecies.rs

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//! This module implements [ECIES](https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme), the
2+
//! elliptic curve variant of the Integrated Encryption Scheme.
3+
//!
4+
//! Please take a look at the vodozemac documentation of this module for more
5+
//! info.
6+
7+
#![allow(missing_debug_implementations)]
8+
use std::sync::{Arc, Mutex};
9+
10+
use matrix_sdk_crypto::vodozemac::ecies;
11+
use wasm_bindgen::prelude::*;
12+
13+
use super::Curve25519PublicKey;
14+
15+
/// The result of an inbound ECIES channel establishment.
16+
#[wasm_bindgen(getter_with_clone)]
17+
pub struct InboundCreationResult {
18+
/// The established ECIES channel.
19+
pub channel: EstablishedEcies,
20+
/// The plaintext of the initial message.
21+
pub message: String,
22+
}
23+
24+
/// The result of an outbound ECIES channel establishment.
25+
#[wasm_bindgen(getter_with_clone)]
26+
pub struct OutboundCreationResult {
27+
/// The established ECIES channel.
28+
pub channel: EstablishedEcies,
29+
/// The initial encrypted message.
30+
pub initial_message: String,
31+
}
32+
33+
/// An unestablished ECIES session.
34+
#[wasm_bindgen]
35+
pub struct Ecies {
36+
inner: Option<ecies::Ecies>,
37+
public_key: Curve25519PublicKey,
38+
}
39+
40+
#[wasm_bindgen]
41+
impl Ecies {
42+
/// Create a new, random, unestablished ECIES session.
43+
///
44+
/// This method will use the `MATRIX_QR_CODE_LOGIN` info.
45+
#[wasm_bindgen(constructor)]
46+
pub fn new() -> Self {
47+
let inner = ecies::Ecies::new();
48+
let public_key = inner.public_key().into();
49+
50+
Self { inner: Some(inner), public_key }
51+
}
52+
53+
/// Get our [`Curve25519PublicKey`].
54+
///
55+
/// This public key needs to be sent to the other side to be able to
56+
/// establish an ECIES channel.
57+
pub fn public_key(&self) -> Curve25519PublicKey {
58+
self.public_key.clone()
59+
}
60+
61+
fn used_up_error() -> JsError {
62+
JsError::new("The ECIES channel was already established and used up.")
63+
}
64+
65+
/// Create a [`EstablishedEcies`] from an initial message encrypted by the
66+
/// other side.
67+
pub fn establish_inbound_channel(
68+
&mut self,
69+
initial_message: &str,
70+
) -> Result<InboundCreationResult, JsError> {
71+
let message = ecies::InitialMessage::decode(&initial_message)?;
72+
let result = self
73+
.inner
74+
.take()
75+
.ok_or_else(Self::used_up_error)?
76+
.establish_inbound_channel(&message)?;
77+
78+
let message = String::from_utf8_lossy(&result.message).to_string();
79+
80+
Ok(InboundCreationResult { message, channel: result.ecies.into() })
81+
}
82+
83+
/// Create an [`EstablishedEcies`] session using the other side's Curve25519
84+
/// public key and an initial plaintext.
85+
///
86+
/// After the channel has been established, we can encrypt messages to send
87+
/// to the other side. The other side uses the initial message to
88+
/// establishes the same channel on its side.
89+
pub fn establish_outbound_channel(
90+
&mut self,
91+
public_key: &Curve25519PublicKey,
92+
initial_message: &str,
93+
) -> Result<OutboundCreationResult, JsError> {
94+
let result = self
95+
.inner
96+
.take()
97+
.ok_or_else(Self::used_up_error)?
98+
.establish_outbound_channel(public_key.inner, initial_message.as_bytes())?;
99+
100+
Ok(OutboundCreationResult {
101+
initial_message: result.message.encode(),
102+
channel: result.ecies.into(),
103+
})
104+
}
105+
}
106+
107+
/// An established ECIES session.
108+
///
109+
/// This session can be used to encrypt and decrypt messages between the two
110+
/// sides of the channel.
111+
#[derive(Clone)]
112+
#[wasm_bindgen]
113+
pub struct EstablishedEcies {
114+
inner: Arc<Mutex<ecies::EstablishedEcies>>,
115+
}
116+
117+
#[wasm_bindgen]
118+
impl EstablishedEcies {
119+
/// Get our [`Curve25519PublicKey`].
120+
///
121+
/// This public key needs to be sent to the other side so that it can
122+
/// complete the ECIES channel establishment.
123+
pub fn public_key(&self) -> Curve25519PublicKey {
124+
self.inner.lock().unwrap().public_key().into()
125+
}
126+
127+
/// Encrypt the given plaintext using this [`EstablishedEcies`] session.
128+
pub fn encrypt(&mut self, message: &str) -> String {
129+
self.inner.lock().unwrap().encrypt(message.as_bytes()).encode()
130+
}
131+
132+
/// Decrypt the given message using this [`EstablishedEcies`] session.
133+
pub fn decrypt(&mut self, message: &str) -> Result<String, JsError> {
134+
let message = ecies::Message::decode(message)?;
135+
let result = self.inner.lock().unwrap().decrypt(&message)?;
136+
137+
Ok(String::from_utf8_lossy(&result).to_string())
138+
}
139+
140+
/// Get the [`CheckCode`] which uniquely identifies this
141+
/// [`EstablishedEcies`] session.
142+
///
143+
/// This check code can be used to check that both sides of the session are
144+
/// indeed using the same shared secret.
145+
pub fn check_code(&self) -> CheckCode {
146+
self.inner.lock().unwrap().check_code().into()
147+
}
148+
}
149+
150+
/// A check code that can be used to confirm that two [`EstablishedEcies`]
151+
/// objects share the same secret. This is supposed to be shared out-of-band to
152+
/// protect against active MITM attacks.
153+
///
154+
/// Since the initiator device can always tell whether a MITM attack is in
155+
/// progress after channel establishment, this code technically carries only a
156+
/// single bit of information, representing whether the initiator has determined
157+
/// that the channel is "secure" or "not secure".
158+
///
159+
/// However, given this will need to be interactively confirmed by the user,
160+
/// there is risk that the user would confirm the dialogue without paying
161+
/// attention to its content. By expanding this single bit into a deterministic
162+
/// two-digit check code, the user is forced to pay more attention by having to
163+
/// enter it instead of just clicking through a dialogue.
164+
#[derive(Clone)]
165+
#[wasm_bindgen]
166+
pub struct CheckCode {
167+
inner: matrix_sdk_crypto::vodozemac::ecies::CheckCode,
168+
}
169+
170+
#[wasm_bindgen]
171+
impl CheckCode {
172+
/// Convert the check code to an array of two bytes.
173+
///
174+
/// The bytes can be converted to a more user-friendly representation. The
175+
/// [`CheckCode::to_digit`] converts the bytes to a two-digit number.
176+
pub fn as_bytes(&self) -> Vec<u8> {
177+
self.inner.as_bytes().to_vec()
178+
}
179+
180+
/// Convert the check code to two base-10 numbers.
181+
///
182+
/// The number should be displayed with a leading 0 in case the first digit
183+
/// is a 0.
184+
///
185+
/// # Examples
186+
///
187+
/// ```no_run
188+
/// # use vodozemac::ecies::CheckCode;
189+
/// # let check_code: CheckCode = unimplemented!();
190+
/// let check_code = check_code.to_digit();
191+
///
192+
/// println!("The check code of the IECS channel is: {check_code:02}");
193+
/// ```
194+
pub fn to_digit(&self) -> u8 {
195+
self.inner.to_digit()
196+
}
197+
}
198+
199+
impl From<&ecies::CheckCode> for CheckCode {
200+
fn from(value: &matrix_sdk_crypto::vodozemac::ecies::CheckCode) -> Self {
201+
Self { inner: value.clone() }
202+
}
203+
}
204+
205+
impl From<ecies::EstablishedEcies> for EstablishedEcies {
206+
fn from(value: ecies::EstablishedEcies) -> Self {
207+
Self { inner: Mutex::new(value).into() }
208+
}
209+
}

src/vodozemac/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use wasm_bindgen::prelude::*;
55

66
use crate::impl_from_to_inner;
77

8+
pub mod ecies;
9+
810
/// An Ed25519 public key, used to verify digital signatures.
911
#[wasm_bindgen]
1012
#[derive(Debug, Clone)]

tests/ecies.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const { Ecies } = require("../pkg/matrix_sdk_crypto_wasm");
2+
3+
describe(Ecies.name, () => {
4+
test("can establish a channel and decrypt the initial message", () => {
5+
const alice = new Ecies();
6+
const bob = new Ecies();
7+
8+
const { initial_message, channel: alice_established } = alice.establish_outbound_channel(
9+
bob.public_key(),
10+
"It's a secret to everybody",
11+
);
12+
13+
const { message, channel } = bob.establish_inbound_channel(initial_message);
14+
expect(message).toStrictEqual("It's a secret to everybody");
15+
16+
const alice_check = alice_established.check_code();
17+
const bob_check = channel.check_code();
18+
19+
expect(alice_check.as_bytes()).toStrictEqual(bob_check.as_bytes());
20+
expect(alice_check.to_digit()).toStrictEqual(bob_check.to_digit());
21+
22+
const ciphertext = channel.encrypt("Other message");
23+
const second_plaintext = alice_established.decrypt(ciphertext);
24+
25+
expect(message).toStrictEqual("It's a secret to everybody");
26+
expect(second_plaintext).toStrictEqual("Other message");
27+
});
28+
});

0 commit comments

Comments
 (0)