//! This scheme uses PBKDF2 for the key derivation and XSalsa20 unauthenticated encryption with a custom //! integrity check. //! //! The encrypted backup is a grouped base32 encoded value, such as: //! //! 4K4M-5Q6T-KFUH-KHL5-2VCJ-ZM57-NL7R-WJTA-V45L-NJAM- //! WLEU-5DS4-XF7S-OPH4-CTCL-N2CF-3C4C-HPB7-YZWW-U3S6 //! //! (without line breaks) use libthreema_macros::concat_fixed_bytes; use subtle::ConstantTimeEq as _;
usesuper::{BackupKey, IdentityBackupData, IdentityBackupError, Salt}; usecrate::crypto::{
cipher::{KeyIvInit as _, StreamCipher as _},
deprecated::{pbkdf2::pbkdf2_hmac_array, salsa20::XSalsa20},
digest::Digest as _,
sha2::Sha256,
};
/// Length of the encrypted ID backup after stripping extra characters and decoding Base32. /// /// ```text /// salt || Salsa20(Threema ID || CK || hash) /// ``` pub(super) const ENCRYPTED_LENGTH: usize = Salt::LENGTH + IdentityBackupData::LENGTH + HASH_LENGTH;
/// Return the first two bytes of the SHA256 hash of the concatenation of the Threema ID and the client key. fn get_digest(data: &[u8]) -> [u8; HASH_LENGTH] { let hash = Sha256::new().chain_update(data).finalize();
hash.get(..HASH_LENGTH)
.expect("SHA-256 hash should have at least 2 bytes")
.try_into()
.expect("SHA-256 hash should have at least 2 bytes")
}
// Encrypt backup data let salt = Salt::random(); let key = derive_key(password, salt);
// XOR with keystream
XSalsa20::new(&key.0.into(), &NONCE.into()).apply_keystream(&mut backup_data);
// Prepend the salt to the ciphertext
concat_fixed_bytes!(salt.0, backup_data)
}
pub(super) fn decrypt(
password: &str, mut encrypted_backup: Vec<u8>,
) -> Result<IdentityBackupData, IdentityBackupError> { // Extract salt and encrypted data let (salt, encrypted_data) = { if encrypted_backup.len() != ENCRYPTED_LENGTH { return Err(IdentityBackupError::DecodingFailed( "Invalid length for legacy backup",
));
}
(
Salt::try_from(
encrypted_backup
.get(..Salt::LENGTH)
.expect("Unable to extract salt from encrypted backup"),
)?,
encrypted_backup
.get_mut(Salt::LENGTH..Salt::LENGTH + IdentityBackupData::LENGTH + HASH_LENGTH)
.expect("Unable to extract encrypted data from encrypted backup"),
)
};
// Decrypt the backup with the extracted salt and hardcoded nonce let decrypted_data = { let key = derive_key(password, salt);
XSalsa20::new(&key.0.into(), &NONCE.into()).apply_keystream(encrypted_data);
encrypted_data
}; let (backup_data, extracted_hash) = (
decrypted_data
.get(..IdentityBackupData::LENGTH)
.expect("Unable to extract backup data"),
decrypted_data
.get(IdentityBackupData::LENGTH..)
.expect("Unable to extract backup data integrity hash"),
);
// Verify backup data integrity let computed_hash = get_digest(backup_data); if bool::from(computed_hash.ct_ne(extracted_hash)) { return Err(IdentityBackupError::DecryptionFailed);
}
// Decode backup data let backup_data = IdentityBackupData::decode(backup_data)?;
// Done
Ok(backup_data)
}
#[cfg(feature = "slow_tests")] #[cfg(test)] mod tests { use assert_matches::assert_matches;
#[test] fn 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 decrypted_backup = decrypt(PASSWORD, decode_chunked_base32(encrypted_backup)?)?;
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.