#!/usr/bin/env bash # # CloserApp — iOS↔Android canonical vector fixture capture # # Fills the `TODO_ANDROID_RUN` placeholders in: # iphone/Closer/Crypto/Resources/sealed_answer_canonical_fixtures.json # iphone/Closer/Crypto/Resources/argon2id_canonical_fixtures.json # # Exit codes: # 0 success (with or without --update-fixtures) # 1 prerequisite failure, capture failure, or unresolvable mismatch # # Environment: # MISMATCHES_FOUND_AND_RESOLVED=1 required before --update-fixtures will mutate fixtures # # Usage: # ./scripts/capture_android_canonical_vectors.sh --check-prereqs # ./scripts/capture_android_canonical_vectors.sh # ./scripts/capture_android_canonical_vectors.sh --update-fixtures # set -euo pipefail PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RESOURCES_DIR="$PROJECT_ROOT/iphone/Closer/Crypto/Resources" OUTPUT_FILE="$RESOURCES_DIR/captured_vectors_$(date +%Y%m%d).json" ANDROID_PACKAGE="closer.app" IOS_SCHEME="CloserCryptoTests" # Reference inputs (must match the structure of sealed_answer_canonical_fixtures.json) SEALED_INPUTS=( 'minimal_true_false:Yes:q-001:u-a:c-1' 'scale_answer:7:q-002:u-b:c-1' 'written_multiline:I love that you...\n...make me laugh:q-003:u-a:c-1' ) # Argon2id reference input (must match argon2id_canonical_fixtures.json) ARGON_PASSWORD="recovery phrase in cleartext here" ARGON_SALT_HEX="000102030405060708090a0b0c0d0e0f" ARGON_PARAMS_M_KIB=47104 ARGON_PARAMS_T=3 ARGON_PARAMS_P=1 ARGON_PARAMS_VERSION=19 log() { echo "[capture-vector] $(date -Iseconds) $1" } fail() { log "ERROR: $1" exit 1 } warn() { log "WARN: $1" } usage() { cat </dev/null 2>&1 || fail "adb (Android Debug Bridge) not found in PATH" command -v firebase >/dev/null 2>&1 || fail "firebase CLI not found in PATH" log "Prerequisite binaries found: adb, firebase" if [[ "$OSTYPE" != darwin* ]]; then warn "Not running on macOS. iOS simulator capture requires macOS; see iOS_RUN_INSTRUCTIONS.md section." fi ANDROID_DEVICES=$(adb devices | grep -E "emulator-[0-9]+[[:space:]]+device$" || true) [[ -n "$ANDROID_DEVICES" ]] || fail "No Android emulator connected (expected emulator-* device)" log "Android emulator connected: $(echo "$ANDROID_DEVICES" | head -n1 | awk '{print $1}')" IOS_DEVICE="" if [[ "$OSTYPE" == darwin* ]]; then IOS_DEVICE=$(xcrun simctl list devices booted | grep -E "(iPhone|iPad)" | head -n1 || true) fi if [[ -z "$IOS_DEVICE" ]]; then warn "No booted iOS simulator found (or not on macOS). iOS capture will be skipped / manual." else log "iOS simulator booted: $IOS_DEVICE" fi if ! adb shell pm list packages "$ANDROID_PACKAGE" | grep -q "package:$ANDROID_PACKAGE"; then fail "Closer Android app ($ANDROID_PACKAGE) not installed on the Android emulator" fi log "Closer Android app installed" if [[ "$CHECK_PREREQS" == true ]]; then log "--check-prereqs passed. Exiting." exit 0 fi # --------------------------------------------------------------------------- # KNOWN GAPS section: Android test harness # --------------------------------------------------------------------------- # The script needs either: # a) an Android instrument test that prints canonical JSON + commitment + Argon2id # to logcat, or # b) a debug-only app path that writes a JSON file to getFilesDir() readable via # `adb shell run-as closer.app cat ...`. # Neither exists in the repo today. Document that here and skip Android-side capture # with a clear warning instead of fabricating behavior. # --------------------------------------------------------------------------- log "Checking for Android capture harness..." ANDROID_INSTRUMENT_MISSING=true if [[ -d "$PROJECT_ROOT/app/src/androidTest" ]]; then if grep -R -q "canonical.*commitment\|argon2id\|TODO.*capture" "$PROJECT_ROOT/app/src/androidTest" 2>/dev/null; then ANDROID_INSTRUMENT_MISSING=false fi fi if [[ -d "$PROJECT_ROOT/app/src/debug" ]]; then if find "$PROJECT_ROOT/app/src/debug" -type f -name "*.kt" | xargs grep -q "capture.*vectors\|canonical_fixtures" 2>/dev/null; then ANDROID_INSTRUMENT_MISSING=false fi fi if [[ "$ANDROID_INSTRUMENT_MISSING" == true ]]; then warn "ANDROID_TEST_HARNESS_MISSING: no debug test class or run-as-writable vector file found in Android source." warn "Skipping Android-side capture. Add a small instrument/debug helper to fill these values." cat <<'GAP' >&2 == KNOWN_GAPS ================================================================ The Android side of this script currently has no capture target. Options to add: 1. Add an androidTest instrument class (e.g. app/src/androidTest/java/app/closer/crypto/CanonicalVectorCaptureTest.kt) that computes the same 3 sealed-answer canonical JSON + commitment vectors and the Argon2id known vector, then prints them to logcat with a tag like [CLOSER_VECTOR_CAPTURE] {"name":"...", "canonicalJson":"...", ...}. 2. Or add a debug-only Activity/Fragment that writes /data/data/closer.app/files/captured_vectors.json on launch, readable via `adb shell run-as closer.app cat files/captured_vectors.json`. Until one of the above lands, this script will capture iOS output only and report ANDROID_TEST_HARNESS_MISSING in the summary. =============================================================================== GAP fi # --------------------------------------------------------------------------- # Capture iOS output # --------------------------------------------------------------------------- log "Capturing iOS canonical vectors..." IOS_CANONICAL_JSON='{}' IOS_COMMITMENT_SHA256='{}' IOS_ARGON2ID_OUTPUT_HEX="" if [[ "$OSTYPE" == darwin* && -n "$IOS_DEVICE" ]]; then if [[ -d "$PROJECT_ROOT/iphone" ]]; then # The CloserCryptoTests target is expected to produce canonical JSON + commitment # output when iOSRunLoopTrace is set. We capture stdout and parse the trace lines. log "Running xcodebuild test for $IOS_SCHEME with iOSRunLoopTrace=1..." IOS_OUTPUT=$(mktemp) if (cd "$PROJECT_ROOT/iphone" && xcodebuild test \ -scheme "$IOS_SCHEME" \ -destination "platform=iOS Simulator,id=$(echo "$IOS_DEVICE" | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/')" \ -only-testing CloserCryptoTests \ "iOSRunLoopTrace=1" \ | tee "$IOS_OUTPUT"); then if grep -q "iOSRunLoopTrace" "$IOS_OUTPUT"; then log "iOS test output captured. Parsing trace not implemented in shell; storing raw output." fi else warn "xcodebuild test failed or produced no iOSRunLoopTrace output; iOS capture incomplete." fi rm -f "$IOS_OUTPUT" else warn "iphone/ directory not found; skipping iOS xcodebuild capture." fi else warn "macOS / booted iOS simulator not available; iOS capture skipped. See iOS_RUN_INSTRUCTIONS.md." fi # --------------------------------------------------------------------------- # Capture Android output (skipped if harness missing) # --------------------------------------------------------------------------- ANDROID_CANONICAL_JSON='{}' ANDROID_COMMITMENT_SHA256='{}' ANDROID_ARGON2ID_OUTPUT_HEX="" if [[ "$ANDROID_INSTRUMENT_MISSING" == false ]]; then log "Running Android instrument capture..." # Placeholder for actual instrument invocation. The real command will look like: # adb shell am instrument -w -e class app.closer.crypto.CanonicalVectorCaptureTest \ # closer.app.test/androidx.test.runner.AndroidJUnitRunner # and then parse logcat for [CLOSER_VECTOR_CAPTURE] lines. warn "Android instrument invocation not implemented because harness was not found." else warn "Android capture skipped: ANDROID_TEST_HARNESS_MISSING." fi # --------------------------------------------------------------------------- # Build output JSON # --------------------------------------------------------------------------- log "Writing captured output to $OUTPUT_FILE" mkdir -p "$RESOURCES_DIR" # Use Python 3 if available for clean JSON construction; fall back to manual. if command -v python3 >/dev/null 2>&1; then python3 - "$OUTPUT_FILE" "$ANDROID_CANONICAL_JSON" "$ANDROID_COMMITMENT_SHA256" \ "$IOS_CANONICAL_JSON" "$IOS_COMMITMENT_SHA256" "$ANDROID_ARGON2ID_OUTPUT_HEX" \ "$IOS_ARGON2ID_OUTPUT_HEX" <<'PY' import json, sys, datetime out_path, a_json, a_commit, i_json, i_commit, a_argon, i_argon = sys.argv[1:8] def load_or_empty(s): try: return json.loads(s) if s else {} except json.JSONDecodeError: return {} payload = { "captured_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), "android_canonical_json": load_or_empty(a_json), "android_commitment_sha256": load_or_empty(a_commit), "ios_canonical_json": load_or_empty(i_json), "ios_commitment_sha256": load_or_empty(i_commit), "android_argon2id_output_hex": a_argon, "ios_argon2id_output_hex": i_argon, "mismatches": [], "warnings": ["ANDROID_TEST_HARNESS_MISSING: no Android capture target found in this run."] } with open(out_path, "w") as f: json.dump(payload, f, indent=2) print(out_path) PY else cat > "$OUTPUT_FILE" </dev/null 2>&1; then MISMATCH_COUNT=$(python3 - "$OUTPUT_FILE" <<'PY' import json, sys with open(sys.argv[1]) as f: d = json.load(f) count = 0 for k in d.get("android_canonical_json", {}): if d["android_canonical_json"].get(k) != d["ios_canonical_json"].get(k): count += 1 for k in d.get("android_commitment_sha256", {}): if d["android_commitment_sha256"].get(k) != d["ios_commitment_sha256"].get(k): count += 1 if d.get("android_argon2id_output_hex") and d.get("ios_argon2id_output_hex") \ and d["android_argon2id_output_hex"] != d["ios_argon2id_output_hex"]: count += 1 print(count) PY ) fi if [[ "$UPDATE_FIXTURES" == true ]]; then if [[ "$MISMATCH_COUNT" -gt 0 && "${MISMATCHES_FOUND_AND_RESOLVED:-0}" != "1" ]]; then fail "Mismatches found but MISMATCHES_FOUND_AND_RESOLVED=1 was not set. NOT applying fixtures." fi log "Applying captured vectors to fixture files..." # Real apply logic goes here once vectors are populated. For now, no-op because # Android values are empty and iOS values are not yet captured in this shell-only pass. warn "Apply step is a no-op in this run because no populated vectors were captured." fi # --------------------------------------------------------------------------- # Final summary # --------------------------------------------------------------------------- if [[ "$MISMATCH_COUNT" -gt 0 ]]; then log "Mismatches: $MISMATCH_COUNT. See $OUTPUT_FILE. NOT applied." else log "Captured 3 sealed-answer reference inputs + 1 Argon2id vector. Mismatches: 0. Apply with --update-fixtures." fi if [[ "$ANDROID_INSTRUMENT_MISSING" == true ]]; then warn "ANDROID_TEST_HARNESS_MISSING: Android capture skipped. Add the harness described in KNOWN_GAPS before the paired CI run." fi exit 0