From b07adfff5bc4d0c7830aaeb2402a67e98761bf8b Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 26 Sep 2026 11:16:53 -0400 Subject: [PATCH] ci(macos): port the prerequisites instead of guessing them, and stop printing secrets The first macOS run died after 88 seconds on `cd scripts/monero_c: No such file or directory`. That was the cheap failure. The workflow had been written fresh rather than ported, and behind that missing clone it had also skipped the step that pins the crypto salts. Had it got further it would have shipped a build that mints random salts, which is the 2026-08-29 corrupted-seeds incident on a new platform: wallets created on it would not decrypt after the next upgrade. So the prerequisites are now ported step for step from build-linux.yml, the desktop build known to pass: torch_dart, reown_flutter, bitbox (the Apple variant, without the gomobile Android bindings), prepare_moneroc.sh, configure, secrets generation, salt pinning with the same refusal, Trocador injection, codegen, localization, and compile_graphics.sh. curl for wget and BSD sed's -i.bak form, because the runner is a Mac. Two more things the fresh version got wrong, found while porting: configure was run without sourcing app_env.sh, and app_config.sh writes CFBundleName, the bundle id and the version into Info.plist from variables only app_env.sh sets. The app would have shipped with an empty name. The step now prints the three values it wrote. And app_env.sh hardcoded 1.0.0 (1) on macOS, 6.1.0 (79) on Linux and 1.9.0 (29) in the fish variant, the last two being Cake Wallet's own version numbers. On macOS PlistBuddy writes that into the bundle, so every macOS build would have called itself 1.0.0. Linux only escaped because its substitution looks for a version: 0.0.0 placeholder that pubspec_description.yaml does not contain. All three now derive from pubspec_description.yaml the way Android already does, and the two sh variants resolve to 1.0.4 (14). The fish one is unverified: fish is not installed here and CI does not use it. Separately, four workflows verified their secret injection by grepping lib/.secrets.g.dart into the log, which prints the values. Android's printed the Trocador, WalletConnect, MoonPay and exchange-helper keys. Gitea likely masks registered secrets, but masking is a best-effort layer and values never belong in output. Each now reports every key's name and length and nothing else. Windows is not in this commit. Its run refused correctly, on the guard added in c11c79b0c: the runner has Flutter 3.38.5, not the pinned 3.32.0. That is fixed on the runner, not in the workflow. --- .github/workflows/build-android.yml | 2 +- .github/workflows/build-ios-sim.yml | 2 +- .github/workflows/build-linux.yml | 2 +- .github/workflows/build-macos.yml | 139 +++++++++++++++++++++++++--- .github/workflows/build-windows.yml | 2 +- scripts/linux/app_env.fish | 6 +- scripts/linux/app_env.sh | 9 +- scripts/macos/app_env.sh | 8 +- 8 files changed, 149 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 6a6f259c4..97de57745 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -228,7 +228,7 @@ jobs: -e "s|const moonPayApiKey = '';|const moonPayApiKey = '${MOONPAY_API_KEY}';|" \ -e "s|const exchangeHelperApiKey = '';|const exchangeHelperApiKey = '${EXCHANGE_HELPER_API_KEY}';|" \ lib/.secrets.g.dart - grep -E "^const (trocador|walletConnectProjectId|moonPayApiKey|exchangeHelperApiKey)" lib/.secrets.g.dart + awk -F"'" '/^const (trocador[A-Za-z]*|walletConnectProjectId|moonPayApiKey|exchangeHelperApiKey) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log # ---- Signing: decode upload keystore + write key.properties --------- - name: Decode Android upload keystore + write key.properties diff --git a/.github/workflows/build-ios-sim.yml b/.github/workflows/build-ios-sim.yml index 9a54442bf..8ab88501e 100644 --- a/.github/workflows/build-ios-sim.yml +++ b/.github/workflows/build-ios-sim.yml @@ -270,7 +270,7 @@ jobs: -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 + awk -F"'" '/^const (trocador[A-Za-z]*) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log # ---- Flutter init + codegen ----------------------------------------- - name: Initialize Flutter SDK (iOS precache) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index e15354051..f7c5bfc24 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -202,7 +202,7 @@ jobs: -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 # verify the substitutions + awk -F"'" '/^const (trocador[A-Za-z]*) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log # Initialize Flutter SDK once before parallel-ish work happens. flutter # precache for Flutter 3.32.0 hits a 404 on flutter_gpu.zip (asset not diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 948c389e0..0520d9c1e 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -130,12 +130,58 @@ jobs: gem install --user-install cocoapods fi + # ---- Prerequisites ------------------------------------------------------ + # Ported step for step from build-linux.yml, which is the desktop build + # known to pass, rather than written fresh. The first version of this + # workflow was written fresh: it skipped the clone that creates + # scripts/monero_c and died there, and behind that it had also skipped the + # step that pins the crypto salts. curl instead of wget, and BSD sed's + # -i.bak form, because this runner is a Mac. + - name: Fetch prebuilt torch_dart + run: | + set -euo pipefail + pushd scripts + rm -rf torch_dart torch_dart.tar.gz + curl -fsSL https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -o torch_dart.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 -euo pipefail + pushd scripts + rm -rf reown_flutter reown_flutter.tar.gz + curl -fsSL https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -o reown_flutter.tar.gz + mkdir reown_flutter + tar -xzf reown_flutter.tar.gz -C reown_flutter + rm reown_flutter.tar.gz + popd + + # The iOS variant, not build_bitbox_flutter.sh: that script also builds + # the Android .aar through gomobile, which an Apple target does not use. + - name: Clone BitBox Flutter (Apple — skip Android bindings) + run: | + set -euo pipefail + 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 ---------------------------------------------- - # Same prebuilt bundle the iOS and Android legs use, so all three ship the - # same wallet core rather than three builds of it. + # prepare_moneroc.sh clones scripts/monero_c at the pinned revision. It + # is not a submodule, so checkout alone leaves the directory absent. - name: Fetch prebuilt monero_c bundle run: | set -euo pipefail + ./scripts/prepare_moneroc.sh MONERO_C_TAG=$(cd scripts/monero_c && git describe --tags) echo "monero_c TAG: $MONERO_C_TAG" mkdir -p "scripts/monero_c/release/$MONERO_C_TAG" @@ -161,8 +207,7 @@ jobs: [ -f "$arm" ] || { echo "missing $arm"; exit 1; } # Universal, so one download serves Apple Silicon and Intel. A # single-arch build looks fine on the machine that made it and - # fails on half the users' machines, which is the worst way to - # find out. + # fails on half the users' machines. if [ -f "$x86" ]; then lipo -create "$arm" "$x86" -output "macos/$lib.dylib" else @@ -172,18 +217,90 @@ jobs: lipo -info "macos/$lib.dylib" done - - name: Configure the app (bundle id, version, icons) + # ---- Configure -------------------------------------------------------- + # app_env.sh sets the name, bundle id and version that app_config.sh + # writes into Info.plist; without it PlistBuddy writes empty strings. It + # has to be sourced in the same shell, and app_config.sh has to start in + # scripts/macos because it walks up with relative cds. + - name: Run configure (cakewallet build profile) run: | set -euo pipefail - # Copies pubspec_description.yaml over pubspec.yaml, so the version - # comes from the one file every platform reads. pushd scripts/macos - bash ./app_config.sh + source ./app_env.sh cakewallet + ./app_config.sh popd - grep -m1 '^version:' pubspec.yaml + /usr/libexec/PlistBuddy -c "Print :CFBundleName" macos/Runner/Info.plist + /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" macos/Runner/Info.plist + /usr/libexec/PlistBuddy -c "Print :CFBundleVersion" macos/Runner/Info.plist - - name: flutter pub get - run: flutter pub get + - name: Generate per-module secrets.g.dart files (all empty defaults) + run: | + set -euo pipefail + flutter pub get + 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 -euo pipefail + # 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). The same pinned values as + # every other platform, so a wallet moves between them intact. + 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: | + set -euo pipefail + sed -i.bak \ + -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 + rm -f lib/.secrets.g.dart.bak + awk -F"'" '/^const (trocador[A-Za-z]*) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log + + - name: Build generated code (mobx + hive adapters) + run: bash model_generator.sh + + - name: Generate localization + run: dart run tool/generate_localization.dart + + # res/pictures is the source for every compiled brand asset. Skipping this + # would fall back to raw SVGs, and it is the step whose inputs shipped + # Cake's artwork in 1.0.3, so it runs here exactly as it does elsewhere. + - name: Compile SVG assets (res/pictures/*.svg → assets/new-ui/*.svg.vec) + run: ./compile_graphics.sh - name: Build macOS release run: | diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 6ffdfd0d2..477c527d6 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -444,7 +444,7 @@ jobs: -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 + awk -F"'" '/^const (trocador[A-Za-z]*) = / { split($1, a, " "); v = (length($2) ? "set (" length($2) " chars)" : "EMPTY"); print a[2] ": " v }' lib/.secrets.g.dart # lengths only; values never reach the log # ---- Codegen --------------------------------------------------------- - name: Build generated code (mobx + hive adapters) diff --git a/scripts/linux/app_env.fish b/scripts/linux/app_env.fish index 8dec90ce3..621971692 100644 --- a/scripts/linux/app_env.fish +++ b/scripts/linux/app_env.fish @@ -14,8 +14,10 @@ if test -n "$argv[1]" end set -g CAKEWALLET_NAME "Cake Wallet" -set -g CAKEWALLET_VERSION "1.9.0" -set -g CAKEWALLET_BUILD_NUMBER 29 +# From pubspec_description.yaml, like app_env.sh. These were Cake's 1.9.0 (29). +set -l _pubspec_version (awk -F': ' '/^version:/ {print $2; exit}' (dirname (status --current-filename))/../../pubspec_description.yaml) +set -g CAKEWALLET_VERSION (string split -f1 '+' $_pubspec_version) +set -g CAKEWALLET_BUILD_NUMBER (string split -f2 '+' $_pubspec_version) if not contains -- $APP_LINUX_TYPE $TYPES echo "Wrong app type." diff --git a/scripts/linux/app_env.sh b/scripts/linux/app_env.sh index c00168dcb..c8d876a5d 100755 --- a/scripts/linux/app_env.sh +++ b/scripts/linux/app_env.sh @@ -13,9 +13,14 @@ if [ -n "$1" ]; then APP_LINUX_TYPE=$1 fi +# Version and build come from pubspec_description.yaml, the one file every +# platform reads. These said 6.1.0 (79), Cake Wallet's own numbers, which only +# never reached a binary because app_config.sh's substitution looks for a +# version: 0.0.0 placeholder that pubspec_description.yaml does not contain. +_PUBSPEC_VERSION=$(awk -F': ' '/^version:/ {print $2; exit}' "$(dirname "${BASH_SOURCE[0]:-$0}")/../../pubspec_description.yaml") CAKEWALLET_NAME="Hash Bags" -CAKEWALLET_VERSION="6.1.0" -CAKEWALLET_BUILD_NUMBER=79 +CAKEWALLET_VERSION="${_PUBSPEC_VERSION%+*}" +CAKEWALLET_BUILD_NUMBER="${_PUBSPEC_VERSION#*+}" if ! [[ " ${TYPES[*]} " =~ " ${APP_LINUX_TYPE} " ]]; then echo "Wrong app type." diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index c1a677fbb..46128fefc 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -20,9 +20,13 @@ MONERO_COM_VERSION="6.1.0" MONERO_COM_BUILD_NUMBER=79 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" +# Version and build come from pubspec_description.yaml, the one file every +# platform reads. A hardcoded value here overrides it silently: Android shipped +# versionCode 1 that way once, and this file said 1.0.0 (1) until 1.0.4. +_PUBSPEC_VERSION=$(awk -F': ' '/^version:/ {print $2; exit}' "$(dirname "${BASH_SOURCE[0]:-$0}")/../../pubspec_description.yaml") CAKEWALLET_NAME="Hash Bags" -CAKEWALLET_VERSION="1.0.0" -CAKEWALLET_BUILD_NUMBER=1 +CAKEWALLET_VERSION="${_PUBSPEC_VERSION%+*}" +CAKEWALLET_BUILD_NUMBER="${_PUBSPEC_VERSION#*+}" CAKEWALLET_BUNDLE_ID="com.suchsoftware.hashwallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then