#!/usr/bin/env bash set -euo pipefail # See https://github.com/ImranR98/Obtainium/blob/main/lib/app_sources/rustore.dart # @describe Download apk from https://rustore.ru # @meta require-tools curl,jq,fzf,xxh64sum,base64,openssl # @flag -u --update download file if it exists but hash not match # @flag -v --verbose verbose output # @flag --persistent-device-id Use persistent device ID (stored in ~/.cache/rustore-apk-get-device-id.txt) # @flag --whats-new show whats new before download # @flag --save-json save package info JSON # @flag --extract extract ZIP on download # @option -q --query search string # @option -o --output=`_default_output` Output directory/file # @arg package_name Application package name # @env RU_GOV_CERTIFICATE_PATH! Path to Russian Ministry of Digital Development (MinCifry) certificates # @env RU_STORE_DEVICE_ID_FILE Path to device id file location # @env RU_STORE_HMAC_KEY=K+eeiCbnVFnZ71KEVal0g5siHaX6v6drh8upeLgEPoU= HMAC key for request signing # @env RU_STORE_APK_CERT=Zh8ggo73gN4LebxZ8mowhkMWNV8w5Pkc+hSiB5GDmRQ= APK certificate for signing # @env RU_STORE_DEVICE_MANUFACTURER_NAME=Google Device manufacturer name # @env RU_STORE_DEVICE_MODEL_NAME=Pixel 8 Pro Device model name # @env RU_STORE_DEVICE_TYPE=mobile Device type # @env RU_STORE_ANDROID_SDK_VER=36 Android SDK version # @env RU_STORE_FIRMWARE_VER=16 Firmware version # @env RU_STORE_FIRMWARE_LANG=ru Firmware language # @env RU_STORE_VER_CODE=1105002 RuStore version code # @env RU_STORE_AGENT=RuStore/1.105.0.2 User agent string BASE_URL=https://backapi.rustore.ru PAGE_SIZE=10 _default_output() { pwd } _generate_device_id() { local part1 part2 part1=$(LC_ALL=C tr -dc "a-z0-9" /dev/null; then local aria2_dir aria2_out aria2_dir=$(dirname "$output_file") aria2_out=$(basename "$output_file") _aria2 --dir="$aria2_dir" --out="$aria2_out" "$download_url" else _curl --progress-bar --output "$output_file" "$download_url" fi } _curl_silent() { _curl --silent "$@" } select_package_name_from_query() { local delimiter=$'\t' _curl_silent --get \ --data-urlencode "query=$argc_query" \ --data-urlencode "pageSize=$PAGE_SIZE" \ "$BASE_URL/applicationData/apps" | jq -r '.body.content[] | [.packageName,.appName,.shortDescription] | @tsv' | column -t -s "$delimiter" -o "$delimiter" | fzf --delimiter "$delimiter" --accept-nth 1 } get_app_info_by_package() { _curl_silent --get "$BASE_URL/applicationData/overallInfo/$1" | jq .body } download_apk() { local app_info="$1" local app_id local package_name local version_name app_id=$(jq -r .appId <<<"$app_info") package_name=$(jq -r .packageName <<<"$app_info") version_name=$(jq -r .versionName <<<"$app_info") local output_file local output_file_hash if [ -d "${argc_output?}" ]; then output_file="$argc_output/$package_name-$version_name.apk" else output_file="$argc_output" fi output_file_hash="${output_file}.xxh64sum" local existing_file_hash if [ -f "$output_file" ] && [ -f "$output_file_hash" ]; then if [ -z "${argc_update:-}" ]; then echo "File already exists: $output_file" exit 1 else existing_file_hash=$(cat "$output_file_hash") fi fi local download_info download_info=$(_curl_silent --request POST --json '{"appId":'"$app_id"'}' "$BASE_URL/applicationData/v2/download-link" | jq .body.downloadUrls[0]) local download_url local expected_hash download_url=$(jq -r .url <<<"$download_info") expected_hash=$(jq -r .hash <<<"$download_info") [ "${existing_file_hash:-}" == "$expected_hash" ] && return [ -n "${argc_whats_new:-}" ] && jq -r '"News: \(.whatsNew)"' <<<"$app_info" temp_file_zip=$(mktemp) temp_file_apk=$(mktemp) trap 'rm -f "$temp_file_zip" "$temp_file_apk"' EXIT _download_file "$temp_file_zip" "$download_url" existing_file_hash=$(xxh64sum "$temp_file_zip" | cut -d ' ' -f 1) if [ "$existing_file_hash" == "$expected_hash" ]; then unzip -p "$temp_file_zip" "*.apk" >"$temp_file_apk" mv "$temp_file_apk" "$output_file" echo "$expected_hash" >"$output_file_hash" else echo "Downloaded file hash not match expected" exit 2 fi } _configure_client() { local content_type="application/json; charset=utf-8" local device_id if [ -n "${argc_persistent_device_id:-}" ]; then local device_filename="${RU_STORE_DEVICE_ID_FILE:-$HOME/.cache/rustore-apk-get-device-id.txt}" if [ -s "$device_filename" ]; then device_id=$(<"$device_filename") else device_id=$(_generate_device_id) echo "$device_id" >"$device_filename" fi else device_id=$(_generate_device_id) fi local device_model="$RU_STORE_DEVICE_MANUFACTURER_NAME $RU_STORE_DEVICE_MODEL_NAME" local user_agent="$RU_STORE_AGENT (Android $RU_STORE_FIRMWARE_VER; SDK $RU_STORE_ANDROID_SDK_VER; arm64-v8a; $device_model; $RU_STORE_FIRMWARE_LANG)" headers=() headers+=("Content-Type:$content_type") headers+=("User-Agent:$user_agent") headers+=("androidSdkVer:$RU_STORE_ANDROID_SDK_VER") headers+=("deviceId:$device_id") headers+=("deviceManufacturerName:$RU_STORE_DEVICE_MANUFACTURER_NAME") headers+=("deviceModelName:$RU_STORE_DEVICE_MODEL_NAME") headers+=("deviceModel:$device_model") headers+=("deviceType:$RU_STORE_DEVICE_TYPE") headers+=("firmwareVer:$RU_STORE_FIRMWARE_VER") headers+=("firmwareLang:$RU_STORE_FIRMWARE_LANG") headers+=("ruStoreVerCode:$RU_STORE_VER_CODE") } _get_signature() { local nonce_b64 nonce_b64=$( _curl_silent \ --request POST \ --url https://api.rustore.ru/v1/secure/nonce | jq -r .nonce ) local rustore_hmac_key_bin rustore_apk_cert_bin nonce_bin rustore_hmac_key_bin=$(echo -n "$RU_STORE_HMAC_KEY" | base64 -d) rustore_apk_cert_bin=$(echo -n "$RU_STORE_APK_CERT" | base64 -d) nonce_bin=$(echo -n "$nonce_b64" | base64 -d) local signature signature=$( { echo -n "$nonce_bin" echo -n "$rustore_apk_cert_bin" } | openssl dgst -sha256 -hmac "$rustore_hmac_key_bin" -binary | base64 ) headers+=("X-Client-Signature:$signature") } main() { _configure_client _get_signature local package_name="${argc_package_name:-}" if [[ "$package_name" == http* ]]; then package_name=$(echo "$package_name" | grep -oP '(?<=/app/)[^/?]+') fi if [ -z "$package_name" ] && [ -n "${argc_query:-}" ]; then package_name=$(select_package_name_from_query) fi [ -z "$package_name" ] && exit 1 local app_info app_info=$(get_app_info_by_package "$package_name") local app_id app_id=$(jq -r .appId <<<"$app_info") if [ -z "${app_id:-}" ]; then echo "No application ID specified or found" exit 1 fi if [ "${argc_verbose:-}" == 1 ]; then jq -r '{appName,versionName,appVerUpdatedAt,shortDescription,whatsNew} | to_entries[] | [.key,.value] | @tsv' <<<"$app_info" | column -t -s $'\t' else jq -r '"\(.appName) - v\(.versionName) [\(.appVerUpdatedAt | strptime("%Y-%m-%dT%H:%M:%S%Z") | strftime("%d.%m.%Y"))]"' <<<"$app_info" fi download_apk "$app_info" } eval "$(argc --argc-eval "$0" "$@")"