vodozemac/utilities/
libolm_compat.rs

1// Copyright 2021 Damir Jelić
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[cfg(feature = "libolm-compat")]
16use std::io::Cursor;
17
18#[cfg(feature = "libolm-compat")]
19use matrix_pickle::{Decode, Encode};
20#[cfg(feature = "libolm-compat")]
21use zeroize::{Zeroize, ZeroizeOnDrop};
22
23#[cfg(feature = "libolm-compat")]
24use super::{base64_decode, base64_encode};
25#[cfg(feature = "libolm-compat")]
26use crate::{LibolmPickleError, cipher::Cipher};
27
28/// Fetch the pickle version from the given pickle source.
29pub(crate) fn get_version(source: &[u8]) -> Option<u32> {
30    // Pickle versions are always u32 encoded as a fixed sized integer in
31    // big endian encoding.
32    let version = source.get(0..4)?;
33    Some(u32::from_be_bytes(version.try_into().ok()?))
34}
35
36/// Decrypt and decode the given pickle with the given pickle key.
37///
38/// # Arguments
39///
40/// * pickle - The base64-encoded and encrypted libolm pickle string
41/// * pickle_key - The key that was used to encrypt the libolm pickle
42/// * pickle_version - The expected version of the pickle. Unpickling will fail
43///   if the version in the pickle doesn't match this one.
44#[cfg(feature = "libolm-compat")]
45pub(crate) fn unpickle_libolm<P: Decode, T: TryFrom<P, Error = LibolmPickleError>>(
46    pickle: &str,
47    pickle_key: &[u8],
48    pickle_version: u32,
49) -> Result<T, LibolmPickleError> {
50    // libolm pickles are always base64 encoded, so first try to decode.
51    let decoded = base64_decode(pickle)?;
52
53    // The pickle is always encrypted, even if a zero key is given. Try to
54    // decrypt next.
55    let cipher = Cipher::new_pickle(pickle_key);
56    let mut decrypted = cipher.decrypt_pickle(&decoded)?;
57
58    // A pickle starts with a version, which will decide how we need to decode.
59    // We only support the latest version so bail out if it isn't the expected
60    // pickle version.
61    let version = get_version(&decrypted).ok_or(LibolmPickleError::MissingVersion)?;
62
63    if version == pickle_version {
64        let mut cursor = Cursor::new(&decrypted);
65        let pickle = P::decode(&mut cursor)?;
66
67        decrypted.zeroize();
68        pickle.try_into()
69    } else {
70        Err(LibolmPickleError::Version(pickle_version, version))
71    }
72}
73
74#[cfg(feature = "libolm-compat")]
75pub(crate) fn pickle_libolm<P>(pickle: P, pickle_key: &[u8]) -> Result<String, LibolmPickleError>
76where
77    P: Encode,
78{
79    let mut encoded = pickle.encode_to_vec()?;
80
81    let cipher = Cipher::new_pickle(pickle_key);
82    let encrypted = cipher.encrypt_pickle(&encoded);
83    encoded.zeroize();
84
85    Ok(base64_encode(encrypted))
86}
87
88#[cfg(feature = "libolm-compat")]
89#[derive(Encode, Decode, Zeroize, ZeroizeOnDrop)]
90pub(crate) struct LibolmEd25519Keypair {
91    pub public_key: [u8; 32],
92    #[secret]
93    pub private_key: Box<[u8; 64]>,
94}
95
96#[cfg(all(feature = "libolm-compat", test))]
97mod test {
98    use super::*;
99
100    #[test]
101    fn encode_cycle() {
102        let key_pair =
103            LibolmEd25519Keypair { public_key: [10u8; 32], private_key: [20u8; 64].into() };
104
105        let encoded = key_pair.encode_to_vec().unwrap();
106        let decoded = LibolmEd25519Keypair::decode_from_slice(&encoded).unwrap();
107
108        assert_eq!(key_pair.public_key, decoded.public_key);
109        assert_eq!(key_pair.private_key, decoded.private_key);
110    }
111}