//! Implementation of the Threema ID Backup use core::str;
use data_encoding::BASE32; use libthreema_macros::concat_fixed_bytes; use rand::RngCore as _; use zeroize::ZeroizeOnDrop;
usecrate::common::{ThreemaId, keys::ClientKey};
mod argon_chacha_poly_scheme; mod legacy_scheme;
/// An error occurred while encrypting/decrypting an identity backup /// /// Note: Errors can occur when using the API incorrectly or when the passed encrypted backups are /// invalid. /// /// When encountering an error: /// /// 1. Let `error` be the provided [`IdentityBackupError`]. /// 2. Abort the encryption/decryption due to `error`. #[derive(Debug, thiserror::Error)] #[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))] pubenum IdentityBackupError { /// Invalid parameter provided by foreign code. #[cfg(feature = "uniffi")] #[error("Invalid parameter: {0}")]
InvalidParameter(&'static str),
/// The decoding failed. #[error("Decoding failed: {0}")]
DecodingFailed(&'static str),
/// The decrypted backup scheme version is unknown. #[error("Unknown backup scheme version: {0}")]
UnknownVersion(u8),
/// The key derivation failed. This is most likely an internal error. #[error("Key derivation failed")]
KdfFailed,
/// The decryption failed, either due to an internal error or due to an invalid ciphertext. #[error("Decryption failed")]
DecryptionFailed,
/// The encryption failed due to an internal error. #[error("Encryption failed")]
EncryptionFailed,
}
/// All information that is stored or can be derived from the ID backup. #[derive(Debug)] pubstruct IdentityBackupData { /// The user's identity. pub threema_id: ThreemaId,
/// Encode the encrypted backup with Base32 and then separate every 4th character by a dash for /// better readability, i.e., "ABCDEFGH..." becomes "ABCD-EFGH..." fn encode_chunked_base32(encrypted_backup: &[u8]) -> String { letmut chunks = String::with_capacity(encrypted_backup.len().saturating_mul(2)); for (position, chunk) in BASE32.encode(encrypted_backup).as_bytes().chunks(4).enumerate() { if position != 0 { // Prepend divider
chunks.push('-');
}
chunks.push_str(str::from_utf8(chunk).expect("Base32 should be ASCII"));
}
chunks
}
/// Strip the extra characters that were added for readability and then decode the encrypted backup /// with Base32. fn decode_chunked_base32(encrypted_backup: &str) -> Result<Vec<u8>, IdentityBackupError> {
BASE32
.decode(
encrypted_backup
.chars()
.filter(|char| *char != '-')
.collect::<String>()
.as_bytes(),
)
.map_err(|_| IdentityBackupError::DecodingFailed("Base32 decoding failed"))
}
/// Encrypt an [`IdentityBackupData`] with the provided password. /// /// This automatically uses the best available encryption scheme. /// /// # Errors /// /// Returns [`IdentityBackupError`] if the backup data could not be encrypted, most likely due to an internal /// error. #[expect(
clippy::unnecessary_wraps,
reason = "Result needed when switching to new scheme"
)] pubfn encrypt_identity_backup(
password: &str,
backup_data: &IdentityBackupData,
) -> Result<String, IdentityBackupError> { // Encrypt using the legacy scheme for now let encrypted_backup = legacy_scheme::encrypt(password, backup_data);
// Encode the encrypted backup with Base32 and then separate every 4th character by a dash for // better readability, i.e., "ABCDEFGH..." becomes "ABCD-EFGH..."
Ok(encode_chunked_base32(&encrypted_backup))
}
/// Decrypt the encrypted backup from the provided password. /// /// This detects the used backup scheme and handles it accordingly. /// /// # Errors /// /// Returns [`IdentityBackupError`] if the backup data could not be decrypted, decoded/parsed or /// otherwise contained invalid data. pubfn decrypt_identity_backup(
password: &str,
encrypted_backup: &str,
) -> Result<(BackupVersion, IdentityBackupData), IdentityBackupError> { // All variants use Base32 with every 4th character separated by a dash, so decode it first. let encrypted_backup = decode_chunked_base32(encrypted_backup)?;
// Determine the scheme and decrypt if encrypted_backup.len() == legacy_scheme::ENCRYPTED_LENGTH { let backup_data = legacy_scheme::decrypt(password, encrypted_backup)?;
Ok((BackupVersion::Legacy, backup_data))
} else { let version = encrypted_backup
.first()
.ok_or(IdentityBackupError::DecodingFailed("Encrypted backup empty"))?; let version =
BackupVersion::from_repr(*version).ok_or(IdentityBackupError::UnknownVersion(*version))?; let backup_data = match version {
BackupVersion::Legacy => Err(IdentityBackupError::DecodingFailed( "Unexpected legacy version in backup",
)),
BackupVersion::ArgonChachaPolyV1 => argon_chacha_poly_scheme::decrypt(password, encrypted_backup),
}?;
Ok((version, backup_data))
}
}
#[cfg(feature = "slow_tests")] #[cfg(test)] mod tests { use assert_matches::assert_matches; use data_encoding::HEXLOWER;
usesuper::*;
const PASSWORD: &str = "ThisIsABadPassword";
pub(crate) fn backup_data() -> IdentityBackupData { let threema_id = ThreemaId::try_from("0ZAHXXHB").expect("Threema ID should be valid"); let client_key = { let client_key = HEXLOWER
.decode(b"255d619ebec82341a5abe0b3ff736f900faa3eda1cb86b34f102394f86a41b2c")
.unwrap(); let client_key: [u8; ClientKey::LENGTH] = client_key.as_slice().try_into().unwrap();
ClientKey::from(client_key)
};
IdentityBackupData {
threema_id,
client_key,
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.