Security and compliance
The iOS Secure Fields SDK is designed so that raw card data never passes through your application code. This page describes what Purse guarantees, what your application must do, and what is prohibited.
Share this page with your qualified security assessor (QSA). Formal PCI DSS scope determination requires an independent assessment.
What Purse guarantees
Card data never reaches your application code
Card field classes (SecurePANField, SecureCVVField, SecureExpDateField, SecureHolderNameField) are internal to the SDK module. Their text and attributedText getters are overridden to return nil:
override var text: String? {
get { nil } // external reads always return nil
set { super.text = newValue }
}
Any attempt to cast a card field to UITextField and read .text returns nil. Display is unaffected — UIKit renders from internal backing storage, bypassing the getter. The only outputs your application receives are:
| Output | What it contains |
|---|---|
| Field state queries | isFieldValid, isFieldFocused, hasFieldContent (all Bool), panDigitCount (Int) |
secureFieldsBrandsDetected(_:) | Detected brand(s) — e.g. [.visa] |
TokenizationResult | vaultFormToken, bin (first 8 digits), lastFourDigits, detectedBrands |
SDK-level hardening
| Protection | Detail |
|---|---|
| Field value getter override | text and attributedText return nil — no cast can extract card data |
| Accessibility blocked | accessibilityValue returns nil — VoiceOver and third-party accessibility services cannot read field content |
| Privacy overlay | UIBlurEffect placed over all card fields on willResignActiveNotification — card data is not visible in app-switcher thumbnails |
| Screen recording detection | UIScreen.capturedDidChangeNotification triggers the privacy overlay automatically when screen recording starts |
| Screenshot notification | UIApplication.userDidTakeScreenshotNotification fires secureFieldsScreenshotDetected() so your app can clear fields and warn the user |
| Keyboard learning reduced | autocorrectionType = .no on all fields, plus spellCheckingType = .no on the PAN and cardholder-name fields — typed card data is not retained in the keyboard dictionary |
| Filled values remain unreadable | The PAN field uses textContentType = .creditCardNumber so iOS can offer to fill a saved card; any filled value is still never readable by your app, because the text/attributedText getters return nil (see above) |
| CVV masked | isSecureTextEntry = true on the CVV field — input is masked and excluded from screenshots |
| Memory cleared on submit | clearSensitiveData() zeroes internal field content immediately after the tokenization request is built |
Memory cleared on clearFields() | All field buffers are zeroed; pending BIN lookups are cancelled |
| Ephemeral URLSession | No URL cache, no cookie storage, reloadIgnoringLocalCacheData policy — no card data persists in the HTTP layer |
| HTTPS-only endpoints | Vault URLs are resolved internally from VaultEnvironment and are always https:// — the host app cannot point the SDK at a plaintext endpoint |
| Card data not logged | Debug prints are guarded by #if DEBUG — no output in production builds |
Merchant obligations
Enable certificate pinning
App Transport Security (ATS) enforces HTTPS and TLS 1.2+ by default, but a rogue root CA installed via an MDM profile or malware can still perform a MITM attack. Enable SPKI pinning for production deployments:
SecureFieldsConfig(
tenantId: "${TENANT_ID}",
environment: .production,
pinnedPublicKeyHashes: [
"YOUR_PRIMARY_SPKI_HASH",
"YOUR_BACKUP_SPKI_HASH", // rotation backup — prevents downtime on cert renewal
]
)
Provide at least two hashes (primary + backup) so a certificate renewal does not break existing app versions in the field. Extract a hash from a live server:
openssl s_client -connect api.vault.purse-secure.com:443 2>/dev/null </dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform DER \
| openssl dgst -sha256 -binary \
| base64
Handle screenshot detection
The SDK fires secureFieldsScreenshotDetected() after the system saves a screenshot. iOS does not allow apps to prevent screenshots, but you can limit the exposure window:
func secureFieldsScreenshotDetected() {
secureFields.clearFields()
showAlert("Screenshot detected. For your security, please re-enter your card details.")
}
Verify the XCFramework signature (recommended)
The SDK is distributed as a signed XCFramework. You can verify the signature independently:
codesign -dv --verbose=4 PurseSecureFields.xcframework/ios-arm64/PurseSecureFields.framework
Swift Package Manager also verifies the SHA-256 checksum embedded in Package.swift before using the downloaded artifact — any tampering with the release archive produces a checksum mismatch and Xcode refuses to resolve the package.
Jailbreak detection (optional, high-security environments)
On jailbroken devices, an attacker with physical access can attach a debugger or memory scanner regardless of SDK protections. If your risk model requires it, check before rendering the payment form:
if deviceIsJailbroken() {
showAlert("Payments are not available on this device.")
return
}
Prohibited practices
The following actions undermine the card data isolation model and are prohibited:
| Practice | Why it is prohibited |
|---|---|
| Screen recording or screen mirroring while the form is visible | The SDK's privacy overlay mitigates this automatically, but deliberately working around it (e.g. disabling obscuresOnBackground without an equivalent replacement) defeats the isolation model |
| Subclassing or extending the internal card field classes | SecurePANField, SecureCVVField, SecureExpDateField, and SecureHolderNameField are internal to the SDK module; attempting to access them via runtime introspection or method swizzling violates the isolation boundary |
Using method swizzling or KVC/KVO to bypass the text getter override | The nil-returning getter is the SDK's core isolation mechanism; circumventing it via Objective-C runtime tricks defeats the SDK's entire security model |
| Building an accessibility service or automation tool to read field content | accessibilityValue is overridden to return nil specifically to block this |
| Logging delegate payloads alongside card-identifying information you hold elsewhere | SDK events never include card data, but do not correlate them with a stored PAN in your own logs in a way that could reconstruct card data |
| Reading memory from the SDK process | Attaching a debugger or memory scanner to extract card data from the SDK's heap is prohibited and constitutes a PCI DSS violation |
SAQ A-EP scope
The SDK architecture is designed to support SAQ A-EP eligibility:
- Card data is captured by an embedded component (the SDK) provided by Purse
- Your application code never directly processes, stores, or transmits raw card data
- Tokenization is handled entirely within the SDK and the Purse gateway
For SAQ A-EP to apply, you must:
- Use the production environment (
.production, which resolves tohttps://api.vault.purse-secure.com) in your release build - Enable certificate pinning via
pinnedPublicKeyHashes - Implement
secureFieldsScreenshotDetected()to clear fields and warn the user - Leave
obscuresOnBackgroundenabled (the default) unless you provide an equivalent mitigation - Not implement any of the prohibited practices listed above
- Obtain a
vaultFormTokenfrom the SDK and pass it to your backend — never handle raw card data server-side either
Your QSA will verify these controls during assessment. Purse can provide an attestation letter describing the SDK's PCI scope on request.