diff --git a/CHANGELOG.md b/CHANGELOG.md index 8609117..1df9df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +- Added support for F-Droid entry point specification and new index versions +- Update dependencies + ## [0.16.0] - 2024-04-04 - Support Google Play API v3 and document workflow for downloading via Google Play diff --git a/Cargo.lock b/Cargo.lock index f2efcaa..463779a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -125,7 +125,8 @@ dependencies = [ "ring", "serde", "serde_json", - "sha-1", + "sha1", + "sha2", "simple-error", "tempfile", "tokio", @@ -269,9 +270,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.24" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ "jobserver", "libc", @@ -308,9 +309,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -318,9 +319,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -635,9 +636,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -650,9 +651,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -660,15 +661,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -677,15 +678,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -694,21 +695,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -745,9 +746,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "googleplay-protobuf" @@ -983,7 +984,7 @@ dependencies = [ "http 1.1.0", "hyper 1.4.1", "hyper-util", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1101,9 +1102,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -1137,9 +1138,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1283,21 +1284,18 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" @@ -1459,9 +1457,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] @@ -1741,9 +1739,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", "rustls-pki-types", @@ -1805,9 +1803,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] @@ -1895,17 +1893,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha1" version = "0.10.6" @@ -2221,7 +2208,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] @@ -2355,9 +2342,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -2366,9 +2353,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -2381,9 +2368,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -2393,9 +2380,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2403,9 +2390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -2416,9 +2403,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-streams" @@ -2435,9 +2422,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 6e0067b..e26d2eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,8 @@ cryptographic-message-syntax = "0.26" ring = "0.17" x509-certificate = "0.23" simple-error = "0.3" -sha-1 = "0.10" +sha1 = "0.10" +sha2 = "0.10" base64 = "0.22" serde_json = "1" hex = "0.4" diff --git a/USAGE-fdroid.md b/USAGE-fdroid.md index a1702ed..3659b6f 100644 --- a/USAGE-fdroid.md +++ b/USAGE-fdroid.md @@ -16,6 +16,12 @@ In addition to specifying a mirror, a wholly separate F-Droid repo can be specif apkeep -a org.torproject.android -d f-droid -o repo=https://guardianproject.info/fdroid/repo?fingerprint=B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135 . ``` +If a repo supports the new [entry point specification](https://f-droid.org/docs/All_our_APIs/#the-repo-index), you can specify that be used instead of the older (v1) package index. This may become the default behavior in the future, but can be specified by use of the `use_entry` option: + +```shell +apkeep -a org.torproject.android -d f-droid -o repo=https://guardianproject.info/fdroid/repo?fingerprint=B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135,use_entry=true . +``` + A special option can also be used to skip verification of the repository index. *Only use for debugging purposes*: ```shell diff --git a/src/fdroid.rs b/src/fdroid.rs index bfd4eee..a655047 100644 --- a/src/fdroid.rs +++ b/src/fdroid.rs @@ -14,7 +14,8 @@ use indicatif::MultiProgress; use regex::Regex; use ring::digest::{Context, SHA256}; use serde_json::Value; -use sha1::{Sha1, Digest}; +use sha1::{Sha1, Digest as Sha1Digest}; +use sha2::Sha256; use simple_error::SimpleError; use tempfile::{tempdir, TempDir}; use tokio::time::{sleep, Duration}; @@ -38,6 +39,10 @@ async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc true, + _ => false, + }; if let Some(full_repo_option) = options.get("repo") { custom_repo = true; if let Some((repo_option, fingerprint_option)) = full_repo_option.split_once("?fingerprint=") { @@ -76,12 +81,16 @@ async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc { let mut contents = String::new(); if file.read_to_string(&mut contents).is_err() { - println!("Could not read latest_etag file for F-Droid package index. Exiting."); + println!("Could not read etag file for F-Droid package index. Exiting."); std::process::exit(1); } Some(contents) @@ -90,36 +99,53 @@ async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc false, _ => true, }; - match verify_and_return_index(&temp_dir, &files, &fingerprint, verify_index) { - Ok(index) => { + match verify_and_return_json(&temp_dir, &files, &fingerprint, verify_index, use_entry) { + Ok(json) => { + let index = if use_entry { + match verify_and_return_index_from_entry(&temp_dir, &repo, &json, verify_index, mp).await { + Ok(index_from_entry) => { + index_from_entry + } + Err(_) => { + println!("Could verify and return package index from entry JSON. Exiting."); + std::process::exit(1); + } + } + } else { + json + }; + match serde_json::from_str(&index) { Ok(index_value) => { if fs::write(index_file, index).is_err() { @@ -138,8 +164,7 @@ async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc { - println!("{}", err); + Err(_) => { println!("Could not verify F-Droid package index. Exiting."); std::process::exit(1); }, @@ -147,6 +172,13 @@ async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc String { + let mut file = File::open(&file).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + contents +} + pub async fn download_apps( apps: Vec<(String, Option)>, parallel: usize, @@ -242,6 +274,10 @@ pub async fn download_apps( } type DownloadInformation = (Vec<(String, Option, String, Vec)>, String); +/// This currently works for `index-v1.json` as well as an index with version `20002`. It is +/// flexible enough to parse either, and may work on future index versions as well. Since `sha256` +/// digests are checked before proceeding, I don't foresee this having an insecure failure mode, so +/// checking the index version and making the parsing overly brittle has no substantive advantage. fn parse_json_for_download_information(index: Value, apps: Vec<(String, Option)>) -> Result { let index_map = index.as_object().ok_or(FDroidError::Dummy)?; let repo_address = index_map @@ -255,40 +291,25 @@ fn parse_json_for_download_information(index: Value, apps: Vec<(String, Option, String, Vec)> = apps.into_iter().map(|app| { let (app_id, app_version) = app; - if packages.contains_key(&app_id) { - let app_array_value = packages.get(&app_id).unwrap(); - if app_array_value.is_array() { - let app_array = app_array_value.as_array().unwrap(); + match packages.get(&app_id) { + Some(Value::Array(app_array)) => { if app_version.is_none() { if !app_array.is_empty() && app_array[0].is_object() { let fdroid_app = app_array[0].as_object().unwrap(); - if fdroid_app.contains_key("apkName") && fdroid_app.contains_key("hash") { - let filename_value = fdroid_app.get("apkName").unwrap(); - let hash_value = fdroid_app.get("hash").unwrap(); - if filename_value.is_string() && hash_value.is_string() { - let filename = filename_value.as_str().unwrap().to_string(); - if let Ok(hash) = hex::decode(hash_value.as_str().unwrap().to_string()) { - return Some((app_id, app_version, filename, hash)); - } + if let (Some(Value::String(filename)), Some(Value::String(hash))) = (fdroid_app.get("apkName"), fdroid_app.get("hash")) { + if let Ok(hash) = hex::decode(hash.to_string()) { + return Some((app_id, app_version, filename.to_string(), hash)); } } } } else { for single_app in app_array { - if single_app.is_object() { - let fdroid_app = single_app.as_object().unwrap(); - if fdroid_app.contains_key("versionName") { - let version_name_value = fdroid_app.get("versionName").unwrap(); - if version_name_value.is_string() { - let version_name = version_name_value.as_str().unwrap().to_string(); - if version_name == *app_version.as_ref().unwrap() && fdroid_app.contains_key("apkName") && fdroid_app.contains_key("hash") { - let filename_value = fdroid_app.get("apkName").unwrap(); - let hash_value = fdroid_app.get("hash").unwrap(); - if filename_value.is_string() && hash_value.is_string() { - let filename = filename_value.as_str().unwrap().to_string(); - if let Ok(hash) = hex::decode(hash_value.as_str().unwrap().to_string()) { - return Some((app_id, app_version, filename, hash)); - } + if let Value::Object(fdroid_app) = single_app { + if let Some(Value::String(version_name)) = fdroid_app.get("versionName") { + if version_name == app_version.as_ref().unwrap() { + if let (Some(Value::String(filename)), Some(Value::String(hash))) = (fdroid_app.get("apkName"), fdroid_app.get("hash")) { + if let Ok(hash) = hex::decode(hash.to_string()) { + return Some((app_id, app_version, filename.to_string(), hash)); } } } @@ -298,9 +319,47 @@ fn parse_json_for_download_information(index: Value, apps: Vec<(String, Option { + if let Some(Value::Object(versions)) = app_object.get("versions") { + let mut latest_version = 0; + let mut filename = String::new(); + let mut hash = String::new(); + for (_, version_value) in versions { + if let Value::Object(version) = version_value { + if let (Some(Value::Object(manifest)), Some(Value::Object(file))) = (version.get("manifest"), version.get("file")) { + if let (Some(Value::String(name)), Some(Value::String(sha256))) = (file.get("name"), file.get("sha256")) { + if app_version.is_some() { + if let Some(Value::String(version_name)) = manifest.get("versionName") { + if version_name == app_version.as_ref().unwrap() { + if let Ok(sha256) = hex::decode(sha256.to_string()) { + return Some((app_id, app_version, name.to_string(), sha256)); + } + } + } + } else { + if let Some(Value::Number(version_code_number)) = manifest.get("versionCode") { + if let Some(version_code) = version_code_number.as_u64() { + if version_code > latest_version { + latest_version = version_code; + filename = name.to_string(); + hash = sha256.to_string(); + } + } + } + } + } + } + } + } + if app_version.is_none() { + if let Ok(hash) = hex::decode(hash) { + return Some((app_id, app_version, filename, hash)); + } + } + } + }, + _ => println!("Could not find {} in package list. Skipping...", app_id), } None }).flatten().collect(); @@ -316,6 +375,7 @@ pub async fn list_versions(apps: Vec<(String, Option)>, options: HashMap }; } +/// The comments for `parse_json_for_download_information` apply here, too. fn parse_json_display_versions(index: Value, apps: Vec<(String, Option)>) -> Result<(), FDroidError> { let index_map = index.as_object().ok_or(FDroidError::Dummy)?; @@ -326,35 +386,43 @@ fn parse_json_display_versions(index: Value, apps: Vec<(String, Option)> for app in apps { let (app_id, _) = app; println!("Versions available for {} on F-Droid:", app_id); - if packages.contains_key(&app_id) { - let app_array_value = packages.get(&app_id).unwrap(); - if app_array_value.is_array() { - let app_array = app_array_value.as_array().unwrap(); - let mut versions = HashSet::new(); + let mut versions_set = HashSet::new(); + match packages.get(&app_id) { + Some(Value::Array(app_array)) => { for single_app in app_array { - if single_app.is_object() { - let fdroid_app = single_app.as_object().unwrap(); - if fdroid_app.contains_key("versionName") { - let version_name_value = fdroid_app.get("versionName").unwrap(); - if version_name_value.is_string() { - let version_name = version_name_value.as_str().unwrap().to_string(); - versions.insert(version_name); + if let Value::Object(fdroid_app) = single_app { + if let Some(Value::String(version_name)) = fdroid_app.get("versionName") { + versions_set.insert(version_name.to_string()); + } + } + } + }, + Some(Value::Object(app_object)) => { + if let Some(Value::Object(versions)) = app_object.get("versions") { + for (_, version_value) in versions { + if let Value::Object(version) = version_value { + if let Some(Value::Object(manifest)) = version.get("manifest") { + if let Some(Value::String(version_name)) = manifest.get("versionName") { + versions_set.insert(version_name.to_string()); + } } } } } - let mut versions = versions.drain().collect::>(); - versions.sort(); - println!("| {}", versions.join(", ")); + }, + _ => { + println!("| Could not find {} in package list. Skipping...", app_id); + continue; } - } else { - println!("| Could not find {} in package list. Skipping...", app_id); } + let mut versions_set = versions_set.drain().collect::>(); + versions_set.sort(); + println!("| {}", versions_set.join(", ")); } Ok(()) } -fn verify_and_return_index(dir: &TempDir, files: &[String], fingerprint: &[u8], verify_index: bool) -> Result> { +fn verify_and_return_json(dir: &TempDir, files: &[String], fingerprint: &[u8], verify_index: bool, use_entry: bool) -> Result> { let re = Regex::new(consts::FDROID_SIGNATURE_BLOCK_FILE_REGEX).unwrap(); let cert_file = { let mut cert_files = vec![]; @@ -391,7 +459,7 @@ fn verify_and_return_index(dir: &TempDir, files: &[String], fingerprint: &[u8], context.update(&cert.encode_ber()?); let cert_fingerprint = context.finish(); if cert_fingerprint.as_ref() != fingerprint { - return Err(Box::new(SimpleError::new("Fingerprint of the key contained in the F-Droid repository index does not match the expected fingerprint."))) + return Err(Box::new(SimpleError::new("Fingerprint of the key contained in the F-Droid repository does not match the expected fingerprint."))) }; } @@ -399,41 +467,125 @@ fn verify_and_return_index(dir: &TempDir, files: &[String], fingerprint: &[u8], let manifest_file = dir.path().join("META-INF").join("MANIFEST.MF"); let manifest_file_data = fs::read(manifest_file)?; if verify_index { - let signed_file_regex = Regex::new(r"\r\nSHA1-Digest-Manifest: (.*)\r\n").unwrap(); - let signed_file_manifest_sha1sum = b64_general_purpose::STANDARD.decode(match signed_file_regex.captures(signed_file_string) { + let (signed_file_regex, sha_algorithm_name) = if use_entry { + (Regex::new(r"\r\nSHA-256-Digest-Manifest: (.*)\r\n").unwrap(), "sha256sum") + } else { + (Regex::new(r"\r\nSHA1-Digest-Manifest: (.*)\r\n").unwrap(), "sha1sum") + }; + let signed_file_manifest_shasum = b64_general_purpose::STANDARD.decode(match signed_file_regex.captures(signed_file_string) { Some(caps) if caps.len() >= 2 => caps.get(1).unwrap().as_str(), _ => { - return Err(Box::new(SimpleError::new("Could not retrieve the manifest sha1sum from the signed file."))); + return Err(Box::new(SimpleError::new(format!("Could not retrieve the manifest {} from the signed file.", sha_algorithm_name)))); } })?; - let mut hasher = Sha1::new(); - hasher.update(manifest_file_data.clone()); - let actual_manifest_sha1sum = hasher.finalize(); - if signed_file_manifest_sha1sum != actual_manifest_sha1sum[..] { - return Err(Box::new(SimpleError::new("The manifest sha1sum from the signed file does not match the actual manifest sha1sum."))); + let actual_manifest_shasum = if use_entry { + let mut hasher = Sha256::new(); + hasher.update(manifest_file_data.clone()); + Vec::from(hasher.finalize().as_slice()) + } else { + let mut hasher = Sha1::new(); + hasher.update(manifest_file_data.clone()); + Vec::from(hasher.finalize().as_slice()) + }; + if signed_file_manifest_shasum != actual_manifest_shasum[..] { + return Err(Box::new(SimpleError::new(format!("The manifest {} from the signed file does not match the actual manifest {}.", sha_algorithm_name, sha_algorithm_name)))); } } let manifest_file_string = std::str::from_utf8(&manifest_file_data)?; - let index_file = dir.path().join("index-v1.json"); - let index_file_data = fs::read(index_file)?; + let json_file = if use_entry { + dir.path().join("entry.json") + } else { + dir.path().join("index-v1.json") + }; + let json_file_data = fs::read(json_file)?; if verify_index { - let manifest_file_regex = Regex::new(r"\r\nName: index-v1\.json\r\nSHA1-Digest: (.*)\r\n").unwrap(); - let manifest_file_index_sha1sum = b64_general_purpose::STANDARD.decode(match manifest_file_regex.captures(manifest_file_string) { + let (manifest_file_regex, file_algo) = if use_entry { + (Regex::new(r"\r\nName: entry\.json\r\nSHA-256-Digest: (.*)\r\n").unwrap(), "entry sha256sum") + } else { + (Regex::new(r"\r\nName: index-v1\.json\r\nSHA1-Digest: (.*)\r\n").unwrap(), "index sha1sum") + }; + let manifest_file_shasum = b64_general_purpose::STANDARD.decode(match manifest_file_regex.captures(manifest_file_string) { Some(caps) if caps.len() >= 2 => caps.get(1).unwrap().as_str(), _ => { - return Err(Box::new(SimpleError::new("Could not retrieve the index sha1sum from the manifest file."))); + return Err(Box::new(SimpleError::new(format!("Could not retrieve the {} from the manifest file.", file_algo)))); } })?; - let mut hasher = Sha1::new(); - hasher.update(index_file_data.clone()); - let actual_index_sha1sum = hasher.finalize(); - if manifest_file_index_sha1sum != actual_index_sha1sum[..] { - return Err(Box::new(SimpleError::new("The index sha1sum from the manifest file does not match the actual index sha1sum."))); + let actual_shasum = if use_entry { + let mut hasher = Sha256::new(); + hasher.update(json_file_data.clone()); + Vec::from(hasher.finalize().as_slice()) + } else { + let mut hasher = Sha1::new(); + hasher.update(json_file_data.clone()); + Vec::from(hasher.finalize().as_slice()) + }; + if manifest_file_shasum != actual_shasum[..] { + return Err(Box::new(SimpleError::new(format!("The {} from the manifest file does not match the actual {}.", file_algo, file_algo)))); } } - Ok(String::from(std::str::from_utf8(&index_file_data)?)) + Ok(String::from(std::str::from_utf8(&json_file_data)?)) +} + +async fn verify_and_return_index_from_entry(dir: &TempDir, repo: &str, json: &str, verify_index: bool, mp: Rc) -> Result> { + let mp_log = Rc::clone(&mp); + let (index_name, index_sha256) = match serde_json::from_str::(json) { + Ok(entry) => { + let entry_map = entry.as_object().ok_or(FDroidError::Dummy)?; + let index_map = entry_map + .get("index").ok_or(FDroidError::Dummy)?; + (index_map.get("name").ok_or(FDroidError::Dummy)? + .as_str().ok_or(FDroidError::Dummy)?.trim_start_matches("/").to_string(), + index_map.get("sha256").ok_or(FDroidError::Dummy)? + .as_str().ok_or(FDroidError::Dummy)?.to_string()) + }, + Err(_) => { + println!("Could not decode JSON for F-Droid entry file. Exiting."); + std::process::exit(1); + } + }; + let index_url = format!("{}/{}", repo, index_name); + let mut dl = AsyncDownload::new(&index_url, dir.path(), &index_name).get().await.unwrap(); + let length = dl.length(); + let cb = match length { + Some(length) => Some(progress_wrapper(mp)(index_name.to_string(), length)), + None => None, + }; + match dl.download(&cb).await { + Ok(_) => { + mp_log.println(format!("Package index downloaded successfully!")).unwrap(); + let index_file = dir.path().join(index_name); + let index_file_data = fs::read(index_file)?; + + if verify_index { + println!("Verifying..."); + let actual_index_shasum = { + let mut hasher = Sha256::new(); + hasher.update(index_file_data.clone()); + Vec::from(hasher.finalize().as_slice()) + }; + println!("{:?}", actual_index_shasum); + let index_sha256 = match hex::decode(index_sha256) { + Ok(index_sha256) => index_sha256, + Err(_) => { + println!("Index sha256sum did not specify valid hex. Exiting."); + std::process::exit(1); + } + }; + println!("{:?}", index_sha256); + if index_sha256 != actual_index_shasum { + return Err(Box::new(SimpleError::new("The index sha256sum from the entry file does not match the actual index sha256sum."))); + } + } + + Ok(String::from(std::str::from_utf8(&index_file_data)?)) + } + Err(_) => { + println!("Could not download F-Droid package index. Exiting."); + std::process::exit(1); + } + } } fn get_signed_data_from_cert_file(signature_block_file: PathBuf) -> Result> { @@ -462,21 +614,26 @@ fn get_signed_data_from_cert_file(signature_block_file: PathBuf) -> Result) -> Vec { +async fn download_and_extract_to_tempdir(dir: &TempDir, repo: &str, mp: Rc, use_entry: bool) -> Vec { let mp_log = Rc::clone(&mp); println!("Downloading F-Droid package repository..."); let mut files = vec![]; - let fdroid_index_url = format!("{}/index-v1.jar", repo); - let mut dl = AsyncDownload::new(&fdroid_index_url, dir.path(), "index.zip").get().await.unwrap(); + let fdroid_jar_url = if use_entry { + format!("{}/entry.jar", repo) + } else { + format!("{}/index-v1.jar", repo) + }; + let jar_local_file = "jar.zip"; + let mut dl = AsyncDownload::new(&fdroid_jar_url, dir.path(), jar_local_file).get().await.unwrap(); let length = dl.length(); let cb = match length { - Some(length) => Some(progress_wrapper(mp)("index.zip".to_string(), length)), + Some(length) => Some(progress_wrapper(mp)(jar_local_file.to_string(), length)), None => None, }; match dl.download(&cb).await { Ok(_) => { mp_log.println(format!("Package repository downloaded successfully!\nExtracting...")).unwrap(); - let file = fs::File::open(dir.path().join("index.zip")).unwrap(); + let file = fs::File::open(dir.path().join(jar_local_file)).unwrap(); match zip::ZipArchive::new(file) { Ok(mut archive) => { for i in 0..archive.len() { @@ -504,19 +661,19 @@ async fn download_and_extract_index_to_tempdir(dir: &TempDir, repo: &str, mp: Rc use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); } } } }, Err(_) => { - println!("F-Droid package index could not be extracted. Please try again."); + println!("F-Droid package repository could not be extracted. Please try again."); std::process::exit(1); } } } Err(_) => { - println!("Could not download F-Droid package index."); + println!("Could not download F-Droid package repository."); std::process::exit(1); } }