Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  mod.rs   Sprache: unbekannt

 
Spracherkennung für: .rs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

//! 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;

use crate::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))]
pub enum 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,
}

#[derive(ZeroizeOnDrop)]
struct BackupKey([u8; BackupKey::LENGTH]);
impl BackupKey {
    const LENGTH: usize = 32;
}

#[derive(Clone, Copy)]
struct Salt([u8; Salt::LENGTH]);
impl Salt {
    const LENGTH: usize = 8;

    fn random() -> Self {
        let mut res = [0; Salt::LENGTH];
        rand::thread_rng().fill_bytes(&mut res);
        Self(res)
    }
}
impl TryFrom<&[u8]> for Salt {
    type Error = IdentityBackupError;

    fn try_from(salt: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self(salt.try_into().map_err(|_| {
            IdentityBackupError::DecodingFailed("Invalid salt length")
        })?))
    }
}

/// ID backup version
#[derive(Debug, PartialEq, Eq, strum::FromRepr)]
#[repr(u8)]
pub enum BackupVersion {
    /// Legacy PBKDF2 scheme using XSalsa20 and a custom integrity hash
    Legacy = 0x00,

    /// Argon2id ChaCha20Poly1305 scheme V1
    ArgonChachaPolyV1 = 0x01,
}

/// All information that is stored or can be derived from the ID backup.
#[derive(Debug)]
pub struct IdentityBackupData {
    /// The user's identity.
    pub threema_id: ThreemaId,

    /// Client key.
    pub client_key: ClientKey,
}
impl IdentityBackupData {
    // Encoded length
    const LENGTH: usize = ThreemaId::LENGTH + ClientKey::LENGTH;

    fn decode(backup_data: &[u8]) -> Result<Self, IdentityBackupError> {
        let (threema_id, client_key) = backup_data
            .split_at_checked(ThreemaId::LENGTH)
            .ok_or(IdentityBackupError::DecodingFailed("Invalid backup data length"))?;
        let threema_id = ThreemaId::try_from(threema_id).map_err(|_| {
            IdentityBackupError::DecodingFailed("Invalid Threema ID in decrypted backup data")
        })?;
        let client_key = <[u8; ClientKey::LENGTH]>::try_from(client_key)
            .map_err(|_| IdentityBackupError::DecodingFailed("Invalid CK length in decrypted backup data"))?;
        Ok(IdentityBackupData {
            threema_id,
            client_key: ClientKey::from(client_key),
        })
    }

    fn encode(&self) -> [u8; Self::LENGTH] {
        concat_fixed_bytes!(self.threema_id.to_bytes(), *self.client_key.as_bytes())
    }
}

#[cfg(test)]
impl PartialEq for IdentityBackupData {
    fn eq(&self, other: &Self) -> bool {
        self.threema_id == other.threema_id && self.client_key.as_bytes() == other.client_key.as_bytes()
    }
}

/// 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 {
    let mut 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"
)]
pub fn 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.
pub fn 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;

    use super::*;

    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,
        }
    }

    #[test]
    fn invalid_base32() {
        assert_matches!(
            decrypt_identity_backup(
                PASSWORD,
                "4K4M-5Q6T-KFUH-KHL5-2VCJ-ZM57-NL7R-WJTA-V45L-NJAM-\
                WLEU-5DS4-XF7S-OPH4-CTCL-N2CF-3C4C-HPB7-YZWW-U3S"
            ),
            Err(IdentityBackupError::DecodingFailed(_))
        );
    }

    #[test]
    fn empty() {
        assert_matches!(
            decrypt_identity_backup(PASSWORD, ""),
            Err(IdentityBackupError::DecodingFailed(_))
        );
    }

    #[test]
    fn unexpected_explicit_legacy_version() {
        assert_matches!(
            decrypt_identity_backup(PASSWORD, "AD77-7777"),
            Err(IdentityBackupError::DecodingFailed(_))
        );
    }

    #[test]
    fn unknown_version() {
        assert_matches!(
            decrypt_identity_backup(PASSWORD, "7777-7777"),
            Err(IdentityBackupError::UnknownVersion(0xff))
        );
    }

    #[test]
    fn default_scheme_roundtrip() -> anyhow::Result<()> {
        let backup_data = backup_data();

        let encrypted_backup = encrypt_identity_backup(PASSWORD, &backup_data)?;
        let (backup_version, decrypted_backup) = decrypt_identity_backup(PASSWORD, &encrypted_backup)?;

        assert_eq!(backup_version, BackupVersion::Legacy);
        assert_eq!(decrypted_backup.threema_id, backup_data.threema_id);
        assert_eq!(
            decrypted_backup.client_key.as_bytes(),
            backup_data.client_key.as_bytes(),
        );

        Ok(())
    }

    #[test]
    fn legacy_static_decrypt() -> anyhow::Result<()> {
        let backup_data = backup_data();

        let encrypted_backup = "4K4M-5Q6T-KFUH-KHL5-2VCJ-ZM57-NL7R-WJTA-V45L-NJAM-\
            WLEU-5DS4-XF7S-OPH4-CTCL-N2CF-3C4C-HPB7-YZWW-U3S6";
        let (backup_version, decrypted_backup) = decrypt_identity_backup("testpassword", encrypted_backup)?;

        assert_eq!(backup_version, BackupVersion::Legacy);
        assert_eq!(decrypted_backup.threema_id, backup_data.threema_id);
        assert_eq!(
            decrypted_backup.client_key.as_bytes(),
            backup_data.client_key.as_bytes(),
        );

        Ok(())
    }

    #[test]
    fn argon_chacha_poly_static_decrypt() -> anyhow::Result<()> {
        let backup_data = backup_data();

        let encrypted_backup = "AHCV-YVN5-MZF6-H47E-BFDA-XPQ4-523T-QEJ7-Q7TB-5O2G-U3LM-IPAY-PMO3-\
            RWYJ-FZ5F-VRAH-5MHT-IP2E-ODYI-GXW4-4NAF-EOCO-OZPR-NZK4-CHVE-LYVL";
        let (backup_version, decrypted_backup) = decrypt_identity_backup(PASSWORD, encrypted_backup)?;

        assert_eq!(backup_version, BackupVersion::ArgonChachaPolyV1);
        assert_eq!(decrypted_backup.threema_id, backup_data.threema_id);
        assert_eq!(
            decrypted_backup.client_key.as_bytes(),
            backup_data.client_key.as_bytes(),
        );

        Ok(())
    }
}

[Dauer der Verarbeitung: 0.22 Sekunden, vorverarbeitet 2026-04-27]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge