Skip to content

Commit 9d4558d

Browse files
committed
Add PkEncryption support
1 parent 3369f99 commit 9d4558d

File tree

5 files changed

+193
-1
lines changed

5 files changed

+193
-1
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ edition = "2021"
77
name = "vodozemac"
88
crate-type = ["cdylib"]
99

10+
[features]
11+
default = ["insecure-pk-encryption"]
12+
insecure-pk-encryption = ["vodozemac/insecure-pk-encryption"]
13+
1014
[dependencies]
1115
paste = "1.0.15"
1216
thiserror = "1.0.63"
13-
vodozemac = "0.7.0"
17+
vodozemac = { git = "https://github.com/matrix-org/vodozemac.git", branch = "poljar/pk-dekurcina" }
1418

1519
[package.metadata.maturin]
1620
name = "vodozemac"

src/error.rs

+23
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,26 @@ impl From<PickleError> for PyErr {
145145
PickleException::new_err(e.to_string())
146146
}
147147
}
148+
149+
#[cfg(feature = "insecure-pk-encryption")]
150+
#[derive(Debug, Error)]
151+
pub enum PkEncryptionError {
152+
#[error("The key doesn't have the correct size, got {0}, expected 32 bytes")]
153+
InvalidKeySize(usize),
154+
#[error(transparent)]
155+
Decode(#[from] vodozemac::pk_encryption::Error),
156+
}
157+
158+
#[cfg(feature = "insecure-pk-encryption")]
159+
pyo3::create_exception!(module, PkInvalidKeySizeException, pyo3::exceptions::PyValueError);
160+
pyo3::create_exception!(module, PkDecodeException, pyo3::exceptions::PyValueError);
161+
162+
#[cfg(feature = "insecure-pk-encryption")]
163+
impl From<PkEncryptionError> for PyErr {
164+
fn from(e: PkEncryptionError) -> Self {
165+
match e {
166+
PkEncryptionError::InvalidKeySize(_) => PkInvalidKeySizeException::new_err(e.to_string()),
167+
PkEncryptionError::Decode(_) => PkDecodeException::new_err(e.to_string()),
168+
}
169+
}
170+
}

src/lib.rs

+18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
mod account;
22
mod error;
33
mod group_sessions;
4+
#[cfg(feature = "insecure-pk-encryption")]
5+
mod pk_encryption;
46
mod sas;
57
mod session;
68
mod types;
@@ -35,6 +37,12 @@ fn my_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
3537
m.add_class::<sas::Sas>()?;
3638
m.add_class::<group_sessions::GroupSession>()?;
3739
m.add_class::<group_sessions::InboundGroupSession>()?;
40+
#[cfg(feature = "insecure-pk-encryption")]
41+
m.add_class::<pk_encryption::PkDecryption>()?;
42+
#[cfg(feature = "insecure-pk-encryption")]
43+
m.add_class::<pk_encryption::PkEncryption>()?;
44+
#[cfg(feature = "insecure-pk-encryption")]
45+
m.add_class::<pk_encryption::Message>()?;
3846

3947
m.add_class::<types::Ed25519PublicKey>()?;
4048
m.add_class::<types::Curve25519PublicKey>()?;
@@ -63,6 +71,16 @@ fn my_module(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
6371
"MegolmDecryptionException",
6472
py.get_type_bound::<MegolmDecryptionException>(),
6573
)?;
74+
#[cfg(feature = "insecure-pk-encryption")]
75+
m.add(
76+
"PkInvalidKeySizeException",
77+
py.get_type_bound::<PkInvalidKeySizeException>(),
78+
)?;
79+
#[cfg(feature = "insecure-pk-encryption")]
80+
m.add(
81+
"PkDecodeException",
82+
py.get_type_bound::<PkDecodeException>(),
83+
)?;
6684

6785
Ok(())
6886
}

src/pk_encryption.rs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use pyo3::{
2+
pyclass, pymethods,
3+
types::{PyBytes, PyType},
4+
Bound, Py, Python,
5+
};
6+
use vodozemac::Curve25519PublicKey;
7+
8+
use crate::PkEncryptionError;
9+
10+
#[pyclass]
11+
pub struct Message {
12+
ciphertext: Vec<u8>,
13+
mac: Vec<u8>,
14+
ephemeral_key: Vec<u8>,
15+
}
16+
17+
/// The decryption component of the PkEncryption support.
18+
#[pyclass]
19+
pub struct PkDecryption {
20+
inner: vodozemac::pk_encryption::PkDecryption,
21+
}
22+
23+
#[pymethods]
24+
impl PkDecryption {
25+
/// Create a new random PkDecryption object.
26+
#[new]
27+
fn new() -> Self {
28+
Self {
29+
inner: vodozemac::pk_encryption::PkDecryption::new(),
30+
}
31+
}
32+
33+
/// Create a PkDecryption object from the secret key bytes.
34+
#[classmethod]
35+
fn from_bytes(_cls: &Bound<'_, PyType>, bytes: &[u8]) -> Result<Self, PkEncryptionError> {
36+
let key: &[u8; 32] = bytes
37+
.try_into()
38+
.map_err(|_| PkEncryptionError::InvalidKeySize(bytes.len()))?;
39+
40+
Ok(Self {
41+
inner: vodozemac::pk_encryption::PkDecryption::from_bytes(&key),
42+
})
43+
}
44+
45+
// The secret key used to decrypt messages.
46+
#[getter]
47+
pub fn key(&self) -> Py<PyBytes> {
48+
Python::with_gil(|py| PyBytes::new_bound(py, self.inner.to_bytes().as_slice()).into())
49+
}
50+
51+
// The public key used to encrypt messages for this decryption object.
52+
#[getter]
53+
pub fn public_key(&self) -> Py<PyBytes> {
54+
Python::with_gil(|py| PyBytes::new_bound(py, self.inner.public_key().as_bytes()).into())
55+
}
56+
57+
/// Decrypt a ciphertext. See the PkEncryption::encrypt function
58+
/// for descriptions of the ephemeral_key and mac arguments.
59+
pub fn decrypt(&self, message: &Message) -> Result<Py<PyBytes>, PkEncryptionError> {
60+
let ephemeral_key_bytes: [u8; 32] = message
61+
.ephemeral_key
62+
.as_slice()
63+
.try_into()
64+
.map_err(|_| PkEncryptionError::InvalidKeySize(message.ephemeral_key.len()))?;
65+
66+
let message = vodozemac::pk_encryption::Message {
67+
ciphertext: message.ciphertext.clone(),
68+
mac: message.mac.clone(),
69+
ephemeral_key: Curve25519PublicKey::from_bytes(ephemeral_key_bytes),
70+
};
71+
72+
self.inner
73+
.decrypt(&message)
74+
.map(|vec| Python::with_gil(|py| PyBytes::new_bound(py, vec.as_slice()).into()))
75+
.map_err(|e| PkEncryptionError::Decode(e))
76+
}
77+
}
78+
79+
/// The encryption component of PkEncryption support.
80+
#[pyclass]
81+
pub struct PkEncryption {
82+
inner: vodozemac::pk_encryption::PkEncryption,
83+
}
84+
85+
#[pymethods]
86+
impl PkEncryption {
87+
/// Create a new PkEncryption object from public key bytes.
88+
#[classmethod]
89+
fn from_public_key(_cls: &Bound<'_, PyType>, bytes: &[u8]) -> Result<Self, PkEncryptionError> {
90+
let key: &[u8; 32] = bytes
91+
.try_into()
92+
.map_err(|_| PkEncryptionError::InvalidKeySize(bytes.len()))?;
93+
94+
Ok(Self {
95+
inner: vodozemac::pk_encryption::PkEncryption::from_key(
96+
Curve25519PublicKey::from_bytes(*key),
97+
),
98+
})
99+
}
100+
101+
/// Encrypt a plaintext for the recipient. Writes to the ciphertext, mac, and
102+
/// ephemeral_key buffers, whose values should be sent to the recipient. mac is
103+
/// a Message Authentication Code to ensure that the data is received and
104+
/// decrypted properly. ephemeral_key is the public part of the ephemeral key
105+
/// used (together with the recipient's key) to generate a symmetric encryption
106+
/// key.
107+
pub fn encrypt(&self, message: &[u8]) -> Message {
108+
let msg = self.inner.encrypt(message);
109+
Message {
110+
ciphertext: msg.ciphertext.to_vec(),
111+
mac: msg.mac.to_vec(),
112+
ephemeral_key: msg.ephemeral_key.to_vec(),
113+
}
114+
}
115+
}

tests/pk_encryption_test.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import importlib
2+
import pytest
3+
4+
SKIP = False
5+
try:
6+
from vodozemac import PkEncryption, PkDecryption, PkDecodeException, PkInvalidKeySizeException
7+
except ImportError:
8+
SKIP = True
9+
10+
CLEARTEXT = b"test"
11+
12+
@pytest.mark.skipif(SKIP, reason="Can't import, feature probably disabled")
13+
class TestClass(object):
14+
def test_encrypt_decrypt(self):
15+
d = PkDecryption()
16+
e = PkEncryption.from_public_key(d.public_key)
17+
18+
decoded = d.decrypt(e.encrypt(CLEARTEXT))
19+
assert decoded == CLEARTEXT
20+
21+
def test_encrypt_decrypt_with_wrong_key(self):
22+
wrong_e = PkEncryption.from_public_key(PkDecryption().public_key)
23+
with pytest.raises(PkDecodeException, match="MAC tag mismatch"):
24+
PkDecryption().decrypt(wrong_e.encrypt(CLEARTEXT))
25+
26+
def test_encrypt_key_wrong_size(self):
27+
with pytest.raises(PkInvalidKeySizeException, match="expected 32 bytes"):
28+
PkEncryption.from_public_key(bytes())
29+
30+
def test_keys_wrong_size(self):
31+
with pytest.raises(PkInvalidKeySizeException, match="expected 32 bytes"):
32+
PkDecryption.from_bytes(bytes())

0 commit comments

Comments
 (0)