diff --git a/Cargo.lock b/Cargo.lock index 52ecdd4..60591b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hkdf" version = "0.12.4" @@ -495,6 +501,7 @@ dependencies = [ "base64", "console_error_panic_hook", "ctr", + "hex", "hkdf", "hmac", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index f195241..391c27c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ prost = "0.11" prost-types = "0.11" serde_bytes = "0.11.15" console_error_panic_hook = "0.1.7" +hex = "0.4.3" [build-dependencies] prost-build = "0.11" diff --git a/src/lib.rs b/src/lib.rs index 42a12a6..3d3a0bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,12 +39,12 @@ pub mod signal { include!(concat!(env!("OUT_DIR"), "/signal.rs")); } -#[derive(Debug)] -enum AttachmentType { - Attachment, - Sticker, - Avatar, -} +// #[derive(Debug)] +// enum AttachmentType { +// Attachment, +// Sticker, +// Avatar, +// } #[wasm_bindgen] pub struct DecryptionResult { @@ -90,14 +90,6 @@ impl ByteReader { 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<()> { let available = self.remaining_data(); @@ -139,6 +131,74 @@ struct Keys { hmac_key: Vec, } +fn parameter_to_string(parameter: &signal::sql_statement::SqlParameter) -> Result { + if let Some(s) = ¶meter.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) = ¶meter.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 { + 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 { 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)) } -fn decrypt_frame( +fn get_frame_length( reader: &mut ByteReader, - hmac_key: &[u8], - cipher_key: &[u8], - initialisation_vector: &[u8], + hmac: &mut HmacSha256, + ctr: &mut Ctr32BE, header_version: Option, - ciphertext_buf: &mut Vec, - plaintext_buf: &mut Vec, - should_update_hmac: bool, -) -> Result, JsValue> { +) -> Result, JsValue> { if reader.remaining_length() < 4 { web_sys::console::log_1(&"too less data to decrypt frame length".into()); return Ok(None); // Not enough data to read the frame length } - let initial_position = reader.get_position(); - - let mut hmac = ::new_from_slice(hmac_key) - .map_err(|_| JsValue::from_str("Invalid HMAC key"))?; - - let mut ctr = - as KeyIvInit>::new_from_slices(cipher_key, initialisation_vector) - .map_err(|_| JsValue::from_str("Invalid CTR parameters"))?; - let length = match header_version { None => { let mut length_bytes = [0u8; 4]; @@ -245,11 +292,9 @@ fn decrypt_frame( // &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; ctr.apply_keystream(&mut decrypted_length); @@ -265,11 +310,20 @@ fn decrypt_frame( 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, + ciphertext_buf: &mut Vec, + plaintext_buf: &mut Vec, + length: u32, +) -> Result, JsValue> { if reader.remaining_length() < length as usize { // 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 } @@ -392,14 +446,14 @@ pub struct BackupDecryptor { database_bytes: Vec, preferences: HashMap>>, key_values: HashMap>, - attachments: HashMap>, - stickers: HashMap>, - avatars: HashMap>, + // attachments: HashMap>, + // stickers: HashMap>, + // avatars: HashMap>, ciphertext_buf: Vec, plaintext_buf: Vec, total_bytes_received: usize, is_initialized: bool, - should_update_hmac_next_run: bool, + current_backup_frame_length: Option, current_backup_frame: Option, } @@ -416,14 +470,14 @@ impl BackupDecryptor { database_bytes: Vec::new(), preferences: HashMap::new(), key_values: HashMap::new(), - attachments: HashMap::new(), - stickers: HashMap::new(), - avatars: HashMap::new(), + // attachments: HashMap::new(), + // stickers: HashMap::new(), + // avatars: HashMap::new(), ciphertext_buf: Vec::new(), plaintext_buf: Vec::new(), total_bytes_received: 0, is_initialized: false, - should_update_hmac_next_run: true, + current_backup_frame_length: None, current_backup_frame: None, } } @@ -475,28 +529,39 @@ impl BackupDecryptor { let backup_frame_cloned = self.current_backup_frame.clone().unwrap(); - let (filename, length, attachment_type) = - if let Some(attachment) = backup_frame_cloned.attachment { - ( - format!("{}.bin", attachment.row_id.unwrap_or(0)), - attachment.length.unwrap_or(0), - AttachmentType::Attachment, - ) - } else if let Some(sticker) = backup_frame_cloned.sticker { - ( - format!("{}.bin", sticker.row_id.unwrap_or(0)), - sticker.length.unwrap_or(0), - AttachmentType::Sticker, - ) - } else if let Some(avatar) = backup_frame_cloned.avatar { - ( - format!("{}.bin", avatar.recipient_id.unwrap_or_default()), - avatar.length.unwrap_or(0), - AttachmentType::Avatar, - ) - } else { - return Err(JsValue::from_str("Invalid field type found")); - }; + // let (filename, length, attachment_type) = + // if let Some(attachment) = backup_frame_cloned.attachment { + // ( + // format!("{}.bin", attachment.row_id.unwrap_or(0)), + // attachment.length.unwrap_or(0), + // AttachmentType::Attachment, + // ) + // } else if let Some(sticker) = backup_frame_cloned.sticker { + // ( + // format!("{}.bin", sticker.row_id.unwrap_or(0)), + // sticker.length.unwrap_or(0), + // AttachmentType::Sticker, + // ) + // } else if let Some(avatar) = backup_frame_cloned.avatar { + // ( + // format!("{}.bin", avatar.recipient_id.unwrap_or_default()), + // avatar.length.unwrap_or(0), + // AttachmentType::Avatar, + // ) + // } else { + // 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( &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 return Ok(true); } - Ok(Some(payload)) => { + Ok(Some(_payload)) => { self.current_backup_frame = None; // match attachment_type { @@ -534,24 +599,37 @@ impl BackupDecryptor { return Ok(false); } + let mut hmac = ::new_from_slice(&keys.hmac_key) + .map_err(|_| JsValue::from_str("Invalid HMAC key"))?; + + let mut ctr = 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 // to decrypt the attachment match decrypt_frame( &mut self.reader, - &keys.hmac_key, - &keys.cipher_key, - iv, - header_data.version, + hmac, + &mut ctr, &mut self.ciphertext_buf, &mut self.plaintext_buf, - self.should_update_hmac_next_run, + frame_length, ) { Ok(None) => { - self.should_update_hmac_next_run = false; + self.current_backup_frame_length = Some(frame_length); return Ok(true); } 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 let mut new_iv = increment_initialisation_vector(iv); @@ -574,8 +652,25 @@ impl BackupDecryptor { && !sql.contains("sms_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 = statement + .parameters + .iter() + .map(|param| parameter_to_string(param)) + .collect::>()?; + + process_parameter_placeholders(&sql, ¶ms)? + } else { + sql + }; + + // Add to concatenated string + self.database_bytes + .extend_from_slice(processed_sql.as_bytes()); self.database_bytes.push(b';'); + + // Store individual statement + // self.database_statements.push(processed_sql); } } } else if let Some(preference) = backup_frame.preference { @@ -657,28 +752,38 @@ impl BackupDecryptor { } else { let backup_frame_cloned = backup_frame.clone(); - let (filename, length, attachment_type) = - if let Some(attachment) = backup_frame_cloned.attachment { - ( - format!("{}.bin", attachment.row_id.unwrap_or(0)), - attachment.length.unwrap_or(0), - AttachmentType::Attachment, - ) - } else if let Some(sticker) = backup_frame_cloned.sticker { - ( - format!("{}.bin", sticker.row_id.unwrap_or(0)), - sticker.length.unwrap_or(0), - AttachmentType::Sticker, - ) - } else if let Some(avatar) = backup_frame_cloned.avatar { - ( - format!("{}.bin", avatar.recipient_id.unwrap_or_default()), - avatar.length.unwrap_or(0), - AttachmentType::Avatar, - ) - } else { - return Err(JsValue::from_str("Invalid field type found")); - }; + // let (filename, length, attachment_type) = + // if let Some(attachment) = backup_frame_cloned.attachment { + // ( + // format!("{}.bin", attachment.row_id.unwrap_or(0)), + // attachment.length.unwrap_or(0), + // AttachmentType::Attachment, + // ) + // } else if let Some(sticker) = backup_frame_cloned.sticker { + // ( + // format!("{}.bin", sticker.row_id.unwrap_or(0)), + // sticker.length.unwrap_or(0), + // AttachmentType::Sticker, + // ) + // } else if let Some(avatar) = backup_frame_cloned.avatar { + // ( + // format!("{}.bin", avatar.recipient_id.unwrap_or_default()), + // avatar.length.unwrap_or(0), + // AttachmentType::Avatar, + // ) + // } else { + // 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( &mut self.reader, @@ -698,7 +803,7 @@ impl BackupDecryptor { return Ok(true); } - Ok(Some(payload)) => { + Ok(Some(_payload)) => { // match attachment_type { // AttachmentType::Attachment => { // self.attachments.insert(filename, payload); diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index 9b3e627..0000000 --- a/test/.gitignore +++ /dev/null @@ -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 diff --git a/test/dist/index.js b/test/dist/index.js index 41463a8..0babb88 100644 --- a/test/dist/index.js +++ b/test/dist/index.js @@ -29,7 +29,7 @@ export async function decryptBackup(file, passphrase, progressCallback) { 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 arrayBuffer = await chunk.arrayBuffer(); const uint8Array = new Uint8Array(arrayBuffer); @@ -47,19 +47,18 @@ export async function decryptBackup(file, passphrase, progressCallback) { } offset += chunkSize; - console.log(`Completed chunk, new offset: ${offset}`); - if (performance.memory) { - const memoryInfo = performance.memory; - console.log(`Total JS Heap Size: ${memoryInfo.totalJSHeapSize} bytes`); - console.log(`Used JS Heap Size: ${memoryInfo.usedJSHeapSize} bytes`); - console.log(`JS Heap Size Limit: ${memoryInfo.jsHeapSizeLimit} bytes`); - } else { - console.log("Memory information is not available in this environment."); - } + // console.log(`Completed chunk, new offset: ${offset}`); + // if (performance.memory) { + // const memoryInfo = performance.memory; + // console.log(`Total JS Heap Size: ${memoryInfo.totalJSHeapSize} bytes`); + // console.log(`Used JS Heap Size: ${memoryInfo.usedJSHeapSize} bytes`); + // console.log(`JS Heap Size Limit: ${memoryInfo.jsHeapSizeLimit} bytes`); + // } else { + // console.log("Memory information is not available in this environment."); + // } } - console.log("All chunks processed, finishing up"); - console.log(window.performance.measureUserAgentSpecificMemory()); + // console.log("All chunks processed, finishing up"); return decryptor.finish(); } catch (e) { console.error("Decryption failed:", e); @@ -71,13 +70,15 @@ async function decrypt(file, passphrase) { try { const result = await decryptBackup(file, passphrase); - console.log("Database bytes length:", result.databaseBytes.length); - console.log("Preferences:", result.preferences); - console.log("Key values:", result.keyValues); + console.log(result, result.database_bytes); - // Example: Convert database bytes to SQL statements - const sqlStatements = new TextDecoder().decode(result.databaseBytes); - console.log("SQL statements:", sqlStatements); + // console.log("Database bytes length:", result.databaseBytes.length); + console.log( + "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) { console.error("Decryption failed:", error); }