fix: chunked processing, concat sql statements with parameters

This commit is contained in:
Samuel 2024-12-24 14:20:47 +01:00
parent b3e3eb7270
commit 55e33a87b1
5 changed files with 231 additions and 292 deletions

7
Cargo.lock generated
View file

@ -182,6 +182,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.12.4" version = "0.12.4"
@ -495,6 +501,7 @@ dependencies = [
"base64", "base64",
"console_error_panic_hook", "console_error_panic_hook",
"ctr", "ctr",
"hex",
"hkdf", "hkdf",
"hmac", "hmac",
"js-sys", "js-sys",

View file

@ -22,6 +22,7 @@ prost = "0.11"
prost-types = "0.11" prost-types = "0.11"
serde_bytes = "0.11.15" serde_bytes = "0.11.15"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
hex = "0.4.3"
[build-dependencies] [build-dependencies]
prost-build = "0.11" prost-build = "0.11"

View file

@ -39,12 +39,12 @@ pub mod signal {
include!(concat!(env!("OUT_DIR"), "/signal.rs")); include!(concat!(env!("OUT_DIR"), "/signal.rs"));
} }
#[derive(Debug)] // #[derive(Debug)]
enum AttachmentType { // enum AttachmentType {
Attachment, // Attachment,
Sticker, // Sticker,
Avatar, // Avatar,
} // }
#[wasm_bindgen] #[wasm_bindgen]
pub struct DecryptionResult { pub struct DecryptionResult {
@ -90,14 +90,6 @@ impl ByteReader {
self.remaining_data().len() self.remaining_data().len()
} }
fn get_position(&self) -> usize {
self.position
}
fn set_position(&mut self, position: usize) {
self.position = position;
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
let available = self.remaining_data(); let available = self.remaining_data();
@ -139,6 +131,74 @@ struct Keys {
hmac_key: Vec<u8>, hmac_key: Vec<u8>,
} }
fn parameter_to_string(parameter: &signal::sql_statement::SqlParameter) -> Result<String, JsValue> {
if let Some(s) = &parameter.string_paramter {
Ok(format!("'{}'", s.replace("'", "''")))
} else if let Some(i) = parameter.integer_parameter {
let signed_i = if i & (1 << 63) != 0 {
i | (-1_i64 << 63) as u64
} else {
i
};
Ok(signed_i.to_string())
} else if let Some(d) = parameter.double_parameter {
Ok(d.to_string())
} else if let Some(b) = &parameter.blob_parameter {
Ok(format!("X'{}'", hex::encode(b)))
} else if parameter.nullparameter.is_some() {
Ok("NULL".to_string())
} else {
Ok("NULL".to_string())
}
}
fn process_parameter_placeholders(sql: &str, params: &[String]) -> Result<String, JsValue> {
let mut result = sql.to_string();
let mut param_index = 0;
// Handle different types of parameter placeholders
while param_index < params.len() {
let rest = &result[param_index..];
// Find the next placeholder
let next_placeholder = rest.find('?').map(|i| (i, 1)); // ? style
match next_placeholder {
Some((pos, len)) => {
// Replace the placeholder with the parameter value
if param_index < params.len() {
let before = &result[..param_index + pos];
let after = &result[param_index + pos + len..];
result = format!("{}{}{}", before, params[param_index], after);
param_index += 1;
} else {
return Err(JsValue::from_str(
"Not enough parameters provided for SQL statement",
));
}
}
None => {
// No more placeholders found
break;
}
}
}
// Check if we have unused parameters
if param_index < params.len() {
web_sys::console::warn_1(
&format!(
"Warning: {} parameters were provided but not all were used in SQL: {}",
params.len(),
sql
)
.into(),
);
}
Ok(result)
}
fn derive_keys(passphrase: &str, salt: &[u8]) -> Result<Keys, JsValue> { fn derive_keys(passphrase: &str, salt: &[u8]) -> Result<Keys, JsValue> {
let passphrase_bytes = passphrase.replace(" ", "").as_bytes().to_vec(); let passphrase_bytes = passphrase.replace(" ", "").as_bytes().to_vec();
@ -203,31 +263,18 @@ fn io_err_to_js(e: io::Error) -> JsValue {
JsValue::from_str(&format!("IO Error: {}", e)) JsValue::from_str(&format!("IO Error: {}", e))
} }
fn decrypt_frame( fn get_frame_length(
reader: &mut ByteReader, reader: &mut ByteReader,
hmac_key: &[u8], hmac: &mut HmacSha256,
cipher_key: &[u8], ctr: &mut Ctr32BE<Aes256>,
initialisation_vector: &[u8],
header_version: Option<u32>, header_version: Option<u32>,
ciphertext_buf: &mut Vec<u8>, ) -> Result<Option<u32>, JsValue> {
plaintext_buf: &mut Vec<u8>,
should_update_hmac: bool,
) -> Result<Option<signal::BackupFrame>, JsValue> {
if reader.remaining_length() < 4 { if reader.remaining_length() < 4 {
web_sys::console::log_1(&"too less data to decrypt frame length".into()); web_sys::console::log_1(&"too less data to decrypt frame length".into());
return Ok(None); // Not enough data to read the frame length return Ok(None); // Not enough data to read the frame length
} }
let initial_position = reader.get_position();
let mut hmac = <HmacSha256 as Mac>::new_from_slice(hmac_key)
.map_err(|_| JsValue::from_str("Invalid HMAC key"))?;
let mut ctr =
<Ctr32BE<Aes256> as KeyIvInit>::new_from_slices(cipher_key, initialisation_vector)
.map_err(|_| JsValue::from_str("Invalid CTR parameters"))?;
let length = match header_version { let length = match header_version {
None => { None => {
let mut length_bytes = [0u8; 4]; let mut length_bytes = [0u8; 4];
@ -245,11 +292,9 @@ fn decrypt_frame(
// &format!("encrypted length bytes: {:02x?}", encrypted_length).into(), // &format!("encrypted length bytes: {:02x?}", encrypted_length).into(),
// ); // );
if should_update_hmac == true { // web_sys::console::log_1(&"updating hmac".into());
// web_sys::console::log_1(&"updating hmac".into());
Mac::update(&mut hmac, &encrypted_length); Mac::update(hmac, &encrypted_length);
}
let mut decrypted_length = encrypted_length; let mut decrypted_length = encrypted_length;
ctr.apply_keystream(&mut decrypted_length); ctr.apply_keystream(&mut decrypted_length);
@ -265,11 +310,20 @@ fn decrypt_frame(
Some(v) => return Err(JsValue::from_str(&format!("Unsupported version: {}", v))), Some(v) => return Err(JsValue::from_str(&format!("Unsupported version: {}", v))),
}; };
Ok(Some(length))
}
fn decrypt_frame(
reader: &mut ByteReader,
mut hmac: HmacSha256,
ctr: &mut Ctr32BE<Aes256>,
ciphertext_buf: &mut Vec<u8>,
plaintext_buf: &mut Vec<u8>,
length: u32,
) -> Result<Option<signal::BackupFrame>, JsValue> {
if reader.remaining_length() < length as usize { if reader.remaining_length() < length as usize {
// web_sys::console::log_1(&"remaining data is too less".into()); // web_sys::console::log_1(&"remaining data is too less".into());
// reset the buffer for the next iteration
reader.set_position(initial_position);
return Ok(None); // Not =enough data to read the frame return Ok(None); // Not =enough data to read the frame
} }
@ -392,14 +446,14 @@ pub struct BackupDecryptor {
database_bytes: Vec<u8>, database_bytes: Vec<u8>,
preferences: HashMap<String, HashMap<String, HashMap<String, serde_json::Value>>>, preferences: HashMap<String, HashMap<String, HashMap<String, serde_json::Value>>>,
key_values: HashMap<String, HashMap<String, serde_json::Value>>, key_values: HashMap<String, HashMap<String, serde_json::Value>>,
attachments: HashMap<String, Vec<u8>>, // attachments: HashMap<String, Vec<u8>>,
stickers: HashMap<String, Vec<u8>>, // stickers: HashMap<String, Vec<u8>>,
avatars: HashMap<String, Vec<u8>>, // avatars: HashMap<String, Vec<u8>>,
ciphertext_buf: Vec<u8>, ciphertext_buf: Vec<u8>,
plaintext_buf: Vec<u8>, plaintext_buf: Vec<u8>,
total_bytes_received: usize, total_bytes_received: usize,
is_initialized: bool, is_initialized: bool,
should_update_hmac_next_run: bool, current_backup_frame_length: Option<u32>,
current_backup_frame: Option<signal::BackupFrame>, current_backup_frame: Option<signal::BackupFrame>,
} }
@ -416,14 +470,14 @@ impl BackupDecryptor {
database_bytes: Vec::new(), database_bytes: Vec::new(),
preferences: HashMap::new(), preferences: HashMap::new(),
key_values: HashMap::new(), key_values: HashMap::new(),
attachments: HashMap::new(), // attachments: HashMap::new(),
stickers: HashMap::new(), // stickers: HashMap::new(),
avatars: HashMap::new(), // avatars: HashMap::new(),
ciphertext_buf: Vec::new(), ciphertext_buf: Vec::new(),
plaintext_buf: Vec::new(), plaintext_buf: Vec::new(),
total_bytes_received: 0, total_bytes_received: 0,
is_initialized: false, is_initialized: false,
should_update_hmac_next_run: true, current_backup_frame_length: None,
current_backup_frame: None, current_backup_frame: None,
} }
} }
@ -475,28 +529,39 @@ impl BackupDecryptor {
let backup_frame_cloned = self.current_backup_frame.clone().unwrap(); let backup_frame_cloned = self.current_backup_frame.clone().unwrap();
let (filename, length, attachment_type) = // let (filename, length, attachment_type) =
if let Some(attachment) = backup_frame_cloned.attachment { // if let Some(attachment) = backup_frame_cloned.attachment {
( // (
format!("{}.bin", attachment.row_id.unwrap_or(0)), // format!("{}.bin", attachment.row_id.unwrap_or(0)),
attachment.length.unwrap_or(0), // attachment.length.unwrap_or(0),
AttachmentType::Attachment, // AttachmentType::Attachment,
) // )
} else if let Some(sticker) = backup_frame_cloned.sticker { // } else if let Some(sticker) = backup_frame_cloned.sticker {
( // (
format!("{}.bin", sticker.row_id.unwrap_or(0)), // format!("{}.bin", sticker.row_id.unwrap_or(0)),
sticker.length.unwrap_or(0), // sticker.length.unwrap_or(0),
AttachmentType::Sticker, // AttachmentType::Sticker,
) // )
} else if let Some(avatar) = backup_frame_cloned.avatar { // } else if let Some(avatar) = backup_frame_cloned.avatar {
( // (
format!("{}.bin", avatar.recipient_id.unwrap_or_default()), // format!("{}.bin", avatar.recipient_id.unwrap_or_default()),
avatar.length.unwrap_or(0), // avatar.length.unwrap_or(0),
AttachmentType::Avatar, // AttachmentType::Avatar,
) // )
} else { // } else {
return Err(JsValue::from_str("Invalid field type found")); // return Err(JsValue::from_str("Invalid field type found"));
}; // };
//
let length = if let Some(attachment) = backup_frame_cloned.attachment {
attachment.length.unwrap_or(0)
} else if let Some(sticker) = backup_frame_cloned.sticker {
sticker.length.unwrap_or(0)
} else if let Some(avatar) = backup_frame_cloned.avatar {
avatar.length.unwrap_or(0)
} else {
return Err(JsValue::from_str("Invalid field type found"));
};
match decrypt_frame_payload( match decrypt_frame_payload(
&mut self.reader, &mut self.reader,
@ -511,7 +576,7 @@ impl BackupDecryptor {
// no need to assign newly here, can stay the same as we need to load even more data // no need to assign newly here, can stay the same as we need to load even more data
return Ok(true); return Ok(true);
} }
Ok(Some(payload)) => { Ok(Some(_payload)) => {
self.current_backup_frame = None; self.current_backup_frame = None;
// match attachment_type { // match attachment_type {
@ -534,24 +599,37 @@ impl BackupDecryptor {
return Ok(false); return Ok(false);
} }
let mut hmac = <HmacSha256 as Mac>::new_from_slice(&keys.hmac_key)
.map_err(|_| JsValue::from_str("Invalid HMAC key"))?;
let mut ctr = <Ctr32BE<Aes256> as KeyIvInit>::new_from_slices(&keys.cipher_key, iv)
.map_err(|_| JsValue::from_str("Invalid CTR parameters"))?;
let frame_length =
match get_frame_length(&mut self.reader, &mut hmac, &mut ctr, header_data.version) {
Ok(None) => {
return Ok(true);
}
Ok(Some(length)) => length,
Err(e) => return Err(e),
};
// if we got to an attachment, but there we demand more data, it will be faulty, because we try to decrypt the frame although we would need // if we got to an attachment, but there we demand more data, it will be faulty, because we try to decrypt the frame although we would need
// to decrypt the attachment // to decrypt the attachment
match decrypt_frame( match decrypt_frame(
&mut self.reader, &mut self.reader,
&keys.hmac_key, hmac,
&keys.cipher_key, &mut ctr,
iv,
header_data.version,
&mut self.ciphertext_buf, &mut self.ciphertext_buf,
&mut self.plaintext_buf, &mut self.plaintext_buf,
self.should_update_hmac_next_run, frame_length,
) { ) {
Ok(None) => { Ok(None) => {
self.should_update_hmac_next_run = false; self.current_backup_frame_length = Some(frame_length);
return Ok(true); return Ok(true);
} }
Ok(Some(backup_frame)) => { Ok(Some(backup_frame)) => {
self.should_update_hmac_next_run = true; self.current_backup_frame_length = None;
// can not assign right here because of borrowing issues // can not assign right here because of borrowing issues
let mut new_iv = increment_initialisation_vector(iv); let mut new_iv = increment_initialisation_vector(iv);
@ -574,8 +652,25 @@ impl BackupDecryptor {
&& !sql.contains("sms_fts_") && !sql.contains("sms_fts_")
&& !sql.contains("mms_fts_") && !sql.contains("mms_fts_")
{ {
self.database_bytes.extend_from_slice(sql.as_bytes()); let processed_sql = if !statement.parameters.is_empty() {
let params: Vec<String> = statement
.parameters
.iter()
.map(|param| parameter_to_string(param))
.collect::<Result<_, _>>()?;
process_parameter_placeholders(&sql, &params)?
} else {
sql
};
// Add to concatenated string
self.database_bytes
.extend_from_slice(processed_sql.as_bytes());
self.database_bytes.push(b';'); self.database_bytes.push(b';');
// Store individual statement
// self.database_statements.push(processed_sql);
} }
} }
} else if let Some(preference) = backup_frame.preference { } else if let Some(preference) = backup_frame.preference {
@ -657,28 +752,38 @@ impl BackupDecryptor {
} else { } else {
let backup_frame_cloned = backup_frame.clone(); let backup_frame_cloned = backup_frame.clone();
let (filename, length, attachment_type) = // let (filename, length, attachment_type) =
if let Some(attachment) = backup_frame_cloned.attachment { // if let Some(attachment) = backup_frame_cloned.attachment {
( // (
format!("{}.bin", attachment.row_id.unwrap_or(0)), // format!("{}.bin", attachment.row_id.unwrap_or(0)),
attachment.length.unwrap_or(0), // attachment.length.unwrap_or(0),
AttachmentType::Attachment, // AttachmentType::Attachment,
) // )
} else if let Some(sticker) = backup_frame_cloned.sticker { // } else if let Some(sticker) = backup_frame_cloned.sticker {
( // (
format!("{}.bin", sticker.row_id.unwrap_or(0)), // format!("{}.bin", sticker.row_id.unwrap_or(0)),
sticker.length.unwrap_or(0), // sticker.length.unwrap_or(0),
AttachmentType::Sticker, // AttachmentType::Sticker,
) // )
} else if let Some(avatar) = backup_frame_cloned.avatar { // } else if let Some(avatar) = backup_frame_cloned.avatar {
( // (
format!("{}.bin", avatar.recipient_id.unwrap_or_default()), // format!("{}.bin", avatar.recipient_id.unwrap_or_default()),
avatar.length.unwrap_or(0), // avatar.length.unwrap_or(0),
AttachmentType::Avatar, // AttachmentType::Avatar,
) // )
} else { // } else {
return Err(JsValue::from_str("Invalid field type found")); // return Err(JsValue::from_str("Invalid field type found"));
}; // };
//
let length = if let Some(attachment) = backup_frame_cloned.attachment {
attachment.length.unwrap_or(0)
} else if let Some(sticker) = backup_frame_cloned.sticker {
sticker.length.unwrap_or(0)
} else if let Some(avatar) = backup_frame_cloned.avatar {
avatar.length.unwrap_or(0)
} else {
return Err(JsValue::from_str("Invalid field type found"));
};
match decrypt_frame_payload( match decrypt_frame_payload(
&mut self.reader, &mut self.reader,
@ -698,7 +803,7 @@ impl BackupDecryptor {
return Ok(true); return Ok(true);
} }
Ok(Some(payload)) => { Ok(Some(_payload)) => {
// match attachment_type { // match attachment_type {
// AttachmentType::Attachment => { // AttachmentType::Attachment => {
// self.attachments.insert(filename, payload); // self.attachments.insert(filename, payload);

175
test/.gitignore vendored
View file

@ -1,175 +0,0 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

37
test/dist/index.js vendored
View file

@ -29,7 +29,7 @@ export async function decryptBackup(file, passphrase, progressCallback) {
console.info(`${percent}% done`); console.info(`${percent}% done`);
} }
console.log(`Processing chunk at offset ${offset}`); // console.log(`Processing chunk at offset ${offset}`);
const chunk = file.slice(offset, offset + chunkSize); const chunk = file.slice(offset, offset + chunkSize);
const arrayBuffer = await chunk.arrayBuffer(); const arrayBuffer = await chunk.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer); const uint8Array = new Uint8Array(arrayBuffer);
@ -47,19 +47,18 @@ export async function decryptBackup(file, passphrase, progressCallback) {
} }
offset += chunkSize; offset += chunkSize;
console.log(`Completed chunk, new offset: ${offset}`); // console.log(`Completed chunk, new offset: ${offset}`);
if (performance.memory) { // if (performance.memory) {
const memoryInfo = performance.memory; // const memoryInfo = performance.memory;
console.log(`Total JS Heap Size: ${memoryInfo.totalJSHeapSize} bytes`); // console.log(`Total JS Heap Size: ${memoryInfo.totalJSHeapSize} bytes`);
console.log(`Used JS Heap Size: ${memoryInfo.usedJSHeapSize} bytes`); // console.log(`Used JS Heap Size: ${memoryInfo.usedJSHeapSize} bytes`);
console.log(`JS Heap Size Limit: ${memoryInfo.jsHeapSizeLimit} bytes`); // console.log(`JS Heap Size Limit: ${memoryInfo.jsHeapSizeLimit} bytes`);
} else { // } else {
console.log("Memory information is not available in this environment."); // console.log("Memory information is not available in this environment.");
} // }
} }
console.log("All chunks processed, finishing up"); // console.log("All chunks processed, finishing up");
console.log(window.performance.measureUserAgentSpecificMemory());
return decryptor.finish(); return decryptor.finish();
} catch (e) { } catch (e) {
console.error("Decryption failed:", e); console.error("Decryption failed:", e);
@ -71,13 +70,15 @@ async function decrypt(file, passphrase) {
try { try {
const result = await decryptBackup(file, passphrase); const result = await decryptBackup(file, passphrase);
console.log("Database bytes length:", result.databaseBytes.length); console.log(result, result.database_bytes);
console.log("Preferences:", result.preferences);
console.log("Key values:", result.keyValues);
// Example: Convert database bytes to SQL statements // console.log("Database bytes length:", result.databaseBytes.length);
const sqlStatements = new TextDecoder().decode(result.databaseBytes); console.log(
console.log("SQL statements:", sqlStatements); "Database bytes as string (partly)",
new TextDecoder().decode(result.database_bytes.slice(0, 1024 * 50)),
);
// console.log("Preferences:", result.preferences);
// console.log("Key values:", result.keyValues);
} catch (error) { } catch (error) {
console.error("Decryption failed:", error); console.error("Decryption failed:", error);
} }