Closer/scripts/capture_android_canonical_v...

315 lines
12 KiB
Bash
Executable File

#!/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 <<EOF
Usage: $0 [--check-prereqs] [--update-fixtures]
--check-prereqs Verify adb/firebase/devices/apps are present, then exit.
--update-fixtures Write captured values back into fixture files. Requires
MISMATCHES_FOUND_AND_RESOLVED=1 when mismatches exist.
EOF
}
CHECK_PREREQS=false
UPDATE_FIXTURES=false
for arg in "$@"; do
case "$arg" in
--check-prereqs) CHECK_PREREQS=true ;;
--update-fixtures) UPDATE_FIXTURES=true ;;
-h|--help) usage; exit 0 ;;
*) fail "Unknown argument: $arg" ;;
esac
done
# ---------------------------------------------------------------------------
# Prerequisite checks
# ---------------------------------------------------------------------------
log "Starting from project root: $PROJECT_ROOT"
command -v adb >/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" <<EOF
{
"captured_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"android_canonical_json": {},
"android_commitment_sha256": {},
"ios_canonical_json": {},
"ios_commitment_sha256": {},
"android_argon2id_output_hex": "",
"ios_argon2id_output_hex": "",
"mismatches": [],
"warnings": ["ANDROID_TEST_HARNESS_MISSING: no Android capture target found in this run."]
}
EOF
fi
# ---------------------------------------------------------------------------
# Compare + apply
# ---------------------------------------------------------------------------
MISMATCH_COUNT=0
if command -v python3 >/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