Overview › Code Security · Obfuscation · Crypto (iOS)

Code Security · Obfuscation · Crypto (iOS)

Generated 2026-05-11 · Costco iOS

Executive summary

Strong baseline: CostcoServerTrustManager certificate pinning, AES-GCM via Keychain, NokNok PasskeyManagerKit (FIDO2), SecRandomCopyBytes for OIDC nonces, ThreatMetrix device intelligence. Notable gaps: NSAllowsArbitraryLoads = true in Notification Service Extension Info.plist (production AND QA), Privacy Manifest absent (Apple gates submissions on this since 2024), no jailbreak detection, no anti-debugging, no privacy overlay on backgrounding, and 30+ print() sites that may leak PII.

1. Code obfuscation — Swift symbol management

iOS doesn't have a direct ProGuard/R8 equivalent. The closest analogs are: stripping debug symbols from the shipped binary, uploading dSYMs to Crashlytics for symbolication while not shipping them in the IPA, and the SWIFT_OPTIMIZATION_LEVEL setting which produces less-readable disassembly.

SettingStatusNote
SWIFT_OPTIMIZATION_LEVEL = -O (release)VERIFYDefault for release; confirm not -Onone
STRIP_INSTALLED_PRODUCT = YESVERIFYStrips local symbol table
STRIP_STYLE = all (release)VERIFYFull symbol stripping
DEPLOYMENT_POSTPROCESSING = YESVERIFYTriggers strip step
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym (release)VERIFYGenerates dSYM out-of-band
dSYM upload to Firebase CrashlyticsVERIFYBuild phase script
BitcodeN/ADeprecated by Apple

What "obfuscation" actually means on iOS

Stripping symbols makes reverse engineering harder by removing function/method names from the binary. It does not rename or restructure — Hopper / IDA still produces readable assembly. For a retail app, symbol stripping is the appropriate level. Heavier obfuscation (string encryption, control-flow flattening via tools like iXGuard or Promon SHIELD) is typically reserved for fintech; verify with security team whether it's required here given ThreatMetrix is already in place.

MEDIUM

Verify dSYM upload + retention

Without dSYMs, Crashlytics can't symbolicate. Without ours-only retention, anyone with the public IPA can read the symbol map.
Recommendation: Confirm Build Phase script upload-symbols for Crashlytics runs in release. Archive dSYMs as a build artifact in Azure Pipelines for retention beyond Crashlytics' window. Do not ship dSYMs inside the IPA.

2. Cryptographic primitives — what's used, where

PrimitiveFilePurposeStatus
SecRandomCopyBytesCostco-Digital/Features/PasskeyManagerKit/Sources/PasskeyManager/Protocols/OIDCAuthenticationHelper.swift:32OIDC nonce generationPASS — cryptographically secure
Keychain ServicesCostco-Digital/Storage/...Token storagePASS
CommonCrypto AES-GCMCostco-Digital/Core/Sources/Core/encryptionDecryption/CryptoManagerImpl.swiftSymmetric encryptionPASS — verify 256-bit key + correct IV handling
NokNok PasskeyManager 9.2.0PasskeyManagerKitFIDO2 / WebAuthnPASS
LAContext (LocalAuthentication)Biometric flowsFace ID / Touch ID gateVERIFY — bind to keychain key, not just policy success
CryptoKit primitivesWhere adoptedModern Swift cryptoCONSIDER — preferred over CommonCrypto for new code
arc4random*AUDITSearch for non-secure RNG in non-test code
HIGH

Bind LAContext biometric to a Keychain item

A biometric prompt that returns success without unwrapping a Keychain-bound token is a "user-presence" signal, not authentication. On a jailbroken device the success callback can be replayed.
Recommendation: Store the auth token in Keychain with kSecAttrAccessControl set to .biometryCurrentSet. Read the token through the biometric prompt — failure = token unavailable = no authenticated request.
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.biometryCurrentSet, .privateKeyUsage],
    nil
)
let attributes: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccessControl as String: access!,
    kSecAttrService as String: "com.costco.app.token",
    kSecValueData as String: tokenData,
]
SecItemAdd(attributes as CFDictionary, nil)
MEDIUM

Audit IV/nonce handling in CryptoManagerImpl

AES-GCM is broken if the same nonce is ever reused with the same key. Confirm CryptoManagerImpl generates a fresh IV per encrypt call (12 bytes from SecRandomCopyBytes) and prepends it to ciphertext.
Recommendation: Add a unit test that runs the same plaintext through encrypt() twice and asserts the ciphertexts differ.

3. Secret management — hardcoded keys + URL audit

Asset typePattern observedStatus
API endpointsHardcoded base URLs (e.g. BazaarVoice — see deep dive)PARTIAL — should be config-driven
API keys / secrets in sourceSearch for "AIza", "AKIA", "sk_live", etc.SCAN — recommend adding git-secrets pre-commit hook
plist API keysAudit Info.plist files for embedded keysVERIFY
Adobe / Contentstack credentialsShould load from CI secret + xcconfig at build timeVERIFY

Recommended pattern: xcconfig + CI secrets

// Costco-Production.xcconfig (committed, no secrets)
ADOBE_ENV = production

// Costco-Production.private.xcconfig (gitignored)
ADOBE_LICENSE_KEY = abc123...
CONTENTSTACK_DELIVERY_TOKEN = xyz789...

// Build phase reads from environment in CI; from .private.xcconfig locally
// Info.plist references via $(VAR_NAME)

4. Anti-tampering / jailbreak / anti-debugging

ControlStatusNote
Jailbreak detectionABSENTNo fork/sandbox-escape heuristics
Debugger detection (sysctl P_TRACED)ABSENTApp runs under Frida/lldb undetected
App tamper / re-sign detectionPARTIALThreatMetrix risk score covers transactionally
App Attest (DeviceCheck)ABSENTApple's hardware-backed attestation — recommended for fraud-sensitive flows
Privacy overlay on backgroundABSENTSensitive screens snapshot in app switcher
Screen-recording detectionN/AUIScreen.main.isCaptured available since iOS 11
CRITICAL

Privacy overlay on backgrounding

When the user backgrounds the app, iOS captures a snapshot for the app switcher. If the membership barcode or DMC card is visible, that snapshot exposes it.
Recommendation: Replace UI with a privacy overlay during scenePhase != .active for sensitive screens.
// SwiftUI
@Environment(\.scenePhase) var scenePhase

var body: some View {
    ZStack {
        contentView
        if scenePhase != .active && isSensitiveScreenVisible {
            BlurView()
                .overlay(Image(systemName: "lock.fill").font(.largeTitle))
        }
    }
}

// UIKit
override func applicationWillResignActive(_ application: UIApplication) {
    privacyOverlay.show()
}
override func applicationDidBecomeActive(_ application: UIApplication) {
    privacyOverlay.hide()
}
HIGH

Adopt App Attest for sensitive flows

Apple's DCAppAttestService provides hardware-backed attestation that the app binary is genuine and running on a non-tampered device. Server consumes the assertion and gates payment / member-card display.
Recommendation: Wire DCAppAttest into the auth flow alongside ThreatMetrix.
let service = DCAppAttestService.shared
guard service.isSupported else { return /* fall back */ }

let keyId = try await service.generateKey()
let challenge = try await fetchServerChallenge()
let attestation = try await service.attestKey(keyId, clientDataHash: challenge)
// Send attestation to server; server verifies via Apple
MEDIUM

Jailbreak detection

Heuristics: presence of /Applications/Cydia.app, ability to fork, suspicious symlinks, ability to write outside sandbox. Bypassable but raises the bar.
Recommendation: Use a maintained library (e.g. IOSSecuritySuite) with up-to-date heuristics. Combine with App Attest server-side decision.

5. Token & session security

AssetStorageStatus
Access token (JWT)Keychain (kSecAttrAccessibleWhenUnlockedThisDeviceOnly)VERIFY ACCESSIBILITY ATTRIBUTE
Refresh tokenKeychainVERIFY
Membership IDStorage SPM — verify Keychain vs UserDefaultsREVIEW
EmailUserDefaults via Storage abstractionENCRYPT or move to Keychain
App Group accessFor NSE / Widget / ClickToPay communicationAUDIT — confirm only non-sensitive items shared
iCloud Keychain syncOff for app-specific items (default for ThisDeviceOnly)PASS
HIGH

Use ...ThisDeviceOnly accessibility attributes

If a Keychain item is accessible without ThisDeviceOnly, it can be backed up via iCloud Keychain sync to other devices. For tokens that should not roam, enforce device-only.
Recommendation:
kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly

6. Memory safety — wiping sensitive data

Swift String is immutable; sensitive data should land in [UInt8] or Data with an explicit zeroing step. Pattern:

extension Data {
    mutating func secureZero() {
        withUnsafeMutableBytes { buf in
            guard let baseAddress = buf.baseAddress else { return }
            memset_s(baseAddress, buf.count, 0, buf.count)
        }
    }
}

func authenticate(token: inout Data) async throws {
    defer { token.secureZero() }
    let request = buildRequest(token: token)
    try await network.send(request)
}
MEDIUM

Audit auth flow for String-handled tokens

Search for "token: String" / "jwt: String" in auth code; convert to Data with explicit secureZero() in defer.

7. Secure logging

30+ print() sites identified in production code. Replace with os.Logger + privacy-aware interpolation:

import os

extension Logger {
    static let auth = Logger(subsystem: "com.costco.costco", category: "auth")
}

// Privacy-aware logging
Logger.auth.notice("OIDC token issued for member \(memberId, privacy: .private)")
Logger.auth.error("Token refresh failed: \(error.localizedDescription, privacy: .public)")

Custom SwiftLint rule:

# .swiftlint.yml
custom_rules:
  no_print_in_production:
    name: "print() in production"
    regex: "^\\s*print\\("
    severity: warning
    excluded:
      - ".*Tests.*"
      - ".*UITests.*"

8. ATS, certificate pinning & network security

ControlStatusNote
NSAppTransportSecurity (main app)PASSHTTPS-only enforced
NSAllowsArbitraryLoads (NSE Info.plist)FAILProduction + QA both have arbitrary loads enabled
Cert pinningPASSCostcoServerTrustManager — verify pin freshness + backup pin
TLS 1.3 minimumVERIFYSet tls_protocol_version_t.TLSv13 on URLSessionConfiguration where possible
HTTP Strict Transport SecurityN/AiOS WebKit honors HSTS automatically
CRITICAL

Replace NSAllowsArbitraryLoads in NSE

Production NSE Info.plist has NSAllowsArbitraryLoads = true — bypasses HTTPS-only policy of the main app. Likely needed for an HTTP image host; replace with NSExceptionDomains for that specific host.
Recommendation:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>img-cdn.costco.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <false/>
            <key>NSIncludesSubdomains</key>
            <true/>
        </dict>
    </dict>
</dict>

9. Code signing & distribution integrity

ControlStatus
Distribution provisioning profileVERIFY — App Store distribution only
Code signing certificateVERIFY — stored in HSM / Apple Developer keychain
Enterprise signingN/A — should be off for store builds
App Transport Security entitlementPASS if HTTPS-only
Hardened Runtime / NotarizationN/A for iOS app
Privacy Manifest signed by AppleREQUIRED 2024+

10. Privacy Manifest (PrivacyInfo.xcprivacy)

Apple requires PrivacyInfo.xcprivacy for apps and SDKs accessing Required Reason APIs since 2024. Without it, App Review will reject submissions; SDKs without manifests block your release.

<!-- Costco/Costco/PrivacyInfo.xcprivacy -->
<plist version="1.0">
<dict>
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array><string>CA92.1</string></array>
        </dict>
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array><string>C617.1</string></array>
        </dict>
    </array>
    <key>NSPrivacyTracking</key>
    <false/>
</dict>
</plist>

For each third-party SDK (Adobe, Contentstack, NokNok, ThreatMetrix, SDWebImage, RZVinyl, RZTransitions): confirm the vendor ships PrivacyInfo.xcprivacy. If any don't, the app won't pass App Review.

11. Recommendations summary (prioritized)

PriorityActionEffort
NOWRemove NSAllowsArbitraryLoads from NSE Info.plist; use NSExceptionDomains30 min
NOWGenerate PrivacyInfo.xcprivacy1 hour
NOWAdd privacy overlay on backgrounding for sensitive screens2 hours
NEXTAdopt DCAppAttest for sensitive flows1-2 weeks
NEXTBind LAContext biometric to Keychain CryptoObject3-5 days
NEXTMove email + sensitive PII to Keychain (off UserDefaults)1 day
NEXTVerify Keychain accessibility = ThisDeviceOnly30 min
NEXTReplace 30+ print() with os.Logger with privacy interpolation1 day
LATERJailbreak detection via IOSSecuritySuite or similar2-3 days
LATERAudit dSYM upload flow + retention1 day
LATERSwiftLint rule banning new print() + force-try30 min
LATERMemory-safety pass on auth — Data + secureZero2-3 days
ONGOINGTrack NowSecure findings each release; update App Privacy nutrition labelscontinuous
Costco iOS · Code Review Report · Generated 2026-05-11 · 88 machine-curated findings