Files
hash-wallet/.github/workflows/build-ios-sim.yml
T
jwinterm d8aab70fa7 fix: 1.0.2 hotfix — heal wallets broken by per-build random crypto secrets
CI ran tool/generate_new_secrets.dart unpinned, so every release binary
shipped different AES keys/salts for the stored PIN and wallet passwords.
Updating the app then failed to decrypt what the previous build stored:
'Invalid or corrupted pad block' at PIN entry and the 'Corrupted seeds'
dialog, with wallet files fully intact.

- entities/encrypt.dart: legacy-key fallback decode for PIN and wallet
  password blobs (candidate sets recovered from the shipped 1.0.1 binary;
  output validated to reject PKCS7 false positives)
- key_service/auth_service: on successful legacy decode, re-encrypt under
  the pinned key — installs self-heal at first unlock
- all build workflows: inject the eight pinned CRYPTO_* repo secrets after
  secret generation and hard-fail if any is missing, so crypto salts can
  never silently rotate again (values pinned via
  ~keys/set-gitea-crypto-secrets.sh)
- bump 1.0.2+12
2026-08-29 14:53:18 -04:00

368 lines
16 KiB
YAML

name: Hash Bags iOS Simulator build
# Phase 1 of iOS CI: validate the Mac runner end-to-end by producing an
# unsigned simulator .app. Drop the resulting Runner.app into the iOS
# Simulator on any Apple Silicon Mac to verify the app actually compiles
# and launches.
#
# Phase 2 (separate workflow): full TestFlight pipeline with signing.
on:
# Manual-only for now. Trigger via Actions → "Hash Bags iOS Simulator
# build" → Run workflow when you want a build.
workflow_dispatch:
# Cancel in-flight runs when a newer commit lands on the same branch — so a
# burst of commits doesn't pile up N copies of the same long build.
concurrency:
group: ios-sim-${{ github.ref }}
cancel-in-progress: true
defaults:
run:
shell: bash
jobs:
build:
runs-on: macos-latest
env:
APP_IOS_TYPE: cakewallet
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
# CocoaPods refuses to operate when LANG is ASCII-8BIT (default for the
# Gitea runner shell). Force UTF-8 globally so `pod install` succeeds.
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
# Belt-and-suspenders fallback for when the self-hosted runner's internal
# git host presents a self-signed cert. Preferred fix is to trust the
# internal CA on the runner, after which this can be removed. (Runner
# host and network specifics live in the internal CI docs, not this repo.)
NODE_TLS_REJECT_UNAUTHORIZED: '0'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
# Self-hosted Mac runner persists Flutter between runs. Only install if
# it's not already on PATH or version doesn't match.
- name: Check Flutter 3.32.0
id: flutter_check
run: |
if command -v flutter >/dev/null && flutter --version 2>/dev/null | grep -q "3\.32\.0"; then
echo "installed=true" >> "$GITHUB_OUTPUT"
flutter --version
else
echo "installed=false" >> "$GITHUB_OUTPUT"
fi
- name: Install Flutter 3.32.0 (if missing)
if: steps.flutter_check.outputs.installed != 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: '3.32.0'
channel: stable
cache: true
- name: Install CocoaPods (if missing)
run: |
if command -v pod >/dev/null; then
pod --version
exit 0
fi
if command -v brew >/dev/null; then
brew install cocoapods
else
export GEM_HOME="$HOME/.gem"
export PATH="$GEM_HOME/bin:$PATH"
echo "GEM_HOME=$HOME/.gem" >> "$GITHUB_ENV"
echo "$HOME/.gem/bin" >> "$GITHUB_PATH"
gem install --user-install cocoapods --no-document
fi
pod --version
- name: Install Rust + iOS targets (if missing)
run: |
if ! command -v rustup >/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --no-modify-path
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
export PATH="$HOME/.cargo/bin:$PATH"
fi
# Clean any partial downloads from a previously crashed install — those
# are the source of the 'could not rename .partial → final: No such
# file or directory' race that breaks parallel pod builds.
rm -rf "$HOME/.rustup/downloads"/*.partial 2>/dev/null || true
# Pre-install iOS targets serially so the pod build doesn't try to
# do it under concurrency.
rustup target add aarch64-apple-ios
rustup target add aarch64-apple-ios-sim
rustup target add x86_64-apple-ios
rustup show
- name: Install Go + gomobile (if missing)
run: |
if ! command -v go >/dev/null; then
if command -v brew >/dev/null; then
brew install go
else
echo "Go missing and brew not available"; exit 1
fi
fi
go version
# Ensure $(go env GOPATH)/bin is on PATH for subsequent steps.
GOPATH=$(go env GOPATH)
echo "$GOPATH/bin" >> "$GITHUB_PATH"
export PATH="$PATH:$GOPATH/bin"
# gomobile + gobind are needed by scripts/build_bitbox_flutter.sh
if ! command -v gomobile >/dev/null; then
go install golang.org/x/mobile/cmd/gomobile@latest
go install golang.org/x/mobile/cmd/gobind@latest
fi
which gomobile && gomobile version || true
- name: Show toolchain
run: |
set -x
flutter --version
xcodebuild -version
pod --version
uname -m
which curl unzip
# ---- External prebuilt deps (same as android/linux) ------------------
# macOS doesn't ship wget by default; using curl everywhere.
- name: Fetch prebuilt torch_dart
run: |
set -x -e
pushd scripts
rm -rf torch_dart torch_dart.tar.gz
curl -fsSL -o torch_dart.tar.gz https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz
mkdir torch_dart
tar -xzf torch_dart.tar.gz -C torch_dart
rm torch_dart.tar.gz
popd
- name: Fetch prebuilt reown_flutter
run: |
set -x -e
pushd scripts
rm -rf reown_flutter reown_flutter.tar.gz
curl -fsSL -o reown_flutter.tar.gz https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz
mkdir reown_flutter
tar -xzf reown_flutter.tar.gz -C reown_flutter
rm reown_flutter.tar.gz
popd
- name: Clone BitBox Flutter (iOS — skip Android bindings)
run: |
# Pubspec has bitbox_flutter as a path: dep at scripts/bitbox_flutter,
# so the directory must exist for pub get. The bundled build_bindings.sh
# runs `gomobile bind -target=android` which needs the Android SDK we
# don't have on the Mac runner — and the resulting .aar is Android-only.
# iOS uses bitbox_flutter's native ios/Classes plugin, no .aar required.
set -x -e
pushd scripts
if [[ ! -d bitbox_flutter ]]; then
git clone https://github.com/konstantinullrich/bitbox_flutter
fi
cd bitbox_flutter
git fetch -a
git reset --hard
git checkout 5a6e6dd388ef64003f86094af80d5453518b601d
git reset --hard
popd
# ---- Native crypto cores (monero_c prebuilt bundle) ------------------
- name: Fetch prebuilt monero_c bundle
run: |
set -x -e
./scripts/prepare_moneroc.sh
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
mkdir -p "scripts/monero_c/release/$MONERO_C_TAG"
pushd "scripts/monero_c/release/$MONERO_C_TAG"
curl -fsSL -O https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC1/release-bundle.zip
unzip -q release-bundle.zip
rm release-bundle.zip
echo "=== bundle contents (one level) ==="
ls
popd
- name: Inspect iOS targets available in monero_c bundle
run: |
set -x
MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags)
BUNDLE_DIR="scripts/monero_c/release/$MONERO_C_TAG"
echo "=== bundle top-level ==="
ls "$BUNDLE_DIR" || true
echo "=== iOS-y subdirs ==="
find "$BUNDLE_DIR" -maxdepth 1 -type d \( -name '*ios*' -o -name '*apple*' \) 2>/dev/null
echo "=== sample contents of one apple-ios target ==="
for d in "$BUNDLE_DIR"/aarch64-apple-ios "$BUNDLE_DIR"/aarch64-apple-ios-simulator; do
[[ -d "$d" ]] && { echo "--- $d ---"; ls "$d"; }
done
- name: Build MoneroWallet + WowneroWallet XCFrameworks
run: |
set -x -e
pushd scripts/ios
bash ./gen_framework.sh
popd
ls -la ios/MoneroWallet.xcframework ios/WowneroWallet.xcframework
# ---- Configure: pubspec.yaml, Info.plist, GeneratedPluginRegistrant ---
- name: Run iOS configure (cakewallet profile)
run: |
set -x -e
pushd scripts/ios
source ./app_env.sh cakewallet
./app_config.sh
popd
# ---- Secrets ---------------------------------------------------------
- name: Generate per-module secrets.g.dart files (all empty defaults)
run: dart run tool/generate_new_secrets.dart
- name: Pin crypto secrets (never ship random salts)
env:
CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }}
CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }}
CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }}
CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }}
CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }}
CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }}
CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }}
CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }}
run: |
set -e
# generate_new_secrets.dart mints RANDOM salts each run. Shipping
# those breaks decryption of everything the previous build stored
# (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned
# values and refuse to build if any is missing.
for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do
eval "val=\${$v:-}"
[ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; }
done
sed -i.bak \
-e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \
-e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \
-e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \
-e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \
-e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \
-e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \
-e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \
-e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \
lib/.secrets.g.dart
rm -f lib/.secrets.g.dart.bak
for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do
grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; }
done
echo "crypto secrets pinned (values not printed)"
- name: Inject Trocador affiliate secrets
env:
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
TROCADOR_MONERO_API_KEY: ${{ secrets.TROCADOR_MONERO_API_KEY }}
TROCADOR_EXCHANGE_MARKUP: ${{ secrets.TROCADOR_EXCHANGE_MARKUP }}
run: |
# macOS sed needs '' after -i; Linux sed doesn't accept that. This
# workflow runs on macos only.
sed -i '' \
-e "s|const trocadorApiKey = '';|const trocadorApiKey = '${TROCADOR_API_KEY}';|" \
-e "s|const trocadorMoneroApiKey = '';|const trocadorMoneroApiKey = '${TROCADOR_MONERO_API_KEY}';|" \
-e "s|const trocadorExchangeMarkup = '';|const trocadorExchangeMarkup = '${TROCADOR_EXCHANGE_MARKUP:-1}';|" \
lib/.secrets.g.dart
grep '^const trocador' lib/.secrets.g.dart
# ---- Flutter init + codegen -----------------------------------------
- name: Initialize Flutter SDK (iOS precache)
run: |
flutter --version
flutter precache --ios --no-linux --no-android --no-macos --no-windows --no-fuchsia --no-web || true
- name: Build generated code (mobx + hive adapters)
run: bash model_generator.sh
- name: Generate localization
run: dart run tool/generate_localization.dart
- name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec)
run: ./compile_graphics.sh
# CocoaPods: the iOS Podfile installs Flutter plugin pods. Required
# before flutter build can link them.
- name: pod install
run: |
cd ios
pod install --repo-update
# ---- Build iOS Simulator app (no codesign) ---------------------------
- name: Build iOS Simulator app
run: |
set -x -e
flutter build ios --simulator --no-codesign \
--dart-define-from-file=env.json
ls -la build/ios/iphonesimulator/
- name: Zip Runner.app
run: |
cd build/ios/iphonesimulator
zip -r hash_wallet_ios_sim_${{ github.sha }}.zip Runner.app
# Confirm git.such.software resolves to a local target (the Mac itself).
# The runner inherits the Mac's /etc/hosts directly (host-mode act_runner),
# so the user should have:
# 127.0.0.1 git.such.software
# already in /etc/hosts. We only assert that here, don't modify (sudo
# without NOPASSWD prompts for a password the runner can't enter).
#
# macOS's `getent` doesn't exist and `nslookup` bypasses /etc/hosts.
# We grep /etc/hosts directly — that's what Node's getaddrinfo will
# actually consult.
- name: Verify Gitea hostname resolves locally
run: |
if grep -qE '^[[:space:]]*(127\.0\.0\.1|host\.docker\.internal|192\.168\.)[[:space:]]+git\.such\.software' /etc/hosts; then
echo "✓ /etc/hosts points git.such.software at a local target:"
grep git.such.software /etc/hosts
else
echo "✗ /etc/hosts is MISSING a local override for git.such.software."
echo " Node will resolve to the public IP and fail on NAT hairpin."
echo " Add (once, on the Mac mini):"
echo " sudo sh -c 'echo \"127.0.0.1 git.such.software\" >> /etc/hosts'"
exit 1
fi
# Diagnostic: print exactly where upload-artifact will try to send data.
# act_runner injects these env vars; their values tell us whether the
# upload targets git.such.software (uses our /etc/hosts override) or
# a LAN IP directly (override irrelevant). Also probe the URL with
# curl -k so we see the cert + reachability before the real upload.
- name: Diagnose Actions API endpoint
run: |
set +e
echo "=== Actions runtime env ==="
echo "ACTIONS_RUNTIME_URL=${ACTIONS_RUNTIME_URL:-<unset>}"
echo "ACTIONS_RESULTS_URL=${ACTIONS_RESULTS_URL:-<unset>}"
echo "ACTIONS_CACHE_URL=${ACTIONS_CACHE_URL:-<unset>}"
echo "GITHUB_SERVER_URL=${GITHUB_SERVER_URL:-<unset>}"
echo "GITHUB_API_URL=${GITHUB_API_URL:-<unset>}"
echo "NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED:-<unset>}"
echo
# Probe each non-empty URL to see what Node will actually hit.
for var in ACTIONS_RUNTIME_URL ACTIONS_RESULTS_URL GITHUB_SERVER_URL; do
url="${!var}"
[ -z "$url" ] && continue
host=$(echo "$url" | sed -E 's|^https?://([^/:]+).*|\1|')
echo "--- $var → $url ---"
echo "DNS for $host:"
dscacheutil -q host -a name "$host" 2>/dev/null || nslookup "$host" 2>/dev/null || true
echo "HEAD probe (curl -kIv, 10s timeout):"
curl -kIv --max-time 10 "$url" 2>&1 | head -40 || true
echo
done
- name: Upload .app artifact
uses: actions/upload-artifact@v3
with:
name: hash-wallet-ios-sim-${{ github.sha }}
path: build/ios/iphonesimulator/hash_wallet_ios_sim_*.zip
retention-days: 14