308 lines
11 KiB
Rust
308 lines
11 KiB
Rust
// Copyright 2021 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
use std::{
|
|
io::{Cursor, Read},
|
|
ops::DerefMut,
|
|
};
|
|
|
|
use bs58;
|
|
use thiserror::Error;
|
|
use zeroize::Zeroizing;
|
|
|
|
use super::{
|
|
compat::{Error as DecryptionError, Message, PkDecryption},
|
|
MegolmV1BackupKey,
|
|
};
|
|
use crate::store::RecoveryKey;
|
|
|
|
/// Error type for the decoding of a RecoveryKey.
|
|
#[derive(Debug, Error)]
|
|
pub enum DecodeError {
|
|
/// The decoded recovery key has an invalid prefix.
|
|
#[error("The decoded recovery key has an invalid prefix: expected {0:?}, got {1:?}")]
|
|
Prefix([u8; 2], [u8; 2]),
|
|
/// The parity byte of the recovery key didn't match.
|
|
#[error("The parity byte of the recovery key doesn't match: expected {0:?}, got {1:?}")]
|
|
Parity(u8, u8),
|
|
/// The recovery key has an invalid length.
|
|
#[error("The decoded recovery key has a invalid length: expected {0}, got {1}")]
|
|
Length(usize, usize),
|
|
/// The recovry key isn't valid base58.
|
|
#[error(transparent)]
|
|
Base58(#[from] bs58::decode::Error),
|
|
/// The recovery key isn't valid base64.
|
|
#[error(transparent)]
|
|
Base64(#[from] base64::DecodeError),
|
|
/// The recovery key is too short, we couldn't read enough data.
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
/// The recovery key, a Curve25519 public key, couldn't be decoded.
|
|
#[error(transparent)]
|
|
PublicKey(#[from] vodozemac::KeyError),
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum UnpicklingError {
|
|
#[error(transparent)]
|
|
Json(#[from] serde_json::Error),
|
|
// #[error("Couldn't decrypt the pickle: {0}")]
|
|
// Decryption(String),
|
|
#[error(transparent)]
|
|
Decode(#[from] DecodeError),
|
|
}
|
|
|
|
impl TryFrom<String> for RecoveryKey {
|
|
type Error = DecodeError;
|
|
|
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
Self::from_base58(&value)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for RecoveryKey {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let string = Zeroizing::new(self.to_base58());
|
|
|
|
let string = Zeroizing::new(
|
|
string
|
|
.chars()
|
|
.collect::<Vec<char>>()
|
|
.chunks(Self::DISPLAY_CHUNK_SIZE)
|
|
.map(|c| c.iter().collect::<String>())
|
|
.collect::<Vec<_>>()
|
|
.join(" "),
|
|
);
|
|
|
|
write!(f, "{}", string.as_str())
|
|
}
|
|
}
|
|
|
|
impl RecoveryKey {
|
|
const PREFIX: [u8; 2] = [0x8b, 0x01];
|
|
const PREFIX_PARITY: u8 = Self::PREFIX[0] ^ Self::PREFIX[1];
|
|
const DISPLAY_CHUNK_SIZE: usize = 4;
|
|
|
|
fn parity_byte(bytes: &[u8]) -> u8 {
|
|
bytes.iter().fold(Self::PREFIX_PARITY, |acc, x| acc ^ x)
|
|
}
|
|
|
|
/// Create a new recovery key from the given byte array.
|
|
///
|
|
/// **Warning**: You need to make sure that the byte array contains correct
|
|
/// random data, either by using a random number generator or by using an
|
|
/// exported version of a previously created [`RecoveryKey`].
|
|
pub fn from_bytes(key: &[u8; Self::KEY_SIZE]) -> Self {
|
|
let mut inner = Box::new([0u8; Self::KEY_SIZE]);
|
|
inner.copy_from_slice(key);
|
|
|
|
Self::from_boxed_bytes(inner)
|
|
}
|
|
|
|
fn from_boxed_bytes(key: Box<[u8; Self::KEY_SIZE]>) -> Self {
|
|
Self { inner: key }
|
|
}
|
|
|
|
/// Get the recovery key as a raw byte representation.
|
|
pub fn as_bytes(&self) -> &[u8; Self::KEY_SIZE] {
|
|
&self.inner
|
|
}
|
|
|
|
/// Try to create a [`RecoveryKey`] from a base64 export of a `RecoveryKey`.
|
|
pub fn from_base64(key: &str) -> Result<Self, DecodeError> {
|
|
let decoded = Zeroizing::new(crate::utilities::decode(key)?);
|
|
|
|
if decoded.len() != Self::KEY_SIZE {
|
|
Err(DecodeError::Length(Self::KEY_SIZE, decoded.len()))
|
|
} else {
|
|
let mut key = Box::new([0u8; Self::KEY_SIZE]);
|
|
key.copy_from_slice(&decoded);
|
|
|
|
Ok(Self::from_boxed_bytes(key))
|
|
}
|
|
}
|
|
|
|
/// Try to create a [`RecoveryKey`] from a base58 export of a `RecoveryKey`.
|
|
pub fn from_base58(value: &str) -> Result<Self, DecodeError> {
|
|
// Remove any whitespace we might have
|
|
let value: String = value.chars().filter(|c| !c.is_whitespace()).collect();
|
|
|
|
let decoded = bs58::decode(value).with_alphabet(bs58::Alphabet::BITCOIN).into_vec()?;
|
|
let mut decoded = Cursor::new(decoded);
|
|
|
|
let mut prefix = [0u8; 2];
|
|
let mut key = Box::new([0u8; Self::KEY_SIZE]);
|
|
let mut expected_parity = [0u8; 1];
|
|
|
|
decoded.read_exact(&mut prefix)?;
|
|
decoded.read_exact(key.deref_mut())?;
|
|
decoded.read_exact(&mut expected_parity)?;
|
|
|
|
let expected_parity = expected_parity[0];
|
|
let parity = Self::parity_byte(key.as_ref());
|
|
|
|
let _ = Zeroizing::new(decoded.into_inner());
|
|
|
|
if prefix != Self::PREFIX {
|
|
Err(DecodeError::Prefix(Self::PREFIX, prefix))
|
|
} else if expected_parity != parity {
|
|
Err(DecodeError::Parity(expected_parity, parity))
|
|
} else {
|
|
Ok(Self::from_boxed_bytes(key))
|
|
}
|
|
}
|
|
|
|
/// Export the `RecoveryKey` as a base58 encoded string.
|
|
pub fn to_base58(&self) -> String {
|
|
let bytes = Zeroizing::new(
|
|
[
|
|
Self::PREFIX.as_ref(),
|
|
self.inner.as_ref(),
|
|
[Self::parity_byte(self.inner.as_ref())].as_ref(),
|
|
]
|
|
.concat(),
|
|
);
|
|
|
|
bs58::encode(bytes.as_slice()).with_alphabet(bs58::Alphabet::BITCOIN).into_string()
|
|
}
|
|
|
|
fn get_pk_decrytpion(&self) -> PkDecryption {
|
|
PkDecryption::from_bytes(self.inner.as_ref())
|
|
}
|
|
|
|
/// Extract the megolm.v1 public key from this `RecoveryKey`.
|
|
pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
|
|
let pk = self.get_pk_decrytpion();
|
|
MegolmV1BackupKey::new(pk.public_key(), None)
|
|
}
|
|
|
|
/// Try to decrypt the given ciphertext using this `RecoveryKey`.
|
|
///
|
|
/// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm
|
|
/// to decrypt the given ciphertext.
|
|
///
|
|
/// [`m.megolm_backup.v1.curve25519-aes-sha2`]:
|
|
/// https://spec.matrix.org/unstable/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
|
|
pub fn decrypt_v1(
|
|
&self,
|
|
ephemeral_key: &str,
|
|
mac: &str,
|
|
ciphertext: &str,
|
|
) -> Result<String, DecryptionError> {
|
|
let message = Message::from_base64(ciphertext, mac, ephemeral_key)?;
|
|
let pk = self.get_pk_decrytpion();
|
|
|
|
let decrypted = pk.decrypt(&message)?;
|
|
|
|
Ok(String::from_utf8_lossy(&decrypted).to_string())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruma::api::client::backup::KeyBackupData;
|
|
use serde_json::json;
|
|
|
|
use super::{DecodeError, RecoveryKey};
|
|
use crate::olm::BackedUpRoomKey;
|
|
|
|
const TEST_KEY: [u8; 32] = [
|
|
0x77, 0x07, 0x6D, 0x0A, 0x73, 0x18, 0xA5, 0x7D, 0x3C, 0x16, 0xC1, 0x72, 0x51, 0xB2, 0x66,
|
|
0x45, 0xDF, 0x4C, 0x2F, 0x87, 0xEB, 0xC0, 0x99, 0x2A, 0xB1, 0x77, 0xFB, 0xA5, 0x1D, 0xB9,
|
|
0x2C, 0x2A,
|
|
];
|
|
|
|
#[test]
|
|
fn base64_decoding() -> Result<(), DecodeError> {
|
|
let key = RecoveryKey::new().expect("Can't create a new recovery key");
|
|
|
|
let base64 = key.to_base64();
|
|
let decoded_key = RecoveryKey::from_base64(&base64)?;
|
|
assert_eq!(key.inner, decoded_key.inner, "The decode key does't match the original");
|
|
|
|
RecoveryKey::from_base64("i").expect_err("The recovery key is too short");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn base58_decoding() -> Result<(), DecodeError> {
|
|
let key = RecoveryKey::new().expect("Can't create a new recovery key");
|
|
|
|
let base64 = key.to_base58();
|
|
let decoded_key = RecoveryKey::from_base58(&base64)?;
|
|
assert_eq!(key.inner, decoded_key.inner, "The decode key does't match the original");
|
|
|
|
let test_key =
|
|
RecoveryKey::from_base58("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d")?;
|
|
assert_eq!(
|
|
test_key.as_bytes(),
|
|
&TEST_KEY,
|
|
"The decoded recovery key doesn't match the test key"
|
|
);
|
|
|
|
let test_key = RecoveryKey::from_base58(
|
|
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
|
)?;
|
|
assert_eq!(
|
|
test_key.as_bytes(),
|
|
&TEST_KEY,
|
|
"The decoded recovery key doesn't match the test key"
|
|
);
|
|
|
|
RecoveryKey::from_base58("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e")
|
|
.expect_err("Can't create a recovery key if the parity byte is invalid");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_decrypt_key() {
|
|
let recovery_key =
|
|
RecoveryKey::from_base64("Ha9cklU/9NqFo9WKdVfGzmqUL/9wlkdxfEitbSIPVXw").unwrap();
|
|
|
|
let data = json!({
|
|
"first_message_index": 0,
|
|
"forwarded_count": 0,
|
|
"is_verified": false,
|
|
"session_data": {
|
|
"ephemeral": "HlLi76oV6wxHz3PCqE/bxJi6yF1HnYz5Dq3T+d/KpRw",
|
|
"ciphertext": "MuM8E3Yc6TSAvhVGb77rQ++jE6p9dRepx63/3YPD2wACKAppkZHeFrnTH6wJ/HSyrmzo\
|
|
7HfwqVl6tKNpfooSTHqUf6x1LHz+h4B/Id5ITO1WYt16AaI40LOnZqTkJZCfSPuE2oxa\
|
|
lwEHnCS3biWybutcnrBFPR3LMtaeHvvkb+k3ny9l5ZpsU9G7vCm3XoeYkWfLekWXvDhb\
|
|
qWrylXD0+CNUuaQJ/S527TzLd4XKctqVjjO/cCH7q+9utt9WJAfK8LGaWT/mZ3AeWjf5\
|
|
kiqOpKKf5Cn4n5SSil5p/pvGYmjnURvZSEeQIzHgvunIBEPtzK/MYEPOXe/P5achNGlC\
|
|
x+5N19Ftyp9TFaTFlTWCTi0mpD7ePfCNISrwpozAz9HZc0OhA8+1aSc7rhYFIeAYXFU3\
|
|
26NuFIFHI5pvpSxjzPQlOA+mavIKmiRAtjlLw11IVKTxgrdT4N8lXeMr4ndCSmvIkAzF\
|
|
Mo1uZA4fzjiAdQJE4/2WeXFNNpvdfoYmX8Zl9CAYjpSO5HvpwkAbk4/iLEH3hDfCVUwD\
|
|
fMh05PdGLnxeRpiEFWSMSsJNp+OWAA+5JsF41BoRGrxoXXT+VKqlUDONd+O296Psu8Q+\
|
|
d8/S618",
|
|
"mac": "GtMrurhDTwo"
|
|
}
|
|
});
|
|
|
|
let key_backup_data: KeyBackupData = serde_json::from_value(data).unwrap();
|
|
let ephemeral = key_backup_data.session_data.ephemeral.encode();
|
|
let ciphertext = key_backup_data.session_data.ciphertext.encode();
|
|
let mac = key_backup_data.session_data.mac.encode();
|
|
|
|
let decrypted = recovery_key
|
|
.decrypt_v1(&ephemeral, &mac, &ciphertext)
|
|
.expect("The backed up key should be decrypted successfully");
|
|
|
|
let _: BackedUpRoomKey = serde_json::from_str(&decrypted)
|
|
.expect("The decrypted payload should contain valid JSON");
|
|
}
|
|
}
|