use data_encoding::BASE32;
use pbkdf2::pbkdf2;
use rand::prelude::*;
use salsa20::{
stream_cipher::{generic_array::GenericArray, NewStreamCipher, SyncStreamCipher},
XSalsa20,
};
use sha2::{Digest, Sha256};
use std::{fs::File, io::prelude::*, path::Path};
type HmacSha256 = hmac::Hmac<sha2::Sha256>;
#[derive(Debug)]
pub struct ThreemaBackup {
pub threema_id: [u8; 8],
pub secret_key: [u8; 32],
}
pub fn import(backup: &String, password: &str) -> Result<ThreemaBackup, &'static str> {
let backup_trimmed = backup.replace('-', "");
if let Ok(backup_decoded) = BASE32.decode(backup_trimmed.as_bytes()) {
let salt = &backup_decoded[..8];
let mut key: [u8; 32] = [0; 32];
pbkdf2::<HmacSha256>(password.as_bytes(), &salt, 100000, &mut key);
let mut cipher = XSalsa20::new(
GenericArray::from_slice(&key),
GenericArray::from_slice(&[0u8; 24]),
);
let mut ciphertext = Vec::from(&backup_decoded[8..]);
cipher.apply_keystream(ciphertext.as_mut_slice());
let mut hasher = Sha256::new();
hasher.input(&ciphertext[0..40]);
if &ciphertext[40..42] == &hasher.result()[..2] {
let mut threema_id = [0u8; 8];
threema_id.copy_from_slice(&ciphertext[..8]);
let mut secret_key = [0u8; 32];
secret_key.copy_from_slice(&ciphertext[8..40]);
Ok(ThreemaBackup {
threema_id,
secret_key,
})
} else {
Err("Checksum incorrect.")
}
} else {
Err("Backup invalid.")
}
}
pub fn export(
threema_id: &[u8],
secret_key: &[u8],
password: &str,
) -> Result<String, &'static str> {
if threema_id.len() >= 8 && secret_key.len() >= 32 {
let mut backup = [0u8; 42];
&backup[..8].copy_from_slice(&threema_id[..8]);
&backup[8..40].copy_from_slice(&secret_key[..32]);
let mut hasher = Sha256::new();
hasher.input(&backup[..40]);
&backup[40..].copy_from_slice(&hasher.result()[..2]);
let mut salt = [0u8; 8];
thread_rng().fill_bytes(&mut salt);
let mut key: [u8; 32] = [0; 32];
pbkdf2::<HmacSha256>(password.as_bytes(), &salt, 100000, &mut key);
let mut cipher = XSalsa20::new(
GenericArray::from_slice(&key),
GenericArray::from_slice(&[0u8; 24]),
);
cipher.apply_keystream(&mut backup);
let mut cipher_bytes = [0u8; 50];
&cipher_bytes[..8].copy_from_slice(&salt);
&cipher_bytes[8..].copy_from_slice(&backup);
let mut backup_result = BASE32.encode(&cipher_bytes);
backup_result.reserve(19);
for i in 0..19 {
backup_result.insert(4 + i * 5, '-');
}
Ok(backup_result)
} else {
Err("Either ID or secret key too short.")
}
}
pub fn import_from_file(path: &Path, password: &str) -> Result<ThreemaBackup, &'static str> {
match File::open(path) {
Ok(mut file) => {
let mut backup = [0u8; 99];
if let Err(_) = file.read_exact(&mut backup) {
Err("Could not read file.")
} else {
match std::str::from_utf8(&backup) {
Ok(backup_string) => import(&backup_string.to_string(), &password),
_ => Err("Could not decode file."),
}
}
}
Err(_) => Err("Could not open file."),
}
}
pub fn export_to_file(
path: &Path,
threema_id: &[u8],
secret_key: &[u8],
password: &str,
) -> Result<(), &'static str> {
match File::create(path) {
Ok(mut file) => match export(threema_id, secret_key, password) {
Ok(backup) => {
if let Err(_) = file.write(&backup.as_bytes()) {
Err("Could not write to file.")
} else {
Ok(())
}
}
Err(e) => Err(e),
},
_ => Err("Could not create/open file."),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_import_export() {
let backup = export(&[0; 8], &[0; 32], "password").unwrap();
let reimport = import(&backup, "password").unwrap();
assert_eq!(reimport.threema_id, [0u8; 8]);
assert_eq!(reimport.secret_key, [0u8; 32]);
}
#[test]
fn test_import_export_file() {
let path = Path::new("./tmp.txt");
export_to_file(path, &[0; 8], &[0; 32], "password").expect("Export failed");
let backup = import_from_file(path, "password").expect("Import failed");
assert_eq!(backup.threema_id, [0u8; 8]);
assert_eq!(backup.secret_key, [0u8; 32]);
std::fs::remove_file(path).expect("Could not remove temporary file tmp.txt");
}
}