feat: load v2 apk signature block

This commit is contained in:
2025-02-14 13:02:50 +00:00
parent 51fe13b105
commit a7df532be4
7 changed files with 427 additions and 309 deletions

225
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -8,15 +8,18 @@ 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"
@ -24,3 +27,4 @@ license: "MIT"
tags:
- "tiktok"
- "shorts"
- "video"

View File

@ -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?;
}

View File

@ -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<String>,
/// Long form app description (with markdown)
pub summary: Option<String>,
/// Repo URL
pub repository: Option<String>,
/// Public project website
pub url: Option<String>,
/// SPDX license code
pub license: Option<String>,
@ -28,32 +34,43 @@ pub struct Manifest {
pub tags: Vec<String>,
}
impl Into<EventBuilder> 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
}
}
pub trait AsStrOrEmpty {
fn as_str_or_empty(&self) -> &str;
}
impl AsStrOrEmpty for Option<String> {
fn as_str_or_empty(&self) -> &str {
self.as_ref().map(|s| s.as_str()).unwrap_or("")
}
}

View File

@ -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};

View File

@ -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<EventBuilder> 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<EventBuilder> 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<ApkSignature>,
public_key: Vec<u8>,
certificates: Vec<Vec<u8>>,
attributes: HashMap<u32, Vec<u8>>,
},
V3 {
signatures: Vec<ApkSignature>,
public_key: Vec<u8>,
},
}
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<u8>,
pub digest: Vec<u8>,
}
#[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<u32> for ApkSignatureAlgo {
type Error = anyhow::Error;
fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
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<String> {
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<EventBuilder> = a.clone().try_into();
match eb {
@ -317,7 +450,7 @@ async fn load_artifact_url(url: &str) -> Result<RepoArtifact> {
.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<RepoArtifact> {
}
}
}
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<RepoArtifact> {
fn load_artifact(path: &Path) -> Result<RepoArtifact> {
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<RepoArtifact> {
let file = File::open(path).await?;
fn load_apk_artifact(path: &Path) -> Result<RepoArtifact> {
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<String> = 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<RepoArtifact> {
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<RepoArtifact> {
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<Vec<u8>> {
let mut file = File::open(path).await?;
fn hash_file(path: &Path) -> Result<Vec<u8>> {
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<Vec<u8>> {
Ok(hash.finalize().to_vec())
}
async fn load_manifest<T>(zip: &mut ZipFileReader<T>) -> Result<AndroidManifest>
fn load_manifest<T>(zip: &mut ZipArchive<T>) -> Result<AndroidManifest>
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()
let mut f = zip.by_name(ANDROID_MANIFEST)?;
let mut manifest_data = Vec::with_capacity(8192);
let r = f.read_to_end(&mut manifest_data)?;
let res: AndroidManifest = parse_android_manifest(&manifest_data[..r])?;
Ok(res)
}
#[derive(Debug, Clone)]
struct ApkSigningBlock {
pub data: Vec<(u32, Vec<u8>)>,
}
impl TryInto<ApkSignatureBlock> for ApkSigningBlock {
type Error = anyhow::Error;
fn try_into(self) -> std::result::Result<ApkSignatureBlock, Self::Error> {
const V2_SIG_BLOCK_ID: u32 = 0x7109871a;
const V3_SIG_BLOCK_ID: u32 = 0xf05368c0;
if let Some(v3) =
self.data
.iter()
.enumerate()
.find_map(|(i, entry)| {
if entry.filename().as_bytes() == ANDROID_MANIFEST.as_bytes() {
Some(i)
.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<u32, &[u8]> = 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
}
})
.ok_or(anyhow!("missing AndroidManifest file"))?;
let mut manifest = zip.reader_with_entry(idx).await?;
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)?;
Ok(res)
.collect(),
public_key: public_key.to_vec(),
});
}
Ok(ApkSignatureBlock::None)
}
}
fn list_libs<T>(zip: &mut ZipFileReader<T>) -> Vec<String>
fn load_signing_block<R>(zip: &mut R) -> Result<ApkSigningBlock>
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::<LittleEndian>()?;
ensure!(size1 <= flen, "Signing block is larger than entire file");
zip.seek(SeekFrom::Current(-(size1 as i64 - 8)))?;
let size2 = zip.read_u64::<LittleEndian>()?;
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<T>(file: &mut T) -> Result<(u32, Vec<u8>)>
where
T: Read + Seek,
{
let kv_len = file.read_u64::<LittleEndian>()?;
let k = file.read_u32::<LittleEndian>()?;
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<Vec<&[u8]>> {
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<Vec<&[u8]>> {
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<Vec<(u32, &[u8])>> {
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<T>(zip: &mut ZipArchive<T>) -> Vec<String>
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<u8>) -> Result<AndroidManifest> {
fn parse_android_manifest(data: &[u8]) -> Result<AndroidManifest> {
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(())
}
}