Pular para o conteúdo
Escalibur Wallet
Auditoria

O verificador

tools/verificar.sh

Verificações que valem por afirmação pública sobre a Escalibur Wallet.

608 linhasSHA-256 2895d410902b0995…Abrir no GitHub, no commit 015f5f5
1#!/bin/bash
2# Verificacoes que valem por afirmacao publica sobre a Escalibur Wallet.
3#
4# Cada uma existe porque a alternativa e o app prometer num texto algo que ninguem
5# confere. Aqui qualquer pessoa confere, com uma linha de shell:
6#
7# ./tools/verificar.sh as verificacoes estaticas
8# ./tools/verificar.sh --testes e tambem os testes do nucleo (vetores oficiais)
9# ./tools/verificar.sh --atualizar-simbolos regrava simbolos.lock (revise o diff)
10#
11# O desenho de cada regra esta em docs/seguranca.md §8.3.
12
13set -uo pipefail
14cd "$(dirname "$0")/.." || exit 1
15
16falhas=0
17aviso() { echo " ✗ $1"; falhas=$((falhas + 1)); }
18ok() { echo " ✓ $1"; }
19secao() { echo; echo "$1"; }
20
21CORE=Kit/Sources/EscaliburCore
22CHAINS=Kit/Sources/EscaliburChains
23KEYS=Kit/Sources/EscaliburKeys
24NET=Kit/Sources/EscaliburNetwork
25ENG=Kit/Sources/EscaliburEngines
26APP=App/EscaliburWallet
27
28swift_em() { find "$@" -name "*.swift" 2>/dev/null; }
29# As regras de codigo leem so .swift. Estas sao as outras extensoes que o Xcode ou o
30# SwiftPM compilam ou incluem; fora do C vendorizado (secao "fontes e build"), nenhuma
31# pode existir.
32nao_swift=( -iname "*.m" -o -iname "*.mm" -o -iname "*.c" -o -iname "*.cc" -o -iname "*.cpp" -o -iname "*.cxx" -o -iname "*.c++"
33 -o -iname "*.s" -o -iname "*.asm" -o -iname "*.h" -o -iname "*.hh" -o -iname "*.hpp" -o -iname "*.hxx" -o -iname "*.inc"
34 -o -iname "*.modulemap" -o -iname "*.metal" )
35# Procura um padrao em codigo, ignorando linhas que sao so comentario (a regra vale
36# para o que compila, nao para a explicacao de por que a regra existe).
37procurar() { local padrao="$1"; shift; swift_em "$@" | xargs grep -nE "$padrao" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//'; }
38
39echo
40echo "ESCALIBUR WALLET: verificacoes"
41
42# Caminho com espaco quebra o "find | xargs grep" das regras e deixa arquivo sem
43# conferencia. Nenhum fonte pode ter espaco no caminho.
44com_espaco=$(find Kit/Sources Kit/Tests App tools \( -name "*.swift" -o -name "*.py" -o -name "*.sh" -o "${nao_swift[@]}" \) 2>/dev/null | grep '[[:space:]]')
45if [ -n "$com_espaco" ]; then
46 echo " ✗ arquivo com espaco no caminho escapa das regras: renomeie"
47 echo "$com_espaco"
48 exit 1
49fi
50
51# 1. Chaves e redes nao falam com a internet.
52secao "fronteira de rede"
53achados=$(procurar 'import (Network|WebKit|SafariServices|CFNetwork)\b|URLSession|URLRequest|NWConnection|NWPathMonitor|CFSocket|CFStream|CFHTTP|NSURLConnection|getaddrinfo|\bsocket\(|WKWebView|XMLParser|\b(Input|Output)?Stream\b|getStreamsToHost' "$CORE" "$CHAINS" "$KEYS")
54[ -n "$achados" ] && { aviso "codigo de rede no nucleo, nas redes ou nas chaves"; echo "$achados"; } || ok "nucleo, redes e chaves nao tem codigo de rede"
55
56achados=$(procurar 'NSClassFromString|NSSelectorFromString|objc_getClass|objc_lookUpClass|class_getInstanceMethod|method_exchange|dlopen|dlsym|objc_msgSend|\.perform\(|value\(forKey' "$CORE" "$CHAINS" "$KEYS")
57[ -n "$achados" ] && { aviso "reflexao que contornaria a fronteira"; echo "$achados"; } || ok "sem reflexao no nucleo, nas redes e nas chaves"
58
59achados=$(swift_em "$APP" "$CORE" "$CHAINS" "$KEYS" "$ENG" | xargs grep -lE 'URLSession' 2>/dev/null)
60[ -n "$achados" ] && { aviso "URLSession fora do modulo de rede"; echo "$achados"; } || ok "URLSession so no modulo de rede"
61
62achados=$(procurar 'URLSession\.shared|AsyncImage' "$APP" "$NET" "$CORE" "$CHAINS" "$KEYS" "$ENG")
63[ -n "$achados" ] && { aviso "URLSession.shared ou AsyncImage (gravam cache em disco)"; echo "$achados"; } || ok "sem URLSession.shared e sem AsyncImage"
64
65achados=$(procurar 'import EscaliburKeys|SecureBytes|WalletSecret|RootKeyVault|WalletVault|Mnemonic|HDKey' "$NET")
66[ -n "$achados" ] && { aviso "o modulo de rede enxerga segredo"; echo "$achados"; } || ok "o modulo de rede nao enxerga segredo nem chave"
67
68achados=$(procurar 'import (Security|LocalAuthentication|EscaliburKeys)\b|SecItem[A-Z]|SecKey[A-Z]|SecAccessControl|LAContext' "$NET" "$ENG")
69[ -n "$achados" ] && { aviso "rede ou motores tocam chaveiro, Secure Enclave ou biometria"; echo "$achados"; } || ok "rede e motores nao tocam chaveiro, Secure Enclave nem biometria"
70deps_motores=$(awk '/name: "EscaliburEngines"/{f=1} f&&/dependencies/{print; exit}' Kit/Package.swift)
71echo "$deps_motores" | grep -q 'EscaliburKeys' && aviso "EscaliburEngines depende de EscaliburKeys no Package.swift" || ok "EscaliburEngines nao depende de EscaliburKeys (o compilador garante)"
72deps_rede=$(awk '/name: "EscaliburNetwork"/{f=1} f&&/dependencies/{print; exit}' Kit/Package.swift)
73echo "$deps_rede" | grep -q 'EscaliburKeys' && aviso "EscaliburNetwork depende de EscaliburKeys no Package.swift" || ok "EscaliburNetwork nao depende de EscaliburKeys (o compilador garante)"
74
75achados=$(procurar 'SecureBytes|WalletSecret|RootKeyVault|WalletVault|KeychainStore|SecureEnclaveWrapper|Signer\b|Mnemonic|HDKey|SecretStore' "$ENG")
76[ -n "$achados" ] && { aviso "os motores enxergam segredo, cofre ou assinador"; echo "$achados"; } || ok "os motores so usam dado publico do modulo de chaves"
77
78achados=$(procurar 'import (Network|WebKit|SafariServices|CFNetwork)\b|URLRequest|NWConnection|WKWebView' "$ENG")
79[ -n "$achados" ] && { aviso "os motores falam com a rede sem passar pelo modulo de rede"; echo "$achados"; } || ok "os motores so falam com a rede pelo modulo de rede"
80
81achados=$(procurar 'SigningPlan\(' "$APP" "$KEYS" "$NET" "$ENG" "$CORE")
82[ -n "$achados" ] && { aviso "plano de assinatura construido fora das redes"; echo "$achados"; } || ok "plano de assinatura so nasce em EscaliburChains"
83
84achados=$(procurar 'Secp256k1\.sign|Ed25519\.sign' "$CHAINS" "$NET" "$APP" "$ENG")
85[ -n "$achados" ] && { aviso "assinatura fora do modulo de chaves"; echo "$achados"; } || ok "so o modulo de chaves assina"
86
87# 2. Hosts travados.
88secao "hosts"
89hosts_atuais=$(grep -ohE 'https://[A-Za-z0-9.-]+' $(swift_em "$NET" "$ENG") "$APP/Resources/Info.plist" 2>/dev/null | sort -u)
90gerar_allowed_hosts() {
91 {
92 echo "// Gerado por tools/verificar.sh --atualizar-hosts a partir do hosts.lock. Nao editar."
93 echo "// O HTTPClient recusa qualquer host fora desta lista, em tempo de execucao."
94 echo "enum AllowedHosts {"
95 echo " static let all: Set<String> = ["
96 sed 's|https://||' hosts.lock | while read -r host; do echo " \"$host\","; done
97 echo " ]"
98 echo "}"
99 } > "$NET/AllowedHosts.swift"
100}
101if [ "${1:-}" = "--atualizar-hosts" ]; then
102 echo "$hosts_atuais" > hosts.lock
103 gerar_allowed_hosts
104 ok "hosts.lock e AllowedHosts.swift atualizados"
105elif [ ! -f hosts.lock ]; then
106 aviso "hosts.lock nao existe (gere com --atualizar-hosts e revise o diff)"
107elif ! diff -q <(echo "$hosts_atuais") hosts.lock >/dev/null; then
108 aviso "a lista de hosts mudou; revise e rode --atualizar-hosts"
109 diff <(echo "$hosts_atuais") hosts.lock
110else
111 ok "$(wc -l < hosts.lock | tr -d ' ') hosts conferem com hosts.lock"
112fi
113achados=$(swift_em "$APP" "$NET" "$CORE" "$CHAINS" "$KEYS" | xargs grep -nE '"http://' 2>/dev/null)
114[ -n "$achados" ] && { aviso "http sem TLS"; echo "$achados"; } || ok "nenhum http sem TLS"
115
116if /usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsArbitraryLoads" "$APP/Resources/Info.plist" 2>/dev/null | grep -q true; then
117 aviso "ATS com NSAllowsArbitraryLoads ligado"
118elif grep -qE 'NSExceptionAllowsInsecureHTTPLoads|NSAllowsLocalNetworking' "$APP/Resources/Info.plist"; then
119 aviso "ATS com excecao"
120else
121 ok "ATS padrao, sem excecao"
122fi
123
124# 3. Dependencias.
125esperado=$(sed 's|https://||' hosts.lock 2>/dev/null | sort)
126gerado=$(grep -oE '^ "[^"]+",$' "$NET/AllowedHosts.swift" 2>/dev/null | tr -d ' ",' | sort)
127[ "$esperado" = "$gerado" ] && ok "o cliente HTTP recusa em tempo de execucao todo host fora do hosts.lock" || aviso "AllowedHosts.swift difere do hosts.lock (rode --atualizar-hosts)"
128grep -q 'allowedHosts.contains(host)' "$NET/HTTPClient.swift" && ok "HTTPClient confere o host de toda requisicao" || aviso "HTTPClient deixou de conferir o host"
129
130secao "dependencias"
131if grep -q "XCRemoteSwiftPackageReference" EscaliburWallet.xcodeproj/project.pbxproj 2>/dev/null; then aviso "pacote remoto no projeto"; else ok "nenhum pacote remoto no projeto"; fi
132# Qualquer `.package(`, com a url na mesma linha ou nao, e `path:`, `id:` e `name:` tambem
133# (auditoria 6, A6-14): o pacote nao tem dependencia nenhuma de fora.
134if grep -qE '\.package[[:space:]]*\(' Kit/Package.swift; then aviso "dependencia de pacote no Package.swift"; else ok "nenhum pacote remoto no Package.swift"; fi
135achados=$(find . -name Package.resolved -not -path "./build/*" -not -path "*/.build/*" -not -path "./.git/*" 2>/dev/null)
136[ -n "$achados" ] && { aviso "Package.resolved: alguma dependencia foi resolvida"; echo "$achados"; } || ok "nenhum Package.resolved"
137locais=$(grep -oE 'relativePath = [^;]+;' EscaliburWallet.xcodeproj/project.pbxproj 2>/dev/null | sort -u)
138[ -z "$locais" ] || [ "$locais" = "relativePath = Kit;" ] && ok "o unico pacote local do projeto e o Kit" || { aviso "pacote local fora do Kit no projeto"; echo "$locais"; }
139if grep -q '\.binaryTarget(' Kit/Package.swift; then aviso "alvo binario no Package.swift"; else ok "nenhum alvo binario"; fi
140achados=$(find . \( -name "*.xcframework" -o -name "*.framework" -o -name "*.a" -o -name "*.dylib" -o -name Podfile -o -name Cartfile \) -not -path "./build/*" -not -path "*/.build/*" -not -path "./.git/*" -not -path "./.claude/*" 2>/dev/null)
141[ -n "$achados" ] && { aviso "binario ou gerenciador de terceiro"; echo "$achados"; } || ok "nenhum binario de terceiro"
142
143# 3b. Fontes e build. As regras de rede, segredo e registro leem so .swift, e a pasta do
144# app inteira e fonte do alvo (project.yml): um .m ou .c ali compilaria sem passar por
145# nenhuma delas. O unico codigo que nao e Swift e o C vendorizado, travado por digesto.
146# Um shader .metal so entra pela lista abaixo, revisado como codigo (hoje, nenhum).
147secao "fontes e build"
148# Caminhos separados por espaco; a lista so vale para .metal.
149metal_permitidos=""
150achados=$(find App Kit/Sources Kit/Tests -type f \( "${nao_swift[@]}" \) -not -path "Kit/Sources/CArgon2/*" -not -path "Kit/Sources/CSecp256k1/*" 2>/dev/null \
151 | while read -r fonte; do
152 [[ "$fonte" == *.metal ]] && printf '%s\n' $metal_permitidos | grep -qxF "$fonte" && continue
153 echo "$fonte"
154 done)
155[ -n "$achados" ] && { aviso "fonte que nao e Swift fora do C vendorizado (CArgon2 e CSecp256k1)"; echo "$achados"; } \
156 || ok "fora do C vendorizado (CArgon2 e CSecp256k1), todo fonte e Swift"
157# Nada roda nem entra no binario por fora do fonte: fase de script de build (shell a cada
158# build, com o repositorio e a rede na mao), acao de esquema, plugin do SwiftPM, framework
159# ou biblioteca que nao e do SDK, flag de linker (carregaria codigo de fora). Hoje nao ha
160# nenhum; o que for legitimo um dia entra aqui por nome.
161pbx=EscaliburWallet.xcodeproj/project.pbxproj
162achados=$( {
163 grep -nE 'PBXShellScriptBuildPhase|shellScript|PBXBuildRule|OTHER_LDFLAGS|OTHER_LIBTOOLFLAGS|-Xlinker' "$pbx" | sed "s|^|$pbx:|"
164 grep -nE '(lastKnownFileType|explicitFileType) = "?(wrapper\.(xc)?framework|archive\.ar|compiled\.mach-o\.dylib|sourcecode\.text-based-dylib-definition)' "$pbx" \
165 | grep -v 'sourceTree = SDKROOT' | sed "s|^|$pbx:|"
166 grep -nHE 'ShellScriptAction|<PreActions|<PostActions' EscaliburWallet.xcodeproj/xcshareddata/xcschemes/*.xcscheme
167 grep -nE 'preBuildScripts|postCompileScripts|postBuildScripts|buildRules|buildToolPlugins|preActions|postActions|OTHER_LDFLAGS|OTHER_LIBTOOLFLAGS|-Xlinker|^[[:space:]]*- (framework|carthage|bundle):' project.yml \
168 | sed 's|^|project.yml:|'
169 grep -nE 'linkerSettings|unsafeFlags|\.plugin\(|plugins:|\.systemLibrary\(|linkedFramework|linkedLibrary' Kit/Package.swift | sed 's|^|Kit/Package.swift:|'
170} 2>/dev/null)
171[ -n "$achados" ] && { aviso "script de build, framework fora do SDK ou flag de linker no projeto"; echo "$achados"; } \
172 || ok "sem fase de script, acao de esquema, plugin de build, framework fora do SDK nem flag de linker"
173
174# 3c. Workflows do GitHub. O que roda no CI e no release tambem e cadeia de suprimentos:
175# toda acao e da propria GitHub (actions/ ou github/) e fixada por commit, 40 hexadecimais,
176# nunca por tag movel; a outra forma aceita e um workflow deste repositorio. O repositorio
177# exige o mesmo nas configuracoes, mas so o dono ve; aqui qualquer um confere. E segredo
178# so aparece em job do environment release, o que tem revisor obrigatorio
179# (docs/release.md); passar segredo adiante (`secrets:`) tambem falha.
180secao "workflows"
181achados=$(grep -nE '^[[:space:]-]*uses:' .github/workflows/*.yml \
182 | grep -vE 'uses:[[:space:]]*((actions|github)/[A-Za-z0-9._/-]+@[0-9a-f]{40}|\./\.github/workflows/[A-Za-z0-9._-]+\.yml)([[:space:]]|$)')
183[ -n "$achados" ] && { aviso "acao de fora da GitHub ou sem SHA num workflow"; echo "$achados"; } \
184 || ok "toda acao dos workflows vem da propria GitHub, fixada por SHA"
185achados=$(awk '
186 function fecha( i) { if (job != "" && !release) for (i = 1; i <= n; i++) print guardadas[i]; job = ""; n = 0; release = 0 }
187 FNR == 1 { fecha(); emjobs = 0 }
188 /^jobs:/ { emjobs = 1; next }
189 emjobs && /^ [A-Za-z0-9_-]+:[[:space:]]*$/ { fecha(); job = $1; next }
190 /^[[:space:]]*#/ { next }
191 job != "" && /^ environment:[[:space:]]*release[[:space:]]*$/ { release = 1 }
192 /^[[:space:]]*secrets:/ { print FILENAME ":" FNR ": " $0; next }
193 /secrets\./ { if (job == "") print FILENAME ":" FNR ": " $0; else guardadas[++n] = FILENAME ":" FNR ": " $0 }
194 END { fecha() }
195' .github/workflows/*.yml)
196[ -n "$achados" ] && { aviso "segredo fora de job do environment release"; echo "$achados"; } \
197 || ok "segredo so em job do environment release"
198
199# 4. Codigo vendorizado travado por digesto.
200secao "codigo vendorizado"
201conferir_lock() {
202 local lock="$1" base="$2" nome="$3"
203 local divergentes=0 total=0
204 while IFS= read -r linha; do
205 arquivo=$(echo "$linha" | sed -n 's/^ "\([^"]*\)": "\([0-9a-f]\{64\}\)".*/\1/p')
206 esperado=$(echo "$linha" | sed -n 's/^ "\([^"]*\)": "\([0-9a-f]\{64\}\)".*/\2/p')
207 [ -z "$arquivo" ] && continue
208 total=$((total + 1))
209 atual=$(shasum -a 256 "$base/$arquivo" 2>/dev/null | cut -d' ' -f1)
210 [ "$atual" != "$esperado" ] && { aviso "$nome/$arquivo nao confere"; divergentes=$((divergentes + 1)); }
211 done < "$lock"
212 [ "$divergentes" = "0" ] && ok "$total arquivos de $nome conferem com $lock"
213}
214conferir_lock secp256k1.lock Kit/Sources/CSecp256k1/upstream secp256k1
215conferir_lock argon2.lock Kit/Sources/CArgon2 argon2
216conferir_lock modelos.lock App/EscaliburWallet/Resources/Moeda3D "moeda 3D"
217fora_do_lock=$(comm -23 <(ls App/EscaliburWallet/Resources/Moeda3D | sort) <(sed -n 's/^ "\([^"]*\)": "[0-9a-f]\{64\}".*/\1/p' modelos.lock | sort))
218[ -n "$fora_do_lock" ] && { aviso "arquivo na moeda 3D fora do modelos.lock"; echo "$fora_do_lock"; } || ok "a pasta da moeda 3D so tem o que o modelos.lock lista"
219divergentes=0
220for idioma in $(sed -n 's/^ *"\([a-z_]*\)": "[0-9a-f]*".*/\1/p' wordlists.lock); do
221 esperado=$(sed -n "s/^ *\"$idioma\": \"\([0-9a-f]*\)\".*/\1/p" wordlists.lock)
222 atual=$(shasum -a 256 "$CORE/Resources/Wordlists/$idioma.txt" | cut -d' ' -f1)
223 [ "$atual" != "$esperado" ] && { aviso "lista $idioma nao confere"; divergentes=$((divergentes + 1)); }
224done
225[ "$divergentes" = "0" ] && ok "as 10 listas BIP-39 conferem com wordlists.lock"
226divergentes=0
227for lista in $(sed -n 's/^ *"\([a-z-]*\)": "[0-9a-f]*".*/\1/p' forca.lock); do
228 esperado=$(sed -n "s/^ *\"$lista\": \"\([0-9a-f]*\)\".*/\1/p" forca.lock)
229 atual=$(shasum -a 256 "$CORE/Resources/Forca/$lista.txt" | cut -d' ' -f1)
230 [ "$atual" != "$esperado" ] && { aviso "lista de forca $lista nao confere"; divergentes=$((divergentes + 1)); }
231done
232[ "$divergentes" = "0" ] && ok "as listas do medidor de forca conferem com forca.lock"
233
234# 5. Registro e aleatoriedade.
235# Decifrador de referencia (so para os testes, fora do app): decifrar.py e FORMATO.md
236# travados por digesto. O app nao distribui decifrador para computador.
237if (cd tools/referencia && shasum -a 256 -c ../../recuperacao.lock >/dev/null 2>&1); then
238 ok "decifrador de referencia confere com recuperacao.lock"
239else
240 aviso "decifrador de referencia difere de recuperacao.lock"
241fi
242[ -e "$APP/Resources/Recuperacao" ] && aviso "decifrador para computador de volta no app" || ok "nenhum decifrador para computador dentro do app"
243
244secao "registro e aleatoriedade"
245achados=$(procurar '(^|[^A-Za-z0-9_.])(print|debugPrint|dump|NSLog)\(' "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET")
246[ -n "$achados" ] && { aviso "print/NSLog fora dos testes"; echo "$achados"; } || ok "nenhum print fora dos testes"
247achados=$(procurar 'Logger\(|os_log' "$KEYS" "$CHAINS")
248[ -n "$achados" ] && { aviso "logger no modulo de chaves ou de redes"; echo "$achados"; } || ok "chaves e redes nao registram nada"
249achados=$(procurar '\.random\(|randomElement|shuffled\(\)|arc4random|drand48|\brand\(|SystemRandomNumberGenerator|GKRandom' "$CORE" "$CHAINS" "$KEYS" "$APP" | grep -v 'SecureBytes\.random(')
250[ -n "$achados" ] && { aviso "aleatoriedade fora do SecRandomCopyBytes"; echo "$achados"; } || ok "a unica fonte de aleatoriedade e o SecRandomCopyBytes"
251
252# 6. Chaveiro e biometria.
253secao "chaveiro e biometria"
254achados=$(swift_em "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET" | xargs grep -lE 'SecItemAdd|SecItemUpdate|SecKeyCreateRandomKey' 2>/dev/null | grep -vE "$KEYS/(SecretStore|Enclave)\.swift")
255[ -n "$achados" ] && { aviso "chamada ao chaveiro fora de SecretStore/Enclave"; echo "$achados"; } || ok "chaveiro so em SecretStore.swift e Enclave.swift"
256# Tambem os outros caminhos para uma flag que sobrevive a um rosto novo: o nome antigo
257# (.touchIDAny), as flags montadas por numero (rawValue) e flags(for:) chamada com uma chave
258# escrita (so a propria chave de cada criacao passa por ela).
259achados=$(procurar 'AfterFirstUnlock|AccessibleAlways|kSecAttrAccessibleWhenUnlocked\b|SynchronizableAny|\.userPresence|\.devicePasscode|\.or\b|kSecAttrSynchronizable as String: kCFBooleanTrue|\.touchIDAny|SecAccessControlCreateFlags\(rawValue|flags\(for: *\.' "$APP" "$KEYS")
260[ -n "$achados" ] && { aviso "protecao fraca de chaveiro ou biometria"; echo "$achados"; } || ok "sem userPresence, devicePasscode, .or, touchIDAny, flags por numero, AfterFirstUnlock, WhenUnlocked sem ThisDeviceOnly"
261# O .biometryAny tem uma excecao so: a kand.qualquer, a camada de fora do modo reforcado que
262# sobrevive a um rosto novo (docs/seguranca.md §2.6), na linha dela em flags(for:) do
263# Enclave.swift. Ela nunca abre nada sozinha: o unico embrulho com ela e o do miolo (que ainda
264# pede o PIN pela kand.pin), no construtor do registro, e o app nem a nomeia.
265excecao_any=$(awk '/static func flags\(for slot: WrapSlot\)/{f=1} f&&/^ }$/{f=0} f&&/^ case \.hardenedBiometryAny: return \[\.privateKeyUsage, \.biometryAny\]$/{print FILENAME":"FNR}' "$KEYS/Enclave.swift")
266achados=$(procurar '\.biometryAny' "$APP" "$KEYS")
267if [ "$(echo "$excecao_any" | sed '/^$/d' | wc -l | tr -d ' ')" != "1" ]; then
268 aviso "a excecao do .biometryAny saiu de flags(for:) da kand.qualquer"
269 echo "$achados"
270else
271 fora=$(echo "$achados" | sed '/^$/d' | grep -vF "$excecao_any:")
272 [ -n "$fora" ] && { aviso ".biometryAny fora da kand.qualquer em flags(for:)"; echo "$fora"; } || ok "biometryAny so na kand.qualquer, em flags(for:) do Enclave.swift"
273fi
274embrulho_any=$(awk '/private func buildHardenedRecord\(/{f=1} f&&/^ }$/{f=0} f&&/^ layers\[\.hardenedBiometryAny\] = try wrapper\.wrap\(middle, slot: \.hardenedBiometryAny\)$/{print FILENAME":"FNR}' "$KEYS/RootKeyVault.swift")
275achados=$(procurar 'slot: *\.hardenedBiometryAny|wrap\([^)]*\.hardenedBiometryAny' "$APP" "$KEYS")
276if [ "$(echo "$embrulho_any" | sed '/^$/d' | wc -l | tr -d ' ')" != "1" ]; then
277 aviso "o embrulho do miolo pela kand.qualquer saiu do construtor do registro"
278 echo "$achados"
279else
280 fora=$(echo "$achados" | sed '/^$/d' | grep -vF "$embrulho_any:")
281 [ -n "$fora" ] && { aviso "kand.qualquer embrulhando fora do construtor do registro"; echo "$fora"; } || ok "a kand.qualquer so embrulha o miolo, no construtor do registro"
282fi
283achados=$(procurar 'hardenedBiometryAny|kand\.qualquer' "$APP")
284[ -n "$achados" ] && { aviso "o app nomeia a kand.qualquer"; echo "$achados"; } || ok "o app nao nomeia a kand.qualquer"
285if ! grep -q 'kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly' "$KEYS/SecretStore.swift"; then aviso "WhenPasscodeSetThisDeviceOnly sumiu"; else ok "itens em WhenPasscodeSetThisDeviceOnly"; fi
286# A unica excecao (simulador) precisa estar atras de targetEnvironment(simulator).
287excecao=$(awk '/#if targetEnvironment\(simulator\)/{d=1} /#else|#endif/{d=0} /kSecAttrAccessibleWhenUnlockedThisDeviceOnly/{ if(!d) print FILENAME": "NR }' "$KEYS/SecretStore.swift")
288[ -n "$excecao" ] && { aviso "classe de simulador fora do bloco de simulador"; echo "$excecao"; } || ok "a excecao do simulador so compila para simulador"
289achados=$(awk 'FNR==1{d=0} /#if targetEnvironment\(simulator\)/{d=1} /#else|#endif/{d=0} /SimulatorWrapper\(\)|SoftwareWrapper\(\)/{ if(!d) print FILENAME": "FNR": "$0 }' $(swift_em "$APP" "$KEYS"))
290[ -n "$achados" ] && { aviso "embrulho em software fora do simulador"; echo "$achados"; } || ok "embrulho em software so no simulador e nos testes"
291achados=$(swift_em "$CORE" "$CHAINS" "$KEYS" "$NET" | xargs grep -nE 'class (MemoryStore|SoftwareWrapper)\b' 2>/dev/null)
292[ -n "$achados" ] && { aviso "duble de teste dentro do codigo que vai para o app"; echo "$achados"; } || ok "armazenamento em memoria e embrulho em software so existem nos testes"
293achados=$(swift_em "$APP" "$KEYS" | xargs grep -n 'evaluatePolicy(' 2>/dev/null | grep -v 'canEvaluatePolicy' | grep -v "$APP/Services/KeyServices.swift")
294[ -n "$achados" ] && { aviso "evaluatePolicy fora do simulador (booleano nao autoriza nada)"; echo "$achados"; } || ok "nenhum booleano de biometria autoriza operacao"
295
296# 6b. Segredo em String e comparacao de segredo. Grep com falso positivo possivel:
297# cada excecao e uma linha em tools/excecoes.allow ("arquivo|trecho|por que"),
298# revisada como codigo.
299secao "segredo"
300filtrar_excecoes() {
301 local entrada
302 entrada=$(cat)
303 [ -z "$entrada" ] && return
304 while IFS='|' read -r arquivo trecho _; do
305 [ -z "$arquivo" ] || [ "${arquivo#\#}" != "$arquivo" ] && continue
306 entrada=$(echo "$entrada" | grep -vF "$arquivo:" ; echo "$entrada" | grep -F "$arquivo:" | grep -vF "$trecho")
307 entrada=$(echo "$entrada" | sed '/^$/d')
308 done < tools/excecoes.allow
309 echo "$entrada"
310}
311achados=$(swift_em "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET" "$ENG" | xargs grep -niE '(var|let) +[A-Za-z_]*(pin|senha|password|seed|mnemonic|phrase|entropy|privatekey|secret)[A-Za-z_]* *(: *String|= *"")|@State[^=]*(pin|senha|seed|mnemonic|phrase)[A-Za-z_]* *(: *String|= *"")' 2>/dev/null \
312 | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//' | filtrar_excecoes)
313[ -n "$achados" ] && { aviso "segredo guardado em String"; echo "$achados"; } || ok "nenhum segredo guardado em String (fora das excecoes revisadas)"
314achados=$(procurar '(digest|tag|mac|hmac|secret|key)[A-Za-z_]* *[!=]=' "$KEYS" | filtrar_excecoes)
315[ -n "$achados" ] && { aviso "segredo comparado com == (use Hash.constantTimeEqual)"; echo "$achados"; } || ok "segredo so comparado em tempo constante no modulo de chaves"
316achados=$(procurar '(Data|String|NSData|NSString|NSArray|NSDictionary)\(contentsOf: *URL\(string' "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET" "$ENG")
317[ -n "$achados" ] && { aviso "leitura de URL remota fora do cliente HTTP"; echo "$achados"; } || ok "nenhuma leitura de rede por contentsOf"
318achados=$(procurar '(Data|String|NSData|NSString)\(contentsOf:' "$CHAINS" "$KEYS" "$ENG")
319[ -n "$achados" ] && { aviso "leitura de arquivo em redes, chaves ou motores"; echo "$achados"; } || ok "redes, chaves e motores nao leem arquivo"
320
321# 7. Plataforma.
322secao "plataforma"
323grep -q 'shouldAllowExtensionPointIdentifier' "$APP/App/PlatformGuards.swift" && grep -q '\.keyboard' "$APP/App/PlatformGuards.swift" \
324 && ok "teclados de terceiros bloqueados" || aviso "bloqueio de teclado de terceiro sumiu"
325grep -q 'willResignActiveNotification' "$APP/App/PlatformGuards.swift" && ok "cobertura antes da foto do seletor de apps" || aviso "cobertura do seletor sumiu"
326grep -q 'NSFileProtectionComplete' "$APP/Resources/EscaliburWallet.entitlements" && ok "NSFileProtectionComplete em todo o conteiner" || aviso "protecao de arquivo sumiu"
327grep -q 'sceneCaptureState' "$APP/App/PlatformGuards.swift" && grep -q 'isCaptured' "$APP/App/PlatformGuards.swift" \
328 && ok "tela sensivel some com gravacao, espelhamento ou captura da cena" || aviso "defesa contra captura de tela incompleta"
329# A camada segura contra captura acha a vista de desenho do campo seguro so pela
330# hierarquia publica: nome de classe privada, seletor ou KVC ali e API privada por nome
331# (docs/seguranca.md §7).
332camada="$APP/Design/SecureCaptureContainer.swift"
333if [ -f "$camada" ] && grep -q 'isSecureTextEntry = true' "$camada"; then
334 achados=$(grep -nE 'NSClassFromString|NSSelectorFromString|Selector\(|#selector|forKey|perform\(|dlsym|objc_|"_[A-Za-z]' "$camada" | grep -vE '^[0-9]+:[[:space:]]*//')
335 [ -n "$achados" ] && { aviso "camada segura contra captura com API privada por nome"; echo "$achados"; } || ok "camada segura contra captura so com API publica"
336else
337 aviso "camada segura contra captura sumiu"
338fi
339grep -q 'isExcludedFromBackup = true' "$APP/Services/Metadata.swift" && ok "metadados fora do backup do iCloud" || aviso "metadados podem ir para o backup"
340# Protecao mais fraca que a Complete: so o marcador de instalacao, o arquivo dos
341# alertas de preco (nao diz o que a carteira tem), o do aviso de recebimento (so os
342# enderecos publicos), o das ordens limite abertas (so o UID de cada uma) e o do widget
343# (so a moeda e o preco), que precisam abrir com o iPhone bloqueado.
344achados=$(procurar 'UntilFirstUserAuthentication|completeUnlessOpen|CompleteUnlessOpen|FileProtectionType\.none|noFileProtection' "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET" "$ENG" \
345 | grep -vE "^$APP/(Services/KeyServices|Services/PriceAlerts|Services/ReceiptWatch|Services/LimitOrderWatch|Services/WidgetFeed)\.swift:")
346[ -n "$achados" ] && { aviso "protecao de arquivo mais fraca fora das cinco excecoes"; echo "$achados"; } || ok "protecao abaixo da Complete so no marcador de instalacao, nos alertas de preco, no aviso de recebimento, nas ordens abertas e no widget"
347# O widget: so o arquivo publico do preco. Quem grava nao toca cofre, chaveiro,
348# metadados nem saldo; o widget nao importa o Kit, nao fala com a rede e nao le chaveiro.
349widget_feed="$APP/Services/WidgetFeed.swift"
350achados=$(grep -nE 'import (EscaliburKeys|EscaliburEngines|Security|LocalAuthentication)|KeyServices|RootKeyVault|SecretStore|MetadataStore|SecureBytes|AppSession|Portfolio|WalletMeta|DerivedAccount|balance|address' "$widget_feed" 2>/dev/null | grep -vE '^[0-9]+:[[:space:]]*//')
351[ -n "$achados" ] && { aviso "o arquivo do widget toca cofre, chaveiro, metadados, saldo ou endereco"; echo "$achados"; } || ok "o arquivo do widget so leva a moeda e o preco, sem cofre, chaveiro, metadados, saldo nem endereco"
352achados=$(find App/EscaliburWidgets App/Shared -name '*.swift' | xargs grep -nE 'import (Escalibur|Security|LocalAuthentication|Network)|URLSession|URLRequest|NWConnection|SecItem|Keychain|UserDefaults' 2>/dev/null)
353[ -n "$achados" ] && { aviso "o widget importa o Kit, fala com a rede ou le o chaveiro"; echo "$achados"; } || ok "o widget so le o arquivo do App Group: sem Kit, sem rede, sem chaveiro"
354chaves=$(/usr/libexec/PlistBuddy -c "Print" App/EscaliburWidgets/EscaliburWidgets.entitlements | grep -E '^\s+[a-z][^ ]* = ' | awk '{print $1}' | sort | tr '\n' ' ')
355grupos=$(/usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups" App/EscaliburWidgets/EscaliburWidgets.entitlements 2>/dev/null | grep -c 'group.app.escalibur.wallet$')
356[ "$chaves" = "com.apple.security.application-groups " ] && [ "$grupos" = "1" ] && ok "entitlements do widget: so o App Group da carteira" || aviso "entitlements do widget inesperados: $chaves"
357recebimento="$APP/Services/ReceiptWatch.swift"
358achados=$(grep -nE 'import (EscaliburKeys|Security|LocalAuthentication)|KeyServices|RootKeyVault|SecretStore|MetadataStore|SecureBytes|AppSession|Portfolio|WalletMeta|privateKey|mnemonic' "$recebimento" 2>/dev/null | grep -vE '^[0-9]+:[[:space:]]*//')
359[ -n "$achados" ] && { aviso "o aviso de recebimento toca cofre, chaveiro ou metadados"; echo "$achados"; } || ok "o aviso de recebimento (segundo plano) so le enderecos publicos e saldo, sem cofre, chaveiro nem metadados"
360ordens="$APP/Services/LimitOrderWatch.swift"
361achados=$(grep -nE 'import (EscaliburKeys|EscaliburEngines|Security|LocalAuthentication)|KeyServices|RootKeyVault|SecretStore|MetadataStore|SecureBytes|AppSession|Portfolio|WalletMeta|DerivedAccount|privateKey|mnemonic' "$ordens" 2>/dev/null | grep -vE '^[0-9]+:[[:space:]]*//')
362[ -n "$achados" ] && { aviso "o aviso das ordens limite toca cofre, chaveiro, conta ou metadados"; echo "$achados"; } || ok "o aviso das ordens limite (segundo plano) so le o UID e o andamento na CoW, sem cofre, chaveiro nem metadados"
363alertas="$APP/Services/PriceAlerts.swift"
364achados=$(grep -nE 'import (EscaliburKeys|Security|LocalAuthentication)|KeyServices|RootKeyVault|SecretStore|MetadataStore|SecureBytes|AppSession|Portfolio|balance' "$alertas" 2>/dev/null | grep -vE '^[0-9]+:[[:space:]]*//')
365[ -n "$achados" ] && { aviso "os alertas de preco tocam cofre, chaveiro, metadados ou saldo"; echo "$achados"; } || ok "os alertas de preco (segundo plano) nao tocam cofre, chaveiro, metadados nem saldo"
366achados=$(swift_em "$APP" "$CORE" "$CHAINS" "$KEYS" "$NET" "$ENG" | xargs grep -nE 'UserDefaults|@AppStorage' 2>/dev/null | grep -v "^$APP/App/Preferences.swift:")
367[ -n "$achados" ] && { aviso "UserDefaults fora de Preferences.swift"; echo "$achados"; } || ok "UserDefaults so em Preferences.swift"
368# Manifesto de privacidade: sem ele a App Store recusa o envio (API de motivo
369# obrigatorio). Sem rastreamento, sem dado coletado, e o motivo do UserDefaults.
370manifesto="$APP/Resources/PrivacyInfo.xcprivacy"
371if plutil -lint "$manifesto" >/dev/null 2>&1 \
372 && [ "$(/usr/libexec/PlistBuddy -c 'Print :NSPrivacyTracking' "$manifesto" 2>/dev/null)" = "false" ] \
373 && [ "$(/usr/libexec/PlistBuddy -c 'Print :NSPrivacyCollectedDataTypes' "$manifesto" 2>/dev/null | grep -c Dict)" = "0" ] \
374 && grep -q 'NSPrivacyAccessedAPICategoryUserDefaults' "$manifesto" && grep -q 'CA92.1' "$manifesto" \
375 && grep -q 'NSPrivacyAccessedAPICategorySystemBootTime' "$manifesto" && grep -q '35F9.1' "$manifesto" \
376 && grep -q 'PrivacyInfo.xcprivacy' EscaliburWallet.xcodeproj/project.pbxproj; then
377 ok "manifesto de privacidade: sem rastreamento, sem coleta, motivos do UserDefaults e do tempo de boot, no projeto"
378else
379 aviso "manifesto de privacidade ausente, invalido ou fora do projeto"
380fi
381chaves=$(/usr/libexec/PlistBuddy -c "Print" "$APP/Resources/EscaliburWallet.entitlements" | grep -E '^\s+[a-z][^ ]* = ' | awk '{print $1}' | sort | tr '\n' ' ')
382grupos=$(/usr/libexec/PlistBuddy -c "Print :com.apple.security.application-groups" "$APP/Resources/EscaliburWallet.entitlements" 2>/dev/null | grep -cE '^\s+group\.')
383[ "$chaves" = "com.apple.developer.default-data-protection com.apple.security.application-groups " ] && [ "$grupos" = "1" ] \
384 && ok "entitlements: a protecao de dados e um App Group so (o do widget)" || aviso "entitlements inesperados: $chaves"
385achados=$(grep -oE 'NS[A-Za-z]+UsageDescription' "$APP/Resources/Info.plist" | sort -u | grep -vE 'NSFaceIDUsageDescription|NSCameraUsageDescription|NSMicrophoneUsageDescription|NSSpeechRecognitionUsageDescription')
386[ -n "$achados" ] && { aviso "permissao fora da lista"; echo "$achados"; } || ok "permissoes: Face ID, camera, microfone e fala, nada mais"
387achados=$(swift_em "$APP" | xargs grep -lE 'UIPasteboard' 2>/dev/null | grep -vE 'Receive/ReceiveSheet\.swift|NewWallet/ImportPhraseView\.swift|NewWallet/SecurityScreens\.swift')
388[ -n "$achados" ] && { aviso "area de transferencia fora dos pontos conhecidos"; echo "$achados"; } || ok "area de transferencia so em tres pontos"
389# A checagem do aparelho so pergunta se ha texto copiado, sem ler o conteudo.
390achados=$(grep -nE 'UIPasteboard' "$APP/Features/NewWallet/SecurityScreens.swift" | grep -v 'UIPasteboard.general.hasStrings')
391[ -n "$achados" ] && { aviso "a checagem do aparelho le a area de transferencia"; echo "$achados"; } || ok "a checagem do aparelho so pergunta se ha texto copiado"
392# Toda copia passa por setItems, local (nao vai para o Mac) e com validade. A frase vale
393# no maximo 1 minuto e so sai pela tela das palavras, depois do aviso (CopyPhraseSheet).
394f="$APP/Features/Receive/ReceiveSheet.swift"
395copias=$(grep -c 'UIPasteboard.general.setItems' "$f"); locais=$(grep -c '\.localOnly: true' "$f"); prazos=$(grep -c '\.expirationDate: Date().addingTimeInterval' "$f")
396outras=$(grep -n 'UIPasteboard' "$f" | grep -v 'UIPasteboard.general.setItems')
397[ "$copias" -gt 0 ] && [ "$copias" = "$locais" ] && [ "$copias" = "$prazos" ] && [ -z "$outras" ] \
398 && ok "toda copia e local e expira" || aviso "copia sem localOnly/expiracao"
399prazo=$(awk '/static func copyPhrase/,/^ }/' "$f" | grep -oE 'addingTimeInterval\([0-9]+\)' | grep -oE '[0-9]+')
400chamadas=$(procurar 'Pasteboard\.copyPhrase' "$APP" | grep -v 'Receive/ReceiveSheet\.swift:')
401[ -n "$prazo" ] && [ "$prazo" -le 60 ] && [ "$(echo "$chamadas" | grep -c .)" = "1" ] && echo "$chamadas" | grep -q 'NewWallet/RecordWordsView\.swift:' \
402 && grep -q 'CopyPhraseSheet {' "$APP/Features/NewWallet/RecordWordsView.swift" \
403 && ok "a frase so e copiada depois do aviso e some em ate 1 minuto" || { aviso "copia da frase fora do aviso ou com validade longa"; echo "$chamadas"; }
404achados=$(procurar '^[[:space:]]*import +(Firebase[A-Za-z]*|Crashlytics|Sentry|Bugsnag|Amplitude|Mixpanel|Segment|AppsFlyer[A-Za-z]*|Adjust[A-Za-z]*|Branch|OneSignal|Datadog[A-Za-z]*|NewRelic|Instabug|FBSDK[A-Za-z]*|GoogleMobileAds|PostHog)\b' "$APP" "$NET" "$CORE" "$CHAINS" "$KEYS")
405[ -n "$achados" ] && { aviso "SDK de terceiro"; echo "$achados"; } || ok "nenhum SDK de analise, falha ou anuncio"
406achados=$(procurar 'WKWebView|UIWebView|ASWebAuthenticationSession|SFSafariViewController' "$APP" | grep -v "^$APP/Design/SiteBrowser.swift:")
407# A unica excecao (pedido do dono, 18/10/2026): o navegador da Apple, em outro processo, so
408# para o site da Escalibur, num arquivo so, que recusa endereco fora dele.
409navegador="$APP/Design/SiteBrowser.swift"
410if [ -n "$achados" ]; then
411 aviso "navegador embutido fora do SiteBrowser"; echo "$achados"
412elif grep -qE 'WKWebView|UIWebView|ASWebAuthenticationSession' "$navegador" \
413 || ! grep -q 'static let host = "escalibur-wallet.github.io"' "$navegador" || ! grep -q 'url.scheme == "https" && url.host == host' "$navegador"; then
414 aviso "o SiteBrowser abre mais que o site da Escalibur"
415else
416 ok "sem navegador embutido; so o Safari da Apple, para o site da Escalibur (SiteBrowser)"
417fi
418
419# 8. Modo de demonstracao so em depuracao.
420secao "depuracao"
421head -1 "$APP/App/DebugDemo.swift" | grep -q '^#if DEBUG' && ok "DebugDemo.swift inteiro atras de #if DEBUG" || aviso "DebugDemo.swift fora de #if DEBUG"
422achados=$(awk 'FNR==1{d=0} /#if DEBUG/{d=1} /#endif/{d=0} /DebugDemo\./{ if(!d) print FILENAME": "FNR }' $(swift_em "$APP" | grep -v DebugDemo.swift))
423[ -n "$achados" ] && { aviso "uso de DebugDemo fora de #if DEBUG"; echo "$achados"; } || ok "nenhum uso de DebugDemo fora de #if DEBUG"
424# Argumento de inicializacao so em depuracao: quem abre o app passando argumentos (Xcode,
425# um atalho, um app de terceiros) mudaria o comportamento do app da loja. Mesma tecnica da
426# regra acima; `#else` e `#elseif` encerram o bloco, e um `#if` aninhado tambem, o que so
427# pode acusar a mais, nunca deixar passar.
428achados=$(awk 'FNR==1{d=0} /^[[:space:]]*#if DEBUG/{d=1} /^[[:space:]]*#(else|elseif|endif)/{d=0}
429 /(ProcessInfo\.processInfo|ProcessInfo\(\)|processInfo)\.arguments|CommandLine\.(arguments|unsafeArgv|argc)/{ if(!d && $0 !~ /^[[:space:]]*\/\//) print FILENAME": "FNR": "$0 }' \
430 $(swift_em App/EscaliburWallet App/Shared App/EscaliburWidgets))
431[ -n "$achados" ] && { aviso "argumento de inicializacao lido fora de #if DEBUG"; echo "$achados"; } || ok "argumentos de inicializacao so lidos dentro de #if DEBUG"
432# E o DEBUG so existe na configuracao Debug do projeto: com ele no Release, todo #if DEBUG
433# iria para a loja, e as duas regras acima nao protegeriam nada.
434achados=$(awk '/^[[:space:]]*[0-9A-F]+ \/\* [^*]+ \*\/ = \{/{ match($0, /\/\* [^*]+ \*\//); cfg=substr($0, RSTART+3, RLENGTH-6) }
435 /SWIFT_ACTIVE_COMPILATION_CONDITIONS|GCC_PREPROCESSOR_DEFINITIONS|OTHER_SWIFT_FLAGS|OTHER_CFLAGS/{f=1}
436 f && /DEBUG/{ if(cfg != "Debug") print FILENAME": "NR" ("cfg"): "$0 } /;[[:space:]]*$/{f=0}' "$pbx")
437[ -n "$achados" ] && { aviso "DEBUG ligado fora da configuracao Debug"; echo "$achados"; } || ok "DEBUG so na configuracao Debug (o Release nao compila nada de #if DEBUG)"
438
439# 9. Texto que o usuario le, sem travessao.
440secao "determinismo"
441achados=$(grep -rnE '__DATE__|__TIME__|__TIMESTAMP__' Kit/Sources/CSecp256k1 Kit/Sources/CArgon2 2>/dev/null)
442[ -n "$achados" ] && { aviso "data de compilacao no C"; echo "$achados"; } || ok "nenhuma data de compilacao no C vendorizado"
443if [ -f .xcode-version ]; then
444 atual=$(xcodebuild -version 2>/dev/null | head -1)
445 [ "$atual" = "$(cat .xcode-version)" ] && ok "Xcode igual ao .xcode-version ($atual)" || aviso "Xcode $atual difere do .xcode-version ($(cat .xcode-version))"
446else
447 aviso ".xcode-version nao existe"
448fi
449
450secao "privacidade"
451faltando=""
452for host in $(sed 's|https://||' hosts.lock); do
453 for politica in privacidade.md privacidade-en.md; do
454 grep -qF "\`$host\`" "$APP/Resources/Legal/$politica" || faltando="$faltando $host($politica)"
455 done
456done
457[ -n "$faltando" ] && aviso "host fora da Politica de privacidade:$faltando" || ok "todo host do hosts.lock esta na Politica de privacidade, em portugues e em ingles"
458# Contato (diretriz 1.5 da App Store e LGPD, art. 9): o e-mail do app e o mesmo da
459# pagina de suporte, dos Termos e da Politica.
460email=$(grep -oE 'static let email = "[^"]+"' "$APP/Features/Settings/SupportView.swift" 2>/dev/null | sed -E 's/.*"(.*)"/\1/')
461faltando=""
462for arquivo in site/suporte.md "$APP/Resources/Legal/termos.md" "$APP/Resources/Legal/privacidade.md" \
463 "$APP/Resources/Legal/termos-en.md" "$APP/Resources/Legal/privacidade-en.md"; do
464 grep -qF "$email" "$arquivo" 2>/dev/null || faltando="$faltando $arquivo"
465done
466if [ -z "$email" ]; then
467 aviso "e-mail de suporte ausente no app (SupportView.swift)"
468elif [ -n "$faltando" ]; then
469 aviso "e-mail de suporte ($email) fora de:$faltando"
470else
471 ok "e-mail de suporte igual no app, na pagina de suporte, nos Termos e na Politica"
472fi
473saida_site=$(mktemp -d)
474python3 tools/gerar-site.py "$saida_site/site" >/dev/null 2>&1 && [ -f "$saida_site/site/privacidade/index.html" ] && [ -f "$saida_site/site/suporte/index.html" ] \
475 && [ -f "$saida_site/site/termos/index.html" ] \
476 && ok "paginas publicas (suporte, privacidade, termos) geradas dos mesmos textos do app" || aviso "tools/gerar-site.py falhou"
477
478# A lista de versoes assinada (docs/seguranca.md, "Versao autorizada"): a publicada no site
479# tem de conferir com as chaves compiladas no app (ReleaseKeys.swift). Antes da primeira
480# assinatura, o arquivo ainda nao existe.
481if [ -f site/versoes.json ]; then
482 if saida_versoes=$(swift tools/versoes/versoes.swift conferir 2>&1); then
483 ok "${saida_versoes}"
484 else
485 aviso "site/versoes.json nao confere com as chaves de ReleaseKeys.swift: $saida_versoes"
486 fi
487else
488 ok "lista de versoes assinada ainda nao criada (tools/versoes/versoes.swift autorizar)"
489fi
490
491# O site (tools/gerar-site.py) le redes e tokens do codigo por expressao regular. Se o
492# codigo mudar de forma e o leitor ficar para tras, a contagem diverge e isto falha, em
493# vez de o site publicar uma lista pela metade.
494secao "site"
495redes_codigo=$(awk '/static let all: \[Chain\] = \[/{f=1;next} f&&/\]/{exit} f' "$CHAINS/Chain.swift" | grep -oE '\.[a-z0-9]+' | wc -l | tr -d ' ')
496tokens_codigo=$(awk '/public static let tokens: \[Asset\] = \[/{f=1;next} f&&/^ \]$/{exit} f' "$CHAINS/TokenRegistry.swift" | grep -cE '^[[:space:]]*(erc20|spl|issued|Asset)\(')
497contagem=$(python3 tools/gerar-site.py --contagem 2>&1)
498if [ "$contagem" = "redes $redes_codigo tokens $tokens_codigo" ]; then
499 ok "o site lista as $redes_codigo redes de Chain.swift e os $tokens_codigo tokens de TokenRegistry.swift"
500else
501 aviso "o site leu \"$contagem\", e o codigo tem $redes_codigo redes e $tokens_codigo tokens (tools/gerador_site/dados.py)"
502fi
503paginas_site=$(find "$saida_site/site" -name '*.html' 2>/dev/null)
504if [ -z "$paginas_site" ]; then
505 aviso "o site nao foi gerado"
506else
507 sem_csp=$(echo "$paginas_site" | xargs grep -L "content=\"default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; media-src 'self'; base-uri 'none'; form-action 'none'\"" 2>/dev/null)
508 de_fora=$(echo "$paginas_site" | xargs grep -noE '(src|poster)="https?://|<link rel="(stylesheet|icon|apple-touch-icon|preload|modulepreload|manifest)"[^>]*href="https?://|<script>|<script [^>]*>[^<]|style="| on[a-z]+="' 2>/dev/null)
509 de_fora="$de_fora$(grep -noE 'url\(["'"'"']?(https?:)?//|@import' "$saida_site/site/estilo.css" 2>/dev/null)"
510 if [ -n "$sem_csp$de_fora" ]; then
511 aviso "pagina do site sem a Content Security Policy, ou com script, estilo ou recurso de fora"; echo "$sem_csp$de_fora"
512 else
513 ok "todas as paginas do site com Content Security Policy, sem script em linha e sem nada carregado de outro site"
514 fi
515 # Codigo citado nas paginas de auditoria sai como entidade HTML: o que sobra e texto.
516 achados=$(echo "$paginas_site" | xargs grep -lE '—|–' 2>/dev/null)
517 [ -n "$achados" ] && { aviso "travessao nas paginas do site"; echo "$achados"; } || ok "nenhum travessao nas paginas do site"
518 # Os selos oficiais aparecem antes do lancamento por decisao do dono (18/10/2026), mas sem
519 # link: enquanto o app nao esta na loja, o selo nao leva a lugar nenhum.
520 sem_link=""
521 grep -qE '^APP_STORE_URL = None' tools/gerar-site.py && sem_link="baixar-na-app-store"
522 grep -qE '^PLAY_STORE_URL = None' tools/gerar-site.py && sem_link="$sem_link|disponivel-no-google-play"
523 sem_link="${sem_link#|}"
524 if [ -n "$sem_link" ] && echo "$paginas_site" | xargs grep -lE "<a [^>]*><img src=\"[^\"]*($sem_link)" >/dev/null 2>&1; then
525 aviso "selo de loja com link sem o app na loja"
526 else
527 ok "selos das lojas sem link enquanto o app nao esta na loja (APP_STORE_URL e PLAY_STORE_URL em tools/gerar-site.py)"
528 fi
529fi
530rm -rf "$saida_site"
531
532secao "texto"
533achados=$(swift_em "$APP" | xargs grep -nE '"[^"]*(—|–)[^"]*"' 2>/dev/null)
534[ -n "$achados" ] && { aviso "travessao em texto de interface"; echo "$achados"; } || ok "nenhum travessao em texto de interface"
535# Toda traducao do catalogo com os campos certos: traducao invalida (campo trocado,
536# travessao) falha sempre. Texto sem traducao so e dito: o dono adiou as traducoes dos textos
537# novos (16/10/2026), e o iPhone mostra o original em portugues ate elas entrarem. Texto novo
538# entra pelo tools/traducao/sincronizar.sh (tools/traducao/glossario.md).
539resumo=$(python3 tools/traducao/conferir.py --resumo 2>&1)
540if echo "$resumo" | grep -qE ' [1-9][0-9]* invalidos'; then
541 aviso "traducao invalida no catalogo (tools/traducao/conferir.py --resumo)"
542 echo "$resumo" | grep -E ' [1-9][0-9]* invalidos' | head -14
543elif python3 tools/traducao/conferir.py >/dev/null 2>&1; then
544 ok "todo texto de tela traduzido nos $(echo "$resumo" | grep -c 'traduzidos') idiomas, sem campo trocado"
545else
546 faltando=$(echo "$resumo" | head -1 | sed -E 's/.* ([0-9]+) faltando.*/\1/')
547 ok "nenhuma traducao invalida; $faltando texto(s) ainda sem traducao, adiados pelo dono (em portugues ate entrarem)"
548fi
549
550# 10. Testes com vetores oficiais, e a fronteira conferida tambem no binario (regra 3
551# de docs/seguranca.md §8.3): os objetos compilados de Chaves, Cadeias e Nucleo nao
552# podem referenciar nenhum simbolo de rede. O modulo de rede e o controle: nele os
553# simbolos aparecem, e a checagem que nao os achasse ali estaria cega.
554# A fronteira tambem por lista de permissao (auditoria 6, A6-15): todo simbolo de fora que
555# Chaves, Cadeias e Nucleo referenciam no binario, fora a biblioteca do Swift e o proprio
556# codigo, esta em simbolos.lock. A lista de proibidos pega URLSession e sockets, nao
557# XMLParser(contentsOf:) nem Stream; aqui um simbolo novo e revisado como um host novo.
558simbolos_do_binario() {
559 for modulo in EscaliburKeys EscaliburChains EscaliburCore; do nm -u Kit/.build/debug/$modulo.build/*.o 2>/dev/null; done \
560 | sed 's/^_//' | grep -vE '^\$ss|^\$sS[^oC]|^\$s12_Concurrency|^\$s[0-9]+Escalibur|^_?swift|^_' | sort -u
561}
562if [ "${1:-}" = "--atualizar-simbolos" ]; then
563 (cd Kit && swift build >/dev/null 2>&1) || { echo " ✗ o Kit nao compilou"; exit 1; }
564 simbolos_do_binario > simbolos.lock
565 ok "simbolos.lock atualizado: $(wc -l < simbolos.lock | tr -d ' ') simbolos (revise o diff)"
566 exit 0
567fi
568
569if [ "${1:-}" = "--testes" ]; then
570 secao "testes"
571 saida=$(cd Kit && swift test 2>&1); codigo=$?
572 if [ "$codigo" = "0" ]; then
573 ok "testes do nucleo passam ($(echo "$saida" | grep -oE 'Test run with [0-9]+ tests' | tail -1))"
574 else
575 aviso "testes do nucleo falharam (saida $codigo)"
576 echo "$saida" | grep -E "✘|error:|fatal error|Fatal error" | head -40
577 echo "$saida" | tail -15
578 fi
579 proibidos='URLSession|_nw_|CFSocket|CFStream|CFHTTP|NSURLConnection|XMLParser|NSInputStream|NSOutputStream|So11InputStream|So12OutputStream|objc_getClass|^_dlopen$|^_dlsym$|^_socket$|^_connect$|^_getaddrinfo$'
580 achados=""
581 for modulo in EscaliburKeys EscaliburChains EscaliburCore; do
582 n=$(nm -u Kit/.build/debug/$modulo.build/*.o 2>/dev/null | grep -cE "$proibidos")
583 [ "$n" != "0" ] && achados="$achados $modulo($n)"
584 done
585 controle=$(nm -u Kit/.build/debug/EscaliburNetwork.build/*.o 2>/dev/null | grep -cE "$proibidos")
586 if [ -n "$achados" ]; then
587 aviso "simbolo de rede no binario:$achados"
588 elif [ "$controle" = "0" ]; then
589 aviso "checagem do binario cega: nem o modulo de rede mostrou simbolo de rede"
590 else
591 ok "binario de Chaves, Cadeias e Nucleo sem simbolo de rede (controle: $controle no de rede)"
592 fi
593 novos=$(comm -23 <(simbolos_do_binario) <(sort -u simbolos.lock 2>/dev/null))
594 if [ ! -f simbolos.lock ]; then
595 aviso "simbolos.lock nao existe (gere com --atualizar-simbolos e revise)"
596 elif [ "$(simbolos_do_binario | wc -l)" -lt 50 ]; then
597 aviso "checagem dos simbolos cega: o binario de Chaves, Cadeias e Nucleo nao mostrou simbolo"
598 elif [ -n "$novos" ]; then
599 aviso "simbolo de fora novo em Chaves, Cadeias ou Nucleo; revise e rode --atualizar-simbolos"
600 echo "$novos" | head -20
601 else
602 ok "Chaves, Cadeias e Nucleo so usam os $(wc -l < simbolos.lock | tr -d ' ') simbolos de fora de simbolos.lock"
603 fi
604fi
605
606echo
607if [ "$falhas" = "0" ]; then echo "tudo conferido."; else echo "$falhas verificacao(oes) falharam."; fi
608exit "$falhas"