Auditoria
O verificador
tools/verificar.sh
Verificações que valem por afirmação pública sobre a Escalibur Wallet.
| 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 | |
| 13 | set -uo pipefail |
| 14 | cd "$(dirname "$0")/.." || exit 1 |
| 15 | |
| 16 | falhas=0 |
| 17 | aviso() { echo " ✗ $1"; falhas=$((falhas + 1)); } |
| 18 | ok() { echo " ✓ $1"; } |
| 19 | secao() { echo; echo "$1"; } |
| 20 | |
| 21 | CORE=Kit/Sources/EscaliburCore |
| 22 | CHAINS=Kit/Sources/EscaliburChains |
| 23 | KEYS=Kit/Sources/EscaliburKeys |
| 24 | NET=Kit/Sources/EscaliburNetwork |
| 25 | ENG=Kit/Sources/EscaliburEngines |
| 26 | APP=App/EscaliburWallet |
| 27 | |
| 28 | swift_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. |
| 32 | nao_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). |
| 37 | procurar() { local padrao="$1"; shift; swift_em "$@" | xargs grep -nE "$padrao" 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*//'; } |
| 38 | |
| 39 | echo |
| 40 | echo "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. |
| 44 | com_espaco=$(find Kit/Sources Kit/Tests App tools \( -name "*.swift" -o -name "*.py" -o -name "*.sh" -o "${nao_swift[@]}" \) 2>/dev/null | grep '[[:space:]]') |
| 45 | if [ -n "$com_espaco" ]; then |
| 46 | echo " ✗ arquivo com espaco no caminho escapa das regras: renomeie" |
| 47 | echo "$com_espaco" |
| 48 | exit 1 |
| 49 | fi |
| 50 | |
| 51 | # 1. Chaves e redes nao falam com a internet. |
| 52 | secao "fronteira de rede" |
| 53 | achados=$(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 | |
| 56 | achados=$(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 | |
| 59 | achados=$(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 | |
| 62 | achados=$(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 | |
| 65 | achados=$(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 | |
| 68 | achados=$(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" |
| 70 | deps_motores=$(awk '/name: "EscaliburEngines"/{f=1} f&&/dependencies/{print; exit}' Kit/Package.swift) |
| 71 | echo "$deps_motores" | grep -q 'EscaliburKeys' && aviso "EscaliburEngines depende de EscaliburKeys no Package.swift" || ok "EscaliburEngines nao depende de EscaliburKeys (o compilador garante)" |
| 72 | deps_rede=$(awk '/name: "EscaliburNetwork"/{f=1} f&&/dependencies/{print; exit}' Kit/Package.swift) |
| 73 | echo "$deps_rede" | grep -q 'EscaliburKeys' && aviso "EscaliburNetwork depende de EscaliburKeys no Package.swift" || ok "EscaliburNetwork nao depende de EscaliburKeys (o compilador garante)" |
| 74 | |
| 75 | achados=$(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 | |
| 78 | achados=$(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 | |
| 81 | achados=$(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 | |
| 84 | achados=$(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. |
| 88 | secao "hosts" |
| 89 | hosts_atuais=$(grep -ohE 'https://[A-Za-z0-9.-]+' $(swift_em "$NET" "$ENG") "$APP/Resources/Info.plist" 2>/dev/null | sort -u) |
| 90 | gerar_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 | } |
| 101 | if [ "${1:-}" = "--atualizar-hosts" ]; then |
| 102 | echo "$hosts_atuais" > hosts.lock |
| 103 | gerar_allowed_hosts |
| 104 | ok "hosts.lock e AllowedHosts.swift atualizados" |
| 105 | elif [ ! -f hosts.lock ]; then |
| 106 | aviso "hosts.lock nao existe (gere com --atualizar-hosts e revise o diff)" |
| 107 | elif ! 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 |
| 110 | else |
| 111 | ok "$(wc -l < hosts.lock | tr -d ' ') hosts conferem com hosts.lock" |
| 112 | fi |
| 113 | achados=$(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 | |
| 116 | if /usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsArbitraryLoads" "$APP/Resources/Info.plist" 2>/dev/null | grep -q true; then |
| 117 | aviso "ATS com NSAllowsArbitraryLoads ligado" |
| 118 | elif grep -qE 'NSExceptionAllowsInsecureHTTPLoads|NSAllowsLocalNetworking' "$APP/Resources/Info.plist"; then |
| 119 | aviso "ATS com excecao" |
| 120 | else |
| 121 | ok "ATS padrao, sem excecao" |
| 122 | fi |
| 123 | |
| 124 | # 3. Dependencias. |
| 125 | esperado=$(sed 's|https://||' hosts.lock 2>/dev/null | sort) |
| 126 | gerado=$(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)" |
| 128 | grep -q 'allowedHosts.contains(host)' "$NET/HTTPClient.swift" && ok "HTTPClient confere o host de toda requisicao" || aviso "HTTPClient deixou de conferir o host" |
| 129 | |
| 130 | secao "dependencias" |
| 131 | if 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. |
| 134 | if grep -qE '\.package[[:space:]]*\(' Kit/Package.swift; then aviso "dependencia de pacote no Package.swift"; else ok "nenhum pacote remoto no Package.swift"; fi |
| 135 | achados=$(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" |
| 137 | locais=$(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"; } |
| 139 | if grep -q '\.binaryTarget(' Kit/Package.swift; then aviso "alvo binario no Package.swift"; else ok "nenhum alvo binario"; fi |
| 140 | achados=$(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). |
| 147 | secao "fontes e build" |
| 148 | # Caminhos separados por espaco; a lista so vale para .metal. |
| 149 | metal_permitidos="" |
| 150 | achados=$(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. |
| 161 | pbx=EscaliburWallet.xcodeproj/project.pbxproj |
| 162 | achados=$( { |
| 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. |
| 180 | secao "workflows" |
| 181 | achados=$(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" |
| 185 | achados=$(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. |
| 200 | secao "codigo vendorizado" |
| 201 | conferir_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 | } |
| 214 | conferir_lock secp256k1.lock Kit/Sources/CSecp256k1/upstream secp256k1 |
| 215 | conferir_lock argon2.lock Kit/Sources/CArgon2 argon2 |
| 216 | conferir_lock modelos.lock App/EscaliburWallet/Resources/Moeda3D "moeda 3D" |
| 217 | fora_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" |
| 219 | divergentes=0 |
| 220 | for 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)); } |
| 224 | done |
| 225 | [ "$divergentes" = "0" ] && ok "as 10 listas BIP-39 conferem com wordlists.lock" |
| 226 | divergentes=0 |
| 227 | for 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)); } |
| 231 | done |
| 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. |
| 237 | if (cd tools/referencia && shasum -a 256 -c ../../recuperacao.lock >/dev/null 2>&1); then |
| 238 | ok "decifrador de referencia confere com recuperacao.lock" |
| 239 | else |
| 240 | aviso "decifrador de referencia difere de recuperacao.lock" |
| 241 | fi |
| 242 | [ -e "$APP/Resources/Recuperacao" ] && aviso "decifrador para computador de volta no app" || ok "nenhum decifrador para computador dentro do app" |
| 243 | |
| 244 | secao "registro e aleatoriedade" |
| 245 | achados=$(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" |
| 247 | achados=$(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" |
| 249 | achados=$(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. |
| 253 | secao "chaveiro e biometria" |
| 254 | achados=$(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). |
| 259 | achados=$(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. |
| 265 | excecao_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") |
| 266 | achados=$(procurar '\.biometryAny' "$APP" "$KEYS") |
| 267 | if [ "$(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" |
| 270 | else |
| 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" |
| 273 | fi |
| 274 | embrulho_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") |
| 275 | achados=$(procurar 'slot: *\.hardenedBiometryAny|wrap\([^)]*\.hardenedBiometryAny' "$APP" "$KEYS") |
| 276 | if [ "$(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" |
| 279 | else |
| 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" |
| 282 | fi |
| 283 | achados=$(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" |
| 285 | if ! 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). |
| 287 | excecao=$(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" |
| 289 | achados=$(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" |
| 291 | achados=$(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" |
| 293 | achados=$(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. |
| 299 | secao "segredo" |
| 300 | filtrar_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 | } |
| 311 | achados=$(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)" |
| 314 | achados=$(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" |
| 316 | achados=$(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" |
| 318 | achados=$(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. |
| 322 | secao "plataforma" |
| 323 | grep -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" |
| 325 | grep -q 'willResignActiveNotification' "$APP/App/PlatformGuards.swift" && ok "cobertura antes da foto do seletor de apps" || aviso "cobertura do seletor sumiu" |
| 326 | grep -q 'NSFileProtectionComplete' "$APP/Resources/EscaliburWallet.entitlements" && ok "NSFileProtectionComplete em todo o conteiner" || aviso "protecao de arquivo sumiu" |
| 327 | grep -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). |
| 332 | camada="$APP/Design/SecureCaptureContainer.swift" |
| 333 | if [ -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" |
| 336 | else |
| 337 | aviso "camada segura contra captura sumiu" |
| 338 | fi |
| 339 | grep -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. |
| 344 | achados=$(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. |
| 349 | widget_feed="$APP/Services/WidgetFeed.swift" |
| 350 | achados=$(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" |
| 352 | achados=$(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" |
| 354 | chaves=$(/usr/libexec/PlistBuddy -c "Print" App/EscaliburWidgets/EscaliburWidgets.entitlements | grep -E '^\s+[a-z][^ ]* = ' | awk '{print $1}' | sort | tr '\n' ' ') |
| 355 | grupos=$(/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" |
| 357 | recebimento="$APP/Services/ReceiptWatch.swift" |
| 358 | achados=$(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" |
| 360 | ordens="$APP/Services/LimitOrderWatch.swift" |
| 361 | achados=$(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" |
| 363 | alertas="$APP/Services/PriceAlerts.swift" |
| 364 | achados=$(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" |
| 366 | achados=$(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. |
| 370 | manifesto="$APP/Resources/PrivacyInfo.xcprivacy" |
| 371 | if 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" |
| 378 | else |
| 379 | aviso "manifesto de privacidade ausente, invalido ou fora do projeto" |
| 380 | fi |
| 381 | chaves=$(/usr/libexec/PlistBuddy -c "Print" "$APP/Resources/EscaliburWallet.entitlements" | grep -E '^\s+[a-z][^ ]* = ' | awk '{print $1}' | sort | tr '\n' ' ') |
| 382 | grupos=$(/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" |
| 385 | achados=$(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" |
| 387 | achados=$(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. |
| 390 | achados=$(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). |
| 394 | f="$APP/Features/Receive/ReceiveSheet.swift" |
| 395 | copias=$(grep -c 'UIPasteboard.general.setItems' "$f"); locais=$(grep -c '\.localOnly: true' "$f"); prazos=$(grep -c '\.expirationDate: Date().addingTimeInterval' "$f") |
| 396 | outras=$(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" |
| 399 | prazo=$(awk '/static func copyPhrase/,/^ }/' "$f" | grep -oE 'addingTimeInterval\([0-9]+\)' | grep -oE '[0-9]+') |
| 400 | chamadas=$(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"; } |
| 404 | achados=$(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" |
| 406 | achados=$(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. |
| 409 | navegador="$APP/Design/SiteBrowser.swift" |
| 410 | if [ -n "$achados" ]; then |
| 411 | aviso "navegador embutido fora do SiteBrowser"; echo "$achados" |
| 412 | elif 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" |
| 415 | else |
| 416 | ok "sem navegador embutido; so o Safari da Apple, para o site da Escalibur (SiteBrowser)" |
| 417 | fi |
| 418 | |
| 419 | # 8. Modo de demonstracao so em depuracao. |
| 420 | secao "depuracao" |
| 421 | head -1 "$APP/App/DebugDemo.swift" | grep -q '^#if DEBUG' && ok "DebugDemo.swift inteiro atras de #if DEBUG" || aviso "DebugDemo.swift fora de #if DEBUG" |
| 422 | achados=$(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. |
| 428 | achados=$(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. |
| 434 | achados=$(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. |
| 440 | secao "determinismo" |
| 441 | achados=$(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" |
| 443 | if [ -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))" |
| 446 | else |
| 447 | aviso ".xcode-version nao existe" |
| 448 | fi |
| 449 | |
| 450 | secao "privacidade" |
| 451 | faltando="" |
| 452 | for 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 |
| 456 | done |
| 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. |
| 460 | email=$(grep -oE 'static let email = "[^"]+"' "$APP/Features/Settings/SupportView.swift" 2>/dev/null | sed -E 's/.*"(.*)"/\1/') |
| 461 | faltando="" |
| 462 | for 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" |
| 465 | done |
| 466 | if [ -z "$email" ]; then |
| 467 | aviso "e-mail de suporte ausente no app (SupportView.swift)" |
| 468 | elif [ -n "$faltando" ]; then |
| 469 | aviso "e-mail de suporte ($email) fora de:$faltando" |
| 470 | else |
| 471 | ok "e-mail de suporte igual no app, na pagina de suporte, nos Termos e na Politica" |
| 472 | fi |
| 473 | saida_site=$(mktemp -d) |
| 474 | python3 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. |
| 481 | if [ -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 |
| 487 | else |
| 488 | ok "lista de versoes assinada ainda nao criada (tools/versoes/versoes.swift autorizar)" |
| 489 | fi |
| 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. |
| 494 | secao "site" |
| 495 | redes_codigo=$(awk '/static let all: \[Chain\] = \[/{f=1;next} f&&/\]/{exit} f' "$CHAINS/Chain.swift" | grep -oE '\.[a-z0-9]+' | wc -l | tr -d ' ') |
| 496 | tokens_codigo=$(awk '/public static let tokens: \[Asset\] = \[/{f=1;next} f&&/^ \]$/{exit} f' "$CHAINS/TokenRegistry.swift" | grep -cE '^[[:space:]]*(erc20|spl|issued|Asset)\(') |
| 497 | contagem=$(python3 tools/gerar-site.py --contagem 2>&1) |
| 498 | if [ "$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" |
| 500 | else |
| 501 | aviso "o site leu \"$contagem\", e o codigo tem $redes_codigo redes e $tokens_codigo tokens (tools/gerador_site/dados.py)" |
| 502 | fi |
| 503 | paginas_site=$(find "$saida_site/site" -name '*.html' 2>/dev/null) |
| 504 | if [ -z "$paginas_site" ]; then |
| 505 | aviso "o site nao foi gerado" |
| 506 | else |
| 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 |
| 529 | fi |
| 530 | rm -rf "$saida_site" |
| 531 | |
| 532 | secao "texto" |
| 533 | achados=$(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). |
| 539 | resumo=$(python3 tools/traducao/conferir.py --resumo 2>&1) |
| 540 | if 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 |
| 543 | elif 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" |
| 545 | else |
| 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)" |
| 548 | fi |
| 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. |
| 558 | simbolos_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 | } |
| 562 | if [ "${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 |
| 567 | fi |
| 568 | |
| 569 | if [ "${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 |
| 604 | fi |
| 605 | |
| 606 | echo |
| 607 | if [ "$falhas" = "0" ]; then echo "tudo conferido."; else echo "$falhas verificacao(oes) falharam."; fi |
| 608 | exit "$falhas" |