Eine aufbereitete Darstellung der Quelle

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

Benutzer

Quelle  mod.rs   Sprache: Rust

 

//! Implementation of the Threema ID Backup.
use core::str;

use libthreema_dependencies::{
    data_encoding::BASE32,
    rand::{self, RngCore as _},
};
use libthreema_macros::concat_fixed_bytes;
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 {
    /// Some provided parameter could not be converted into the required internal representation.
    #[cfg(feature = "uniffi")]
    #[error("Conversion error: {0}")]
    ConversionError(String),

    /// 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 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,
}
#[cfg(feature = "uniffi")]
impl From<crate::bindings::uniffi::ConversionError> for IdentityBackupError {
    fn from(error: crate::bindings::uniffi::ConversionError) -> Self {
        Self::ConversionError(error.to_string())
    }
}

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

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

    fn random() -> Self {
        let mut res = [0; Self::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 identity: 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(Self {
            identity: threema_id,
            client_key: ClientKey::from(client_key),
        })
    }

    fn encode(&self) -> [u8; Self::LENGTH] {
        concat_fixed_bytes!(self.identity.to_bytes(), *self.client_key.as_bytes())
    }
}
#[cfg(test)]
impl PartialEq for IdentityBackupData {
    fn eq(&self, other: &Self) -> bool {
        self.identity == other.identity && 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 libthreema_dependencies::data_encoding::HEXLOWER_PERMISSIVE;

    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_PERMISSIVE
                .decode(b"255d619ebec82341a5abe0b3ff736f900faa3eda1cb86b34f102394f86a41b2c")
                .unwrap();
            let client_key: [u8; ClientKey::LENGTH] = client_key.as_slice().try_into().unwrap();
            ClientKey::from(client_key)
        };
        IdentityBackupData {
            identity: 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.identity, backup_data.identity);
        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.identity, backup_data.identity);
        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.identity, backup_data.identity);
        assert_eq!(
            decrypted_backup.client_key.as_bytes(),
            backup_data.client_key.as_bytes(),
        );

        Ok(())
    }
}

Messung
CHZG
9110010096

¤ Dauer der Verarbeitung: 0.7 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover


Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

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.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=1104857
#Domains=439037