Closer/scripts/capture_android_canonical_v...

325 lines
12 KiB
Bash
Raw Normal View History

#!/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 <<'KNOWN_GAPS' >&2
== KNOWN_GAPS ================================================================
The Android capture harness is now present as:
app/src/androidTest/java/app/closer/crypto/CanonicalVectorCaptureInstrumentTest.kt
It is a DEBUG-ONLY instrument test (never production code). It prints:
- 3 sealed-answer canonical JSON + commitment vectors
- 1 Argon2id known vector
- 1 sealed-answer round-trip sanity check
to logcat with the tag `CanonicalVectorCapture` in this format:
CanonicalVectorCapture name=...;canonical_json=...;commitment_sha256=...
Before running the full paired capture you must:
1. Add the missing androidTest deps to app/build.gradle.kts:
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test:runner:1.5.2")
2. Run the harness on a connected Android emulator:
./gradlew :app:connectedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=app.closer.crypto.CanonicalVectorCaptureInstrumentTest
3. Capture the logcat output:
adb logcat -d CanonicalVectorCapture:I '*:S'
4. Parse the output into the fixture files or extend this script to do it.
Until those steps are wired into CI, this script will capture iOS output only
and report ANDROID_TEST_HARNESS_NOT_INVOKED in the summary.
===============================================================================
KNOWN_GAPS
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