diff --git a/Cargo.lock b/Cargo.lock index 83e859c..122e3d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,21 +48,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.18" @@ -127,7 +112,7 @@ checksum = "1cb37b529daddddf129612580831db21a538d1aa2798b367039e9316762c81a7" dependencies = [ "anyhow", "byteorder", - "quick-xml 0.26.0", + "quick-xml", "rasn", "rasn-pkix", "roxmltree", @@ -151,24 +136,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "async-compression" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" -dependencies = [ - "bzip2", - "deflate64", - "flate2", - "futures-core", - "futures-io", - "memchr", - "pin-project-lite", - "xz2", - "zstd", - "zstd-safe", -] - [[package]] name = "async-trait" version = "0.1.86" @@ -211,22 +178,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "async_zip" -version = "0.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" -dependencies = [ - "async-compression", - "chrono", - "crc32fast", - "futures-lite", - "pin-project", - "thiserror 1.0.69", - "tokio", - "tokio-util", -] - [[package]] name = "atomic-destructor" version = "0.3.0" @@ -403,27 +354,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.12+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "cbc" version = "0.1.2" @@ -439,8 +369,6 @@ version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -480,10 +408,7 @@ version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ - "android-tzdata", - "iana-time-zone", "num-traits", - "windows-targets", ] [[package]] @@ -679,12 +604,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f" -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - [[package]] name = "der" version = "0.6.1" @@ -910,19 +829,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -1214,29 +1120,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "1.5.0" @@ -1461,15 +1344,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1545,17 +1419,6 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "memchr" version = "2.7.4" @@ -1602,7 +1465,7 @@ dependencies = [ "anyhow", "apk", "async-trait", - "async_zip", + "byteorder", "clap", "config", "dialoguer", @@ -1610,7 +1473,6 @@ dependencies = [ "indicatif", "log", "nostr-sdk", - "quick-xml 0.37.2", "reqwest", "semver", "serde", @@ -1859,12 +1721,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "password-hash" version = "0.5.0" @@ -1961,26 +1817,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pin-project" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.98", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2079,16 +1915,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quick-xml" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quote" version = "1.0.38" @@ -2889,7 +2715,6 @@ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", @@ -3259,15 +3084,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-registry" version = "0.2.0" @@ -3443,15 +3259,6 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yaml-rust2" version = "0.9.0" @@ -3568,31 +3375,3 @@ dependencies = [ "crossbeam-utils", "flate2", ] - -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 262c14f..23bd278 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,9 @@ tokio = { version = "1.43.0", features = ["fs", "rt", "macros", "rt-multi-thread serde = { version = "1.0.217", features = ["derive"] } async-trait = "0.1.86" apk = "0.4.0" -async_zip = { version = "0.0.17", features = ["full", "tokio"] } semver = "1.0.25" indicatif = "0.17.11" dialoguer = "0.11.0" env_logger = "0.11.6" sha2 = "0.10.8" -quick-xml = { version = "0.37.2", features = ["serialize"] } +byteorder = "1.5.0" diff --git a/nap.yaml b/nap.yaml index 2a70680..3341b49 100644 --- a/nap.yaml +++ b/nap.yaml @@ -8,19 +8,23 @@ name: "Freeflow" description: "Live in the moment" # Application icon -icon: "https://freeflow.app/icon.png" +icon: "https://nostr.download/b7f6bd1e3951185e49a04f733de61d1658b710645a40a875d5d18fdbcadf85cd.webp" # Banner / Preview of the app images: - - "https://freeflow.app/banner.jpg" + - "https://nostr.download/b6120e8aff11395bca8e51d720e9f4af636ed240dfe04d3b32f5f89cb5966eab.webp" # Public code repo or project website repository: "https://github.com/nostrlabs-io/freeflow" +# Public project website +url: "https://freeflow.app" + # SPDX code license license: "MIT" # Descriptive app tags tags: - "tiktok" - - "shorts" \ No newline at end of file + - "shorts" + - "video" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index d2ef142..36b49b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,10 @@ use crate::manifest::Manifest; use crate::repo::Repo; use anyhow::{anyhow, bail, Result}; use clap::Parser; -use config::{Config, File, FileSourceFile}; +use config::{Config, File}; use log::info; use nostr_sdk::prelude::Coordinate; -use nostr_sdk::{Client, EventBuilder, JsonUtil, Keys, Kind, Tag}; +use nostr_sdk::{Client, EventBuilder, Keys, Kind, Tag}; use std::path::PathBuf; #[derive(clap::Parser)] @@ -100,7 +100,7 @@ async fn main() -> Result<()> { client.add_relay(r).await?; } if args.relay.is_empty() { - const DEFAULT_RELAY: &'static str = "wss://relay.zapstore.dev"; + const DEFAULT_RELAY: &str = "wss://relay.zapstore.dev"; info!("Connecting to default relay {DEFAULT_RELAY}"); client.add_relay(DEFAULT_RELAY).await?; } diff --git a/src/manifest.rs b/src/manifest.rs index 5f153a8..439a591 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,4 +1,4 @@ -use nostr_sdk::{Event, EventBuilder, Kind, Tag}; +use nostr_sdk::{EventBuilder, Kind, Tag}; use serde::Deserialize; #[derive(Deserialize)] @@ -9,12 +9,18 @@ pub struct Manifest { /// Application display name pub name: String, - /// Long app description / release notes + /// App description pub description: Option, + /// Long form app description (with markdown) + pub summary: Option, + /// Repo URL pub repository: Option, + /// Public project website + pub url: Option, + /// SPDX license code pub license: Option, @@ -28,32 +34,43 @@ pub struct Manifest { pub tags: Vec, } -impl Into for &Manifest { - fn into(self) -> EventBuilder { - let mut b = EventBuilder::new( - Kind::Custom(32_267), - self.description.clone().unwrap_or_default(), - ) +impl From<&Manifest> for EventBuilder { + fn from(val: &Manifest) -> Self { + let mut b = EventBuilder::new(Kind::Custom(32_267), val.description.as_str_or_empty()) .tags([ - Tag::parse(["d", &self.id]).unwrap(), - Tag::parse(["name", &self.name]).unwrap(), + Tag::parse(["d", &val.id]).unwrap(), + Tag::parse(["name", &val.name]).unwrap(), + Tag::parse(["url", val.url.as_str_or_empty()]).unwrap(), ]); - if let Some(icon) = &self.icon { + if let Some(s) = &val.summary { + b = b.tag(Tag::parse(["summary", s]).unwrap()); + } + if let Some(icon) = &val.icon { b = b.tag(Tag::parse(["icon", icon]).unwrap()); } - if let Some(repository) = &self.repository { + if let Some(repository) = &val.repository { b = b.tag(Tag::parse(["repository", repository]).unwrap()); } - if let Some(license) = &self.license { + if let Some(license) = &val.license { b = b.tag(Tag::parse(["license", license]).unwrap()); } - for image in &self.images { + for image in &val.images { b = b.tag(Tag::parse(["image", image]).unwrap()); } - for tag in &self.tags { + for tag in &val.tags { b = b.tag(Tag::parse(["t", tag]).unwrap()); } b } -} \ No newline at end of file +} + +pub trait AsStrOrEmpty { + fn as_str_or_empty(&self) -> &str; +} + +impl AsStrOrEmpty for Option { + fn as_str_or_empty(&self) -> &str { + self.as_ref().map(|s| s.as_str()).unwrap_or("") + } +} diff --git a/src/repo/github.rs b/src/repo/github.rs index e9e5e97..9f7dd96 100644 --- a/src/repo/github.rs +++ b/src/repo/github.rs @@ -1,5 +1,5 @@ use crate::repo::{ - load_artifact, load_artifact_url, Repo, RepoArtifact, RepoRelease, RepoResource, + load_artifact_url, Repo, RepoRelease, }; use anyhow::{anyhow, Result}; use log::{info, warn}; diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 6e67510..f1810a1 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -1,26 +1,24 @@ use crate::manifest::Manifest; use crate::repo::github::GithubRepo; -use anyhow::{anyhow, bail, ensure, Context, Result}; -use apk::manifest::Sdk; +use anyhow::{anyhow, bail, ensure, Result}; use apk::res::Chunk; +use apk::zip::ZipArchive; use apk::AndroidManifest; -use async_zip::tokio::read::seek::ZipFileReader; -use async_zip::ZipFile; +use byteorder::LittleEndian; +use byteorder::ReadBytesExt; use log::{debug, info, warn}; -use nostr_sdk::async_utility::futures_util::TryStreamExt; use nostr_sdk::prelude::{hex, Coordinate, StreamExt}; use nostr_sdk::{Event, EventBuilder, Kind, NostrSigner, Tag}; use reqwest::Url; use semver::Version; -use serde::Deserialize; use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; use std::env::temp_dir; -use std::fmt::{write, Display, Formatter}; -use std::io::Cursor; +use std::fmt::{Display, Formatter}; +use std::fs::File; +use std::io::{Cursor, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; -use tokio::fs::File; -use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, AsyncSeek, AsyncWriteExt, BufReader}; +use tokio::io::AsyncWriteExt; mod github; @@ -73,7 +71,31 @@ impl TryInto for RepoArtifact { b = b.tag(Tag::parse(["url", u.as_str()])?); } match self.metadata { - ArtifactMetadata::APK { manifest } => { + ArtifactMetadata::APK { + manifest, + signature, + } => { + match signature { + ApkSignatureBlock::None => { + warn!("No signature found in metadata"); + } + ApkSignatureBlock::V2 { signatures, .. } => { + for signature in signatures { + b = b.tag(Tag::parse([ + "apk_signature_hash", + &hex::encode(signature.digest), + ])?); + } + } + ApkSignatureBlock::V3 { signatures, .. } => { + for signature in signatures { + b = b.tag(Tag::parse([ + "apk_signature_hash", + &hex::encode(signature.digest), + ])?); + } + } + } if let Some(vn) = manifest.version_name { b = b.tag(Tag::parse(["version", vn.as_str()])?); } @@ -101,19 +123,127 @@ impl TryInto for RepoArtifact { #[derive(Debug, Clone)] pub enum ArtifactMetadata { - APK { manifest: AndroidManifest }, + APK { + manifest: AndroidManifest, + signature: ApkSignatureBlock, + }, +} + +#[derive(Debug, Clone)] +pub enum ApkSignatureBlock { + None, + /// Android V2 Signature Block + /// + /// https://source.android.com/docs/security/features/apksigning/v2#apk-signature-scheme-v2-block-format + V2 { + signatures: Vec, + public_key: Vec, + certificates: Vec>, + attributes: HashMap>, + }, + V3 { + signatures: Vec, + public_key: Vec, + }, +} + +impl Display for ApkSignatureBlock { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ApkSignatureBlock::None => write!(f, "none"), + ApkSignatureBlock::V2 { signatures, .. } => { + write!(f, "v2: ")?; + for sig in signatures { + write!( + f, + "algo={}, digest={}, sig={}", + sig.algo, + hex::encode(&sig.digest), + hex::encode(&sig.signature) + )?; + } + Ok(()) + } + ApkSignatureBlock::V3 { signatures, .. } => { + write!(f, "V3: ")?; + for sig in signatures { + write!( + f, + "algo={}, digest={}, sig={}", + sig.algo, + hex::encode(&sig.digest), + hex::encode(&sig.signature) + )?; + } + Ok(()) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct ApkSignature { + pub algo: ApkSignatureAlgo, + pub signature: Vec, + pub digest: Vec, +} + +#[derive(Debug, Clone)] +pub enum ApkSignatureAlgo { + RsaSsaPssSha256, + RsaSsaPssSha512, + RsaSsaPkcs1Sha256, + RsaSsaPkcs1Sha512, + EcdsaSha256, + EcdsaSha512, + DsaSha256, +} + +impl Display for ApkSignatureAlgo { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ApkSignatureAlgo::RsaSsaPssSha256 => write!(f, "RSASSA-PSS-SHA256"), + ApkSignatureAlgo::RsaSsaPssSha512 => write!(f, "RSASSA-PSS-SHA512"), + ApkSignatureAlgo::RsaSsaPkcs1Sha256 => write!(f, "RSASSA-PKCS1-SHA256"), + ApkSignatureAlgo::RsaSsaPkcs1Sha512 => write!(f, "RSASSA-PKCS1-SHA512"), + ApkSignatureAlgo::EcdsaSha256 => write!(f, "ECDSA-SHA256"), + ApkSignatureAlgo::EcdsaSha512 => write!(f, "ECDSA-SHA512"), + ApkSignatureAlgo::DsaSha256 => write!(f, "DSA-SHA256"), + } + } +} + +impl TryFrom for ApkSignatureAlgo { + type Error = anyhow::Error; + + fn try_from(value: u32) -> std::result::Result { + match value { + 0x0101 => Ok(ApkSignatureAlgo::RsaSsaPssSha256), + 0x0102 => Ok(ApkSignatureAlgo::RsaSsaPssSha512), + 0x0103 => Ok(ApkSignatureAlgo::RsaSsaPkcs1Sha256), + 0x0104 => Ok(ApkSignatureAlgo::RsaSsaPkcs1Sha512), + 0x0201 => Ok(ApkSignatureAlgo::EcdsaSha256), + 0x0202 => Ok(ApkSignatureAlgo::EcdsaSha512), + 0x0301 => Ok(ApkSignatureAlgo::DsaSha256), + _ => bail!("Unknown signature algo"), + } + } } impl Display for ArtifactMetadata { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - ArtifactMetadata::APK { manifest } => { + ArtifactMetadata::APK { + manifest, + signature, + } => { write!( f, - "APK id={}, version={}, code={}", + "APK id={}, version={}, code={}, sig={}", manifest.package.as_ref().unwrap_or(&"missing".to_string()), manifest.version_name.as_ref().unwrap_or(&String::new()), - manifest.version_code.as_ref().unwrap_or(&0) + manifest.version_code.as_ref().unwrap_or(&0), + signature ) } } @@ -233,7 +363,7 @@ impl RepoRelease { self.artifacts .iter() .find_map(|a| match &a.metadata { - ArtifactMetadata::APK { manifest } if manifest.package.is_some() => { + ArtifactMetadata::APK { manifest, .. } if manifest.package.is_some() => { Some(manifest.package.as_ref().unwrap().to_string()) } _ => None, @@ -243,7 +373,7 @@ impl RepoRelease { /// [app_id]@[version] pub fn release_tag(&self) -> Result { - Ok(format!("{}@{}", self.app_id()?, self.version.to_string())) + Ok(format!("{}@{}", self.app_id()?, self.version)) } /// Create nostr release artifact list event @@ -255,13 +385,16 @@ impl RepoRelease { let mut ret = vec![]; let mut b = EventBuilder::new( Kind::Custom(30063), - self.description.as_ref().map(|s| s.as_str()).unwrap_or(""), + self.description.as_deref().unwrap_or(""), ) .tags([ Tag::coordinate(app_coord), Tag::parse(["d", &self.release_tag()?])?, ]); + if let Some(url) = self.url { + b = b.tag(Tag::parse(["url", &url])?); + } for a in &self.artifacts { let eb: Result = a.clone().try_into(); match eb { @@ -317,7 +450,7 @@ async fn load_artifact_url(url: &str) -> Result { .unwrap(), ); if !tmp.exists() { - let mut tmp_file = File::create(&tmp).await?; + let mut tmp_file = tokio::fs::File::create(&tmp).await?; let mut rsp_stream = rsp.bytes_stream(); while let Some(data) = rsp_stream.next().await { if let Ok(data) = data { @@ -325,37 +458,38 @@ async fn load_artifact_url(url: &str) -> Result { } } } - let mut a = load_artifact(&tmp).await?; + let mut a = load_artifact(&tmp)?; // replace location back to URL for publishing a.location = RepoResource::Remote(url.to_string()); Ok(a) } -async fn load_artifact(path: &Path) -> Result { +fn load_artifact(path: &Path) -> Result { match path .extension() .ok_or(anyhow!("missing file extension"))? .to_str() .unwrap() { - "apk" => load_apk_artifact(path).await, + "apk" => load_apk_artifact(path), v => bail!("unknown file extension: {v}"), } } -async fn load_apk_artifact(path: &Path) -> Result { - let file = File::open(path).await?; +fn load_apk_artifact(path: &Path) -> Result { + let file = std::fs::File::open(path)?; + let mut file = std::io::BufReader::new(file); + let sig_block = load_signing_block(&mut file)?; - let mut zip = ZipFileReader::with_tokio(BufReader::new(file)).await?; - let manifest = load_manifest(&mut zip).await?; + let mut zip = ZipArchive::new(file)?; + let manifest = load_manifest(&mut zip)?; let lib_arch: HashSet = list_libs(&mut zip) .iter() .filter_map(|p| { PathBuf::from(p) .iter() - .skip(1) - .next() + .nth(1) .map(|p| p.to_str().unwrap().to_owned()) }) .collect(); @@ -366,7 +500,7 @@ async fn load_apk_artifact(path: &Path) -> Result { name: path.file_name().unwrap().to_str().unwrap().to_string(), size: path.metadata()?.len(), location: RepoResource::Local(path.to_path_buf()), - hash: Some(hash_file(&path).await?), + hash: Some(hash_file(path)?), content_type: "application/vnd.android.package-archive".to_string(), platform: Platform::Android { arch: match lib_arch.iter().next().unwrap().as_str() { @@ -377,15 +511,18 @@ async fn load_apk_artifact(path: &Path) -> Result { v => bail!("unknown architecture: {v}"), }, }, - metadata: ArtifactMetadata::APK { manifest }, + metadata: ArtifactMetadata::APK { + manifest, + signature: sig_block.try_into()?, + }, }) } -async fn hash_file(path: &Path) -> Result> { - let mut file = File::open(path).await?; +fn hash_file(path: &Path) -> Result> { + let mut file = File::open(path)?; let mut hash = Sha256::default(); let mut buf = Vec::with_capacity(4096); - while let Ok(r) = file.read(&mut buf).await { + while let Ok(r) = file.read(&mut buf) { if r == 0 { break; } @@ -394,42 +531,201 @@ async fn hash_file(path: &Path) -> Result> { Ok(hash.finalize().to_vec()) } -async fn load_manifest(zip: &mut ZipFileReader) -> Result +fn load_manifest(zip: &mut ZipArchive) -> Result where - T: AsyncBufRead + AsyncSeek + Unpin, + T: Read + Seek, { - const ANDROID_MANIFEST: &'static str = "AndroidManifest.xml"; + const ANDROID_MANIFEST: &str = "AndroidManifest.xml"; - let idx = zip - .file() - .entries() - .iter() - .enumerate() - .find_map(|(i, entry)| { - if entry.filename().as_bytes() == ANDROID_MANIFEST.as_bytes() { - Some(i) - } else { - None - } - }) - .ok_or(anyhow!("missing AndroidManifest file"))?; - let mut manifest = zip.reader_with_entry(idx).await?; + let mut f = zip.by_name(ANDROID_MANIFEST)?; let mut manifest_data = Vec::with_capacity(8192); - manifest.read_to_end_checked(&mut manifest_data).await?; - let res: AndroidManifest = parse_android_manifest(&manifest_data)?; + let r = f.read_to_end(&mut manifest_data)?; + let res: AndroidManifest = parse_android_manifest(&manifest_data[..r])?; Ok(res) } -fn list_libs(zip: &mut ZipFileReader) -> Vec +#[derive(Debug, Clone)] +struct ApkSigningBlock { + pub data: Vec<(u32, Vec)>, +} + +impl TryInto for ApkSigningBlock { + type Error = anyhow::Error; + + fn try_into(self) -> std::result::Result { + const V2_SIG_BLOCK_ID: u32 = 0x7109871a; + const V3_SIG_BLOCK_ID: u32 = 0xf05368c0; + + if let Some(v3) = + self.data + .iter() + .find_map(|(k, v)| if *k == V3_SIG_BLOCK_ID { Some(v) } else { None }) + { + todo!("Not done yet") + } + if let Some(v2) = + self.data + .iter() + .find_map(|(k, v)| if *k == V2_SIG_BLOCK_ID { Some(v) } else { None }) + { + let v2 = get_length_prefixed_u32_sequence(&v2[4..])?; + let signed_data = get_sequence(v2[0])?; + let digests = get_sequence_kv(signed_data[0])?; + let certificates = get_sequence(signed_data[1])?; + let attributes = get_sequence_kv(signed_data[2])?; + let signatures = get_sequence_kv(v2[1])?; + let public_key = v2[2]; + let digests: HashMap = HashMap::from_iter(digests); + return Ok(ApkSignatureBlock::V2 { + attributes: HashMap::from_iter( + attributes.into_iter().map(|(k, v)| (k, v.to_vec())), + ), + certificates: certificates.into_iter().map(|v| v.to_vec()).collect(), + signatures: signatures + .into_iter() + .filter_map(|(k, v)| { + let sig_len = u32::from_le_bytes(v[..4].try_into().ok()?) as usize; + if sig_len > v.len() - 4 { + warn!("Invalid signature length: {} > {}", sig_len, v.len()); + return None; + } + if let Ok(a) = ApkSignatureAlgo::try_from(k) { + Some(ApkSignature { + algo: a, + digest: digests.get(&k).map(|v| v[4..].to_vec())?, + signature: v[4..sig_len + 4].to_vec(), + }) + } else { + None + } + }) + .collect(), + public_key: public_key.to_vec(), + }); + } + Ok(ApkSignatureBlock::None) + } +} + +fn load_signing_block(zip: &mut R) -> Result where - T: AsyncBufRead + AsyncSeek + Unpin, + R: Read + Seek, { - zip.file() - .entries() - .iter() - .filter_map(|entry| { - if entry.filename().as_bytes().starts_with(b"lib/") { - Some(entry.filename().as_str().unwrap().to_owned()) + const SIG_BLOCK_MAGIC: &[u8] = b"APK Sig Block 42"; + + // scan backwards until we find the singing block + let flen = zip.seek(SeekFrom::End(0))?; + let mut magic_buf = [0u8; 16]; + loop { + let magic_pos = zip.seek(SeekFrom::Current(-17))?; + if magic_pos <= 4 { + bail!("Failed to find signing block"); + } + + zip.read_exact(&mut magic_buf)?; + if magic_buf == SIG_BLOCK_MAGIC { + zip.seek(SeekFrom::Current(-(16 + 8)))?; + let size1 = zip.read_u64::()?; + ensure!(size1 <= flen, "Signing block is larger than entire file"); + + zip.seek(SeekFrom::Current(-(size1 as i64 - 8)))?; + let size2 = zip.read_u64::()?; + ensure!( + size2 == size1, + "Invalid block sizes, {} != {}", + size1, + size2 + ); + + let mut data_bytes = size1 - 8 - 16; + let mut sigs = Vec::new(); + loop { + let (k, v) = read_u64_length_prefixed_kv(zip)?; + data_bytes -= (v.len() + 4 + 8) as u64; + sigs.push((k, v)); + if data_bytes == 0 { + break; + } + } + + zip.seek(SeekFrom::Start(0))?; + return Ok(ApkSigningBlock { data: sigs }); + } + } +} + +#[inline] +fn read_u64_length_prefixed_kv(file: &mut T) -> Result<(u32, Vec)> +where + T: Read + Seek, +{ + let kv_len = file.read_u64::()?; + let k = file.read_u32::()?; + let v_len = kv_len as usize - 4; + let mut v = vec![0; v_len]; + file.read_exact(v.as_mut_slice())?; + Ok((k, v)) +} + +#[inline] +fn get_u64_length_prefixed_kv(slice: &[u8]) -> Result<(u32, &[u8])> { + let kv_len = u64::from_le_bytes(slice[..8].try_into()?); + let k = u32::from_le_bytes(slice[8..12].try_into()?); + Ok((k, &slice[12..(kv_len as usize - 12)])) +} + +#[inline] +fn get_u32_length_prefixed_kv(slice: &[u8]) -> Result<(u32, &[u8])> { + let kv_len = u32::from_le_bytes(slice[..4].try_into()?); + let k = u32::from_le_bytes(slice[4..8].try_into()?); + Ok((k, &slice[8..(kv_len as usize - 8)])) +} + +#[inline] +fn get_length_prefixed_u32(slice: &[u8]) -> Result<&[u8]> { + let len = u32::from_le_bytes(slice[..4].try_into()?); + Ok(&slice[4..4 + len as usize]) +} + +#[inline] +fn get_length_prefixed_u32_sequence(slice: &[u8]) -> Result> { + let sequence_len = u32::from_le_bytes(slice[..4].try_into()?); + get_sequence(&slice[4..4 + sequence_len as usize]) +} + +#[inline] +fn get_sequence(mut slice: &[u8]) -> Result> { + let mut ret = Vec::new(); + while slice.len() >= 4 { + let data = get_length_prefixed_u32(slice)?; + let r_len = data.len() + 4; + slice = &slice[r_len..]; + ret.push(data); + } + Ok(ret) +} + +#[inline] +fn get_sequence_kv(slice: &[u8]) -> Result> { + let seq = get_sequence(slice)?; + Ok(seq + .into_iter() + .map(|s| { + let k = u32::from_le_bytes(s[..4].try_into().unwrap()); + let v = &s[4..]; + (k, v) + }) + .collect()) +} + +fn list_libs(zip: &mut ZipArchive) -> Vec +where + T: Read + Seek, +{ + zip.file_names() + .filter_map(|f| { + if f.starts_with("lib/") { + Some(f.to_string()) } else { None } @@ -437,7 +733,7 @@ where .collect() } -fn parse_android_manifest(data: &Vec) -> Result { +fn parse_android_manifest(data: &[u8]) -> Result { let chunks = if let Chunk::Xml(chunks) = Chunk::parse(&mut Cursor::new(data))? { chunks } else { @@ -528,3 +824,26 @@ fn find_value_in( } }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[test] + fn read_apk() -> Result<()> { + let path = "/home/kieran/Downloads/app-arm64-v8a-release.apk"; + + let apk = load_apk_artifact(&PathBuf::from(path))?; + assert!( + matches!(&apk.platform, Platform::Android { arch } if matches!(arch, Architecture::ARM64 { .. })) + ); + assert!(matches!(&apk.metadata, + ArtifactMetadata::APK { signature, .. } if matches!(signature, + ApkSignatureBlock::V2 { signatures, .. } if signatures.len() == 1 && + matches!(signatures[0].algo, ApkSignatureAlgo::RsaSsaPkcs1Sha256)))); + + eprint!("{}", apk); + Ok(()) + } +}