إنتقل إلى المحتوى الرئيسي

11 — Security threat model and device-security posture for Rasd Forms

Research note, 2026-08-15. Scope: consolidated threat model + device/app-level security posture for an offline-first React/React Native forms library used on shared, low-end Android phones by rotating enumerators handling PII of vulnerable people. Complements existing notes on encryption-at-rest choices, npm supply chain, ODK-style submission encryption, license-token security and data-protection frameworks.

Method note: the session's WebSearch budget was exhausted before this brief started, so all evidence below comes from direct WebFetch of ~45 primary pages (OWASP, Android/Apple developer docs, Expo SDK 57 docs, ODK docs, SQLite/SQLCipher, Google Play / App Store policy pages, MDN, package READMEs). Anything not fetched today is flagged [unverified].

Summary

  • Baseline standards to claim conformance against: OWASP MASVS v2.1.0 (18 Jan 2024) — categories STORAGE, PLATFORM, AUTH, RESILIENCE, PRIVACY — with the MASTG test catalogue as the verification method [1][2][8]; OWASP ASVS 5.0.0 (30 May 2025) for the web/PWA side [10]. Aim for MASVS-L2 storage/platform controls; MASVS-RESILIENCE (root detection, obfuscation) is host-app territory, not the library's.
  • Android Auto Backup is on by default (android:allowBackup="true") and backs up databases, shared prefs and filesDir (25 MB quota); Android 12+ needs android:dataExtractionRules with separate <cloud-backup> and <device-transfer> sections; allowBackup=false may not stop device-to-device transfer on some Android 12+ devices [11][12]. Expo exposes android.allowBackup in app.json; expo-secure-store's config plugin (configureAndroidBackup: true by default) already injects exclusion rules for its own prefs [22][27].
  • iOS: default Data Protection class for third-party app files is Class C (Protected Until First User Authentication); Class A (Complete) makes files unreadable while locked, which breaks background sync [18]. Keychain default is kSecAttrAccessibleWhenUnlocked (migrates via backup); use …ThisDeviceOnly variants for keys/tokens [19]. expo-file-system (SDK 57) exposes no API to set NSURLIsExcludedFromBackupKey or file-protection classes — a config plugin / native module is required [26].
  • SQLite PRAGMA secure_delete defaults off; without it (or a VACUUM) deleted rows leave forensic traces; FAST mode purges b-tree pages but not freelist pages; FTS shadow tables are not scrubbed [31]. SQLCipher encrypts every page (AES-256-CBC + HMAC-SHA512, PBKDF2 256k iterations) including WAL/journal, and wipes/locks its memory — so crypto-shredding (destroy the key) is the only reliable "secure delete" on any platform [32].
  • Browser storage is best-effort unless navigator.storage.persist() is granted; Safari can evict script-writable storage after 7 days without interaction; eviction is all-or-nothing and not a secure erase [45]. Clear-Site-Data: "storage" deletes IndexedDB/SW registrations on logout [46].
  • expo-screen-capture (SDK 57) sets Android FLAG_SECURE and blocks iOS screenshots/recordings; the screenshot listener needs READ_MEDIA_IMAGES on Android ≤13, a permission Google Play now restricts (enforced 28 May 2025) — do not use the listener [21][13][41].
  • expo-local-authentication returns only a boolean; it does not bind a key. MASTG explicitly tests for "event-bound" (non key-bound) biometrics (MASTG-TEST-0266/0327). Key-bound app-lock must go through expo-secure-store requireAuthentication (which becomes unreadable if biometrics are re-enrolled) [22][23][8].
  • Untrusted form content: DOMPurify 3.4.13 (Apache-2.0/MPL-2.0) is the web sanitizer of record (allow-list tags/attrs, ALLOWED_URI_REGEXP, RETURN_TRUSTED_TYPE, DOM-clobbering protection); it needs a DOM, so RN needs a different strategy (render markdown to native views, never HTML). react-native-markdown-display is no longer maintained, opens links via Linking.openURL and loads http(s)/data images by default [33][35].
  • Google Play requires a Permissions Declaration Form + ≤30 s video + prominent in-app disclosure for ACCESS_BACKGROUND_LOCATION, and Play Console declarations per foreground-service type (TYPE_LOCATION, TYPE_MICROPHONE) for Android 14+ [40][42]; App Store Guideline 2.5.4 limits background modes to their intended purpose and 5.1.5 requires consent before location collection [44]. Background audio recording is a near-certain rejection unless the app is genuinely a recorder — keep audio foreground-only by default.
  • Vendor packs: no UN-agency vendor questionnaire could be fetched (unverified); use CSA STAR Level 1 / CAIQ v4 as the self-assessment format [48], and prepare a MASTG-based test report plus SBOM. Residual risks that no library can close: root/jailbroken devices, coerced unlock, forensic extraction of a device with a weak PIN, host-app misconfiguration, and malicious server operators.

1. Standards baseline

StandardVersion / dateControls relevant to Rasd Forms
OWASP MASVSv2.1.0, 2024-01-18 (added MASVS-PRIVACY) [1]STORAGE-1 "securely stores sensitive data"; STORAGE-2 "prevents leakage of sensitive data" (backups, logs, IPC) [2][3]; PLATFORM-2 "uses WebViews securely" [5]; AUTH-2 "performs local authentication securely" [6]; RESILIENCE-1 "validates the integrity of the platform" [4]; PRIVACY-1 "minimizes access to sensitive data and resources" incl. SDK/SBOM accountability [7]
OWASP MASTGrolling; test IDs listed in [8]Backups: TEST-0009/0216/0262 (Android), 0058/0215 (iOS). Logs: 0003/0203/0231, 0053/0296/0297. Keyboard cache: 0006/0258, 0055/0313/0314. Pasteboard: 0073/0276–0280. Screenshots: 0010/0289/0291–0294, 0059/0290. Biometrics: 0017/0018/0326–0328, 0064/0266–0271. WebView: 0031–0033, 0077/0078/0376/0377. Root/JB: 0045/0324/0325, 0088/0240/0241
OWASP ASVSv5.0.0, 2025-05-30 [10]Web client: V3 session/token handling, V5 validation/sanitization, V14 configuration/CSP (chapter numbers [unverified] against 5.0 renumbering)

2. Threat model (attacker × asset)

Assets: D drafts, F finalized-unsynced submissions, A attachments (photos/audio/signatures), E entity/dataset lists with beneficiary PII, T audit trails (incl. GPS trail), K keys (DB key, WebCrypto key), C credentials (host bearer token, license token), X form definitions.

#Attacker / scenarioAssetsEntry vectorPrimary controls (owner)
T1Lost/stolen phone, screen-lockedD F A E T K CPhysical; adb/fastboot; cloud backup restore on another deviceEncrypted DB + Keystore/SecureStore keys (lib); backup exclusion (lib plugin + host manifest); app-lock (lib hook + host UI); purge-after-sync (lib policy)
T2Second enumerator on shared deviceD F A E TSame app session; browsing "sent" list; exportingPer-enumerator local profile + logout without data loss (lib); admin-locked settings ODK-style (host, lib exposes flags); redact PII in lists (lib default)
T3Seized device, forensic extraction (conflict setting)allChip-off / logical extraction; freelist pages; WAL; thumbnails; logsCrypto-shredding on wipe (lib); secure_delete/VACUUM for plaintext dbs (lib); no PII in logs/crash reports (lib); Class-A/WHEN_PASSCODE_SET for keys (lib default); strong device PIN + MDM (host/agency) — residual
T4Malicious/compromised server or form authorX → renderer, DXSS via labels/hints/media URLs; hostile expressions (CPU/memory bombs); oversized datasets; prototype pollution via __proto__ in JSON; malicious deep-linkSanitizer allow-list (lib MUST); no HTML on RN (lib MUST); expression sandbox with step/time limits (lib MUST); size/row caps + streaming (lib MUST); JSON schema hardening (lib MUST); media allow-list (lib option)
T5Malicious host page / third-party script (web)K C D FSame-origin JS reads IndexedDB, hijacks token in memoryNon-extractable WebCrypto key (lib); token held in memory, not IndexedDB (lib default); CSP/Trusted Types (host DOCUMENT); no unsafe-eval needed (lib)
T6Network MITM on captive Wi-FiC, sync payloadsCleartext, rogue CA on device, downgradeHTTPS only, cleartext blocked (lib refuses http: endpoints except localhost dev); optional public-key pinning hook (lib option); user-installed CAs untrusted on API ≥ 24 by default (platform) [14]
T7Insider with export rightsF A E TBulk export to unencrypted file/SD card, sharing intentEncrypted export bundle only (lib MUST); export gated by admin PIN/host permission (lib option); audit event (lib)
T8OS backup / cloud leakageD F A E K CAndroid Auto Backup / D2D transfer; iCloud/iTunes backup; Keychain migrationBackup exclusion rules (lib plugin, host applies); ThisDeviceOnly keychain class (lib); iOS isExcludedFromBackup on lib dirs (lib native module)
T9Leakage side-channelsD ELogs, crash reporters, clipboard, autofill/keyboard cache, notifications, Recents thumbnail, WebView widgets, intentsLog redaction (lib default); Sentry beforeSend scrubber + attachScreenshot=false (lib helper); FLAG_SECURE on sensitive screens (lib option); importantForAutofill="no", autoCorrect=false on PII fields (lib default); local-only WebView with originWhitelist=[] (lib)

3. Control matrix (asset × platform)

Legend: MUST = library implements and enables by default; OPT = library implements, host opts in/out; DOC = host responsibility, library documents and (where possible) lints/warns.

AssetWeb / PWAAndroid (RN/Expo)iOS (RN/Expo)
Drafts / finalized-unsynced (JSON)MUST: AES-GCM with non-extractable WebCrypto key; MUST: navigator.storage.persist() request; OPT: purge-after-sync; DOC: CSP, no third-party scripts on the form originMUST: SQLCipher when available (expo-sqlite useSQLCipher) [25], key in expo-secure-store; MUST: PRAGMA secure_delete=ON on plaintext fallback; OPT: purge-after-sync; DOC: allowBackup=false or dataExtractionRules excluding database/ [11]MUST: same DB; MUST: isExcludedFromBackup on lib DB/attachment dirs (native module); OPT: file protection Class A vs default C trade-off vs background sync [18]; DOC: com.apple.developer.default-data-protection entitlement [unverified exact key]
Attachments (photos/audio/signatures)MUST: encrypted blobs in IndexedDB or OPFS; MUST: never object-URL cached beyond render; OPT: max size capsMUST: stored under app-private dir, encrypted file-level (AES-GCM streaming); MUST: no MediaStore/gallery copies unless host opts in; DOC: exclude dir from backup; DOC: FileProvider grants when sharingMUST: isExcludedFromBackup; MUST: not written to Photos; DOC: NSPhotoLibraryAddUsageDescription only if host opts in
Datasets / entity lists (beneficiary PII)MUST: encrypted; MUST: field-level projection (only columns referenced by forms are downloaded/kept); OPT: TTL + auto-refresh purgesame + MUST: no full-table logging; OPT: search index only over non-PII columnssame
Audit trail / GPS trailMUST: encrypted, capped size, opt-in per form; MUST: never in logssame; DOC: background location = Play declaration [40]same; DOC: App Store 5.1.5 consent [44]
Host bearer tokenMUST: memory only by default; OPT: encrypted IndexedDB with explicit persistToken: true; DOC: refresh flowMUST: expo-secure-store (Keystore-backed) with keychainAccessible n/a; DOC: values > ~2 KB fail on iOS [22]MUST: SecureStore WHEN_UNLOCKED_THIS_DEVICE_ONLY [19][22]
License token (Ed25519, offline)MUST: integrity-verified; may live in IndexedDB (not secret)MUST: verified on load; store in SecureStore or plain DB (public-key verified)same
Encryption keysMUST: crypto.subtle non-extractable, stored in IndexedDB; DOC: browser profile is the boundaryMUST: SecureStore; OPT: requireAuthentication (biometric-bound; re-enrolment invalidates) [22]MUST: SecureStore WHEN_PASSCODE_SET_THIS_DEVICE_ONLY OPT (fails without passcode) [19]
Form definitions (X)MUST: sanitized on render; MUST: size cap; MUST: __proto__/constructor keys rejectedsame; MUST: markdown → native, no HTMLsame
Screens showing PIIOPT: blur on visibility-change (best effort)OPT: preventScreenCaptureAsync per screen [21]OPT: same + enableAppSwitcherProtectionAsync [21]
Logs / crash reportsMUST: structured logger with redaction; MUST: never log answers; OPT: Sentry scrubber helper [38]samesame

4. Platform facts (with citations)

4.1 Android

  • Auto Backup: default on for API ≥ 23; includes shared prefs, getFilesDir(), databases, getExternalFilesDir(); excludes getCacheDir()/getNoBackupFilesDir(); 25 MB/app quota; on Android 9+ backups are E2E-encrypted with device PIN. API ≤ 30 uses android:fullBackupContent (<full-backup-content> with <include>/<exclude domain=… path=…>), API ≥ 31 uses android:dataExtractionRules (<data-extraction-rules><cloud-backup>…</cloud-backup><device-transfer>…</device-transfer>); requireFlags="clientSideEncryption" restricts to encrypted backups; allowBackup=false may not disable D2D transfer on some Android 12+ devices [11]. Android's own risk page recommends allowBackup=false, exclusion rules, Keystore-encrypted data and getNoBackupFilesDir() [12]. MASTG-TEST-0216 fails an app that has allowBackup="true" without restrictive rules [9].
  • Expo: android.allowBackup app.json property (default true, "useful if your app deals with sensitive information" to set false) [27]; expo-secure-store plugin configureAndroidBackup (default true) writes exclusion rules for its prefs — the Rasd plugin must merge with, not overwrite, that file [22].
  • FLAG_SECURE: "Window content cannot appear in screenshots or on non-secure displays" [13]; expo-screen-capture uses it; MASTG additionally checks setRecentsScreenshotEnabled (Android 13+) and SurfaceView.setSecure [8][21].
  • Network Security Config: cleartext disabled by default API ≥ 28; user CAs untrusted by default API ≥ 24; <pin-set expiration=…> with mandatory backup pin; <debug-overrides> for dev CAs [14]. expo-build-properties (SDK 57) exposes android.usesCleartextTraffic, networkInspector, R8 minify flags — no pinning option [24]. Community react-native-ssl-public-key-pinning (MIT; OkHttp/TrustKit; Expo dev-build compatible; ≥ 2 SPKI hashes required on iOS) is the practical pinning route [47].
  • Enterprise: lock task mode requires a DPC (setLockTaskPackages), hides status bar/nav, blocks other apps; LOCK_TASK_FEATURE_* flags since 9.0 [15]. Managed configurations: res/xml/app_restrictions.xml + RestrictionsManager.getApplicationRestrictions() + ACTION_APPLICATION_RESTRICTIONS_CHANGED — the right channel for MDM-pushed server URL / project token / policy flags [16]. Dedicated devices support ephemeral users (data deleted on logout/reboot) via createAndManageUser(... MAKE_USER_EPHEMERAL) — OS-level answer to shared devices, but optional per OEM [17].
  • Keystore: expo-secure-store values are Keystore-encrypted and stored in SharedPreferences, deleted on uninstall; requireAuthentication data becomes inaccessible after biometric changes [22].

4.2 iOS

  • Data Protection classes A–D; Class C is the default for third-party app data; A and C class keys are discarded on lock (A within 10 s), so Class A files are unreadable during background sync [18].
  • Keychain accessibility default kSecAttrAccessibleWhenUnlocked (migrates via backup); …ThisDeviceOnly and WhenPasscodeSetThisDeviceOnly do not migrate [19]. SecureStore items persist across reinstall on iOS [22] — a wipe must explicitly delete them.
  • Backup exclusion: NSURLIsExcludedFromBackupKey/isExcludedFromBackupKey per file/directory; Caches and tmp are never backed up (Apple QA1719 — page not machine-readable today; facts long-standing, [unverified today]) [20]. expo-file-system SDK 57 has no exposed API for it [26], and expo-sqlite docs do not address backups [25] → Rasd needs a tiny native module / config plugin.
  • Screenshots: expo-screen-capture prevents recordings (iOS 11+) and screenshots (iOS 13+) and offers app-switcher blur [21].

4.3 Web / PWA

  • Best-effort storage evicted under pressure (LRU); persist() upgrades to persistent (Firefox prompts, Chrome/Safari auto-decide); Safari ITP deletes script-writable storage after 7 days without interaction; eviction is whole-origin and not a secure erase [45].
  • Clear-Site-Data: "storage" runs deleteDatabase on every IndexedDB and unregisters service workers — a good server-side logout/wipe backstop [46].
  • DOMPurify 3.4.13: allow-list config (ALLOWED_TAGS/ATTR, ALLOWED_URI_REGEXP, FORBID_TAGS), RETURN_TRUSTED_TYPE, SANITIZE_DOM (clobbering) default on; requires a DOM (jsdom in Node; not RN) [33].

4.4 Secure deletion facts

  • SQLite: secure_delete normally off; ON zero-fills deleted content; FAST scrubs b-tree pages only; otherwise VACUUM after delete; FTS3/FTS5 shadow tables may still hold traces [31].
  • SQLCipher: page-level AES-256-CBC + per-page HMAC-SHA512, PBKDF2-HMAC-SHA512 256,000 iterations, encrypted WAL/journal/statement journals, memory locked+wiped; file-based temp storage should be disabled [32]. Deleted-but-unvacuumed pages remain ciphertext → destroying the key is sufficient.
  • IndexedDB: no API-level secure delete; rely on crypto-shredding of the WebCrypto key record [45].

5. Untrusted form-definition content

  1. Web renderer: DOMPurify.sanitize(html, { USE_PROFILES: {html:true}, ALLOWED_TAGS: [p,br,b,strong,i,em,u,s,ul,ol,li,a,span,img,h1-h6,blockquote,code,pre,table,thead,tbody,tr,th,td,sup,sub,bdi,bdo], ALLOWED_ATTR: [href,title,alt,src,dir,lang,class,colspan,rowspan], ALLOWED_URI_REGEXP: /^(?:https?|mailto|tel|data:image\/(?:png|jpeg|webp|gif);base64,)/i, RETURN_TRUSTED_TYPE: true }) plus a uponSanitizeAttribute hook that drops src not matching mediaAllowList and forces rel="noopener noreferrer" + target="_blank" [33]. Keep dir/lang for Arabic RTL. Never use dangerouslySetInnerHTML outside the sanitized path.
  2. RN renderer: parse markdown (markdown-it, html: false) to an AST and render native Text/Image; drop raw HTML tokens; images only from mediaAllowList; links open only via host callback (onOpenLink), never Linking.openURL implicitly. Do not adopt react-native-markdown-display (unmaintained; default Linking.openURL; http(s)/data image loading) [35].
  3. WebView widgets (signature pad): bundle HTML locally, originWhitelist={[]}/['about:blank'], javaScriptEnabled only for the bundled asset, allowFileAccess=false, mixedContentMode="never", keep setSupportMultipleWindows=true, validate every onMessage payload as JSON with a schema [34]; prefer a pure-RN/Skia signature pad to avoid WebView entirely.
  4. JSON hardening: parse with a reviver or post-walk that rejects __proto__, constructor, prototype keys; use Object.create(null)/Map for lookup tables; freeze compiled form definitions [37]. Cap: definition ≤ 2 MB, dataset ≤ 100k rows default, choice list ≤ 10k, string ≤ 64 KB, expression AST ≤ 5k nodes, evaluation budget ≤ 10 ms/step with abort.
  5. Media/URLs: mediaAllowList defaults to the host's sync origin; anything else is blocked and reported via onPolicyViolation.

6. Transport and tokens

  • Refuse non-https: endpoints unless allowInsecureDev and host is localhost/10.0.2.2 [14][24]. Do not ship a pinning implementation; expose fetch injection so hosts can plug react-native-ssl-public-key-pinning [47]. Pinning is worth it only for agencies with stable, agency-controlled certificates; for typical NGO backends behind Cloudflare/managed TLS, pin failures cause field outages — document that trade-off and default off.
  • Web token: hold in memory; if the host wants offline re-auth it supplies a token provider; never write tokens to localStorage. RN token: expo-secure-store (≤ ~2 KB on iOS; large JWTs must be split or exchanged for opaque tokens) [22].
  • Bearer tokens in sync logs and tus upload URLs must be redacted; tus URLs are capability URLs — treat as secrets.

7. PII leakage vectors checklist

VectorLibrary defaultEvidence
Console/OS logsRedacting logger; answers/entities never logged; debug level stripped in production buildsMASTG-TEST-0003/0053/0203 [8]
Crash reporterscreateSentryScrubber() returning beforeSend/beforeBreadcrumb that drops form data; sendDefaultPii=false; attachScreenshot=false (default off, opt-in only, "screenshots may contain PII")[38][39]
ClipboardPII fields render with contextMenuHidden when field.sensitive; no lib copy-to-clipboard; note expo-clipboard lacks iOS local-only/expiry and Android sensitive flags[36][28]
Autofill / keyboard cacheimportantForAutofill="no", autoComplete="off", autoCorrect={false}, textContentType="none" on sensitive fields[36]; MASTG-TEST-0006/0055 [8]
Recents thumbnail / screenshotspreventScreenCaptureAsync while a sensitive screen is mounted (opt-in policy flag)[21][13]
NotificationsNever include answers or names in sync notifications; only countsMASVS-STORAGE-2 [3]
Deep linksLibrary exposes no deep-link handler; host validates params; form IDs onlyMASVS-PLATFORM
WebView widgetsLocal content only, no bridge except typed messages[34]
Intents / share sheetsExport only through exportEncryptedBundle(); no implicit SharingT7
BackupsPlugin writes exclusion rules; iOS native module marks lib dirs excluded[11][20]

8. Shared devices, app lock, ODK model

  • ODK Collect: admin password protects "Access Control" (hide Get Blank Form / Delete Saved Form / Edit Saved Form / Send Finalized; restrict form-entry navigation) and settings; "Delete after send" removes filled forms after upload [29]. Settings QR codes contain admin and server passwords in plaintext unless stripped — a cautionary model for any QR provisioning Rasd offers [30].
  • Rasd model: profiles keyed by enumerator ID; each profile has its own DB namespace and record ownership; switchProfile() requires PIN/biometric via host onLock; logout() never deletes unsynced records but hides them from other profiles; supervisor override via admin PIN with audit event. Screen lock after N minutes idle and on app background (autoLockAfterMs).
  • MDM path: expose managed-configuration keys (serverUrl, projectToken, policyProfile) read via a host adapter; recommend Android Enterprise ephemeral users or lock-task kiosk for high-risk deployments [16][17][15]. Agency-specific device guidance (UNHCR/WFP) [unverified — not fetched].

9. Remote wipe and purge semantics

  • Server signals via sync response header/flag (X-Rasd-Wipe: <nonce> or policy.wipeAt) → library validates nonce against the host token, then: (1) revoke SecureStore/WebCrypto keys (crypto-shred), (2) delete DB files (deleteDatabaseAsync) and attachment dirs, (3) on plaintext fallback DBs run secure_delete=ON + VACUUM before delete, (4) clear profiles, (5) emit onRemoteWipe({ reason, unsyncedCount }) — the host decides UX. Web adds Clear-Site-Data from the server [46][25][31].
  • Purge-after-sync default: finalized records + attachments removed after server ACK; drafts kept; "sent" list shows metadata only (no answers) — mirrors ODK "delete after send" [29].
  • Local wipe when unsynced data exists must be a two-step confirmation with count shown; never silent.
type SecurityPolicy = {
encryption: { atRest: 'required' | 'preferred' | 'off' /* default 'preferred' */; keyAuth: 'none' | 'biometric' /* default 'none' */ };
backup: { excludeFromOsBackup: boolean /* true */; excludeFromDeviceTransfer: boolean /* true */ };
retention: { purgeFinalizedAfterSync: boolean /* true */; draftMaxAgeDays?: number /* 30 */; datasetTtlHours?: number /* 168 */; auditTrail: 'off' | 'events' | 'events+gps' /* 'events' */ };
lock: { autoLockAfterMs: number /* 300000 */; lockOnBackground: boolean /* true */; requireForExport: boolean /* true */ };
screen: { preventCaptureOn: 'none' | 'sensitiveFields' | 'all' /* 'sensitiveFields' */ };
content: { markdown: 'safe' | 'off' /* 'safe' */; mediaAllowList: string[] /* [syncOrigin] */; maxDefinitionBytes: number /* 2 MiB */; maxDatasetRows: number /* 100000 */; exprBudgetMs: number /* 10 */ };
transport: { requireHttps: boolean /* true */; fetch?: typeof fetch /* pinning hook */ };
tokens: { persistHostToken: boolean /* false on web */ };
logging: { level: 'error'|'warn'|'info'|'debug' /* 'warn' */; redactor?: (rec) => rec };
export: { mode: 'encrypted-only' /* only mode */; recipientKeys?: string[] };
profiles: { enabled: boolean /* true */; adminPinHash?: string };
};
// events
onLock(reason: 'idle'|'background'|'switch'|'policy'); onUnlockRequired(next: () => void);
onRemoteWipe({ reason, unsyncedCount }); onLocalWipeRequested({ unsyncedCount, confirm });
onPolicyViolation({ kind: 'html'|'url'|'size'|'expr'|'proto'|'transport', detail });
onSecureStoreUnavailable({ fallback: 'plaintext-db' | 'refuse' }); onBackupExclusionMissing(platform);

Startup self-check (rasd.securityReport()) returns which controls are active (SQLCipher on?, backup rules present?, iOS exclusion applied?, https?, sanitizer profile) for the vendor pack and for the host's CI.

11. Store policy checklist (location / audio / camera / media)

FeatureGoogle PlayApp Store
Foreground GPS on questionRuntime ACCESS_FINE_LOCATION; Data safety "Location" declared [43]NSLocationWhenInUseUsageDescription; 5.1.5 consent [44]
Background location trail (audit)ACCESS_BACKGROUND_LOCATION only for core feature; Permissions Declaration Form (single feature), prominent disclosure with the word "location" and "when the app is closed", ≤30 s video, privacy policy URL [40]; Android 14+ FOREGROUND_SERVICE_LOCATION type declaration + video [42]5.1.5 + Background Modes only for intended purpose (2.5.4) [44]; expect review scrutiny — ship as opt-in module, off by default
Audio recordingRECORD_AUDIO; background needs FOREGROUND_SERVICE_MICROPHONE declaration ("voice recording" listed as acceptable) [42]Foreground only by default; background audio recording under 2.5.4 is a common rejection [unverified statistic]
Camera / photosUse system Photo Picker; do not request READ_MEDIA_IMAGES/VIDEO unless core (enforced 28 May 2025) — so no expo-screen-capture screenshot listener [41]NSCameraUsageDescription; only add Photos usage strings if host saves to library
Data safety / privacy labelsDeclare SDK-originated collection; local-only processing is not "collection"; deletion mechanism or auto-deletion within 90 days [43]5.1.1 privacy policy + purpose strings; 5.1.2 no repurposing [44]

12. Verification plan

  • MASTG automation (host CI, lib provides scripts): 0216/0262 (parse merged manifest + XML rules; fail if DB/attachment dirs not excluded), 0215 (iOS: assert exclusion attribute on lib dirs via XCTest helper), 0203/0231 (grep release bundle for console.*/Log. calls in lib code), 0258/0313 (static: sensitive inputs carry autofill/keyboard-cache props), 0291–0294 (static: FLAG_SECURE API references present), 0326–0328/0266–0271 (biometric key-binding path uses SecureStore requireAuthentication, not bare LocalAuthentication) [8][9].
  • Static checks in the lib repo: ESLint rules banning dangerouslySetInnerHTML, eval/new Function, Linking.openURL, console.log outside logger; type test that SecurityPolicy defaults match this note; fuzz tests for the expression sandbox (time/step budget), JSON prototype-pollution corpus, DOMPurify bypass corpus (mXSS vectors).
  • Runtime evidence: securityReport() JSON, SQLCipher PRAGMA cipher_version check, backup restore test on Android emulator (bmgr backupnow → reinstall → assert no DB), iOS backup exclusion via xcrun simctl file attribute dump.
  • Vendor pack (see §13) built from these outputs.

13. UN vendor security pack (content list)

No UNHCR/WFP vendor questionnaire could be fetched (unverified); structure the pack around CSA STAR Level 1 / CAIQ v4 (self-assessment against CCM v4; Valid-AI-ted review $595, free for CSA members) [48], with HECVAT Lite and SIG Lite as alternate formats [versions unverified]. Contents: (1) architecture & data-flow diagram (device ↔ host server; the vendor never receives beneficiary data — only license telemetry); (2) data inventory per asset class with encryption/retention defaults; (3) MASVS 2.1 mapping table with MASTG evidence; (4) ASVS 5.0 mapping for the web bundle; (5) SBOM (CycloneDX) + provenance attestations from OIDC publishing (see existing supply-chain note); (6) DPIA template for host agencies (lawful basis under UN PDPP, data minimization defaults, retention, wipe procedure); (7) vulnerability disclosure policy + SLA; (8) SOC 2 Type 1 scope statement covering only the license service and update distribution (device data out of scope, since it never transits vendor systems); (9) store-policy compliance sheet (§11); (10) residual-risk register (§14).

14. Residual risks (cannot be closed by a library)

  1. Rooted/jailbroken or malware-infected devices: Keystore/SecureStore, FLAG_SECURE and sandboxing are all bypassable; only host-side root detection (MASVS-RESILIENCE) and MDM attestation reduce this [4].
  2. Coerced unlock / weak or shared device PIN: on-device encryption is bound to the device lock; enumerators sharing a PIN defeats T1/T3.
  3. Forensic recovery of freed pages / thumbnails / keyboard learning data outside the app sandbox.
  4. Host misconfiguration: allowBackup=true without rules, tokens in localStorage, missing CSP, disabling purgeFinalizedAfterSync — mitigated only by warnings and securityReport().
  5. Malicious server operator: can push forms that collect more PII than needed; sanitizer prevents code execution, not over-collection.
  6. Web platform limits: no key-bound biometrics, storage eviction, browser extensions with page access.
  7. Loss of unsynced data on wipe/eviction is itself a protection risk (beneficiary interviews repeated) — a trade-off the host must own.

Implications & recommendations for Rasd Forms

  1. Ship a @rasd/forms-expo config plugin that (a) sets allowBackup=false or writes dataExtractionRules excluding database/ and the attachments dir for both cloud-backup and device-transfer, merging with expo-secure-store rules; (b) adds a small iOS native module that sets isExcludedFromBackup on lib dirs at first run; (c) optionally sets the Data Protection entitlement (document Class A vs background-sync trade-off) [11][22][18][26].
  2. Default encryption.atRest='preferred' (SQLCipher via useSQLCipher, key in SecureStore WHEN_UNLOCKED_THIS_DEVICE_ONLY; web AES-GCM non-extractable) and emit onSecureStoreUnavailable rather than silently falling back [25][22][19].
  3. Treat crypto-shredding as the wipe primitive; add secure_delete=ON+VACUUM only for plaintext fallback DBs; expose wipeLocal() and server-triggered onRemoteWipe [31][32].
  4. Purge finalized submissions and attachments after ACK by default; keep only metadata for the "sent" view [29].
  5. Provide createSentryScrubber() and a redacting logger; document attachScreenshot stays false [38][39].
  6. Sanitize with DOMPurify on web (allow-list above, Trusted Types) and render markdown-to-native on RN; never render HTML in RN; enforce media allow-list and size/expr budgets; reject prototype-polluting keys [33][35][37].
  7. App-lock: expose onLock/onUnlockRequired and a key-bound option using SecureStore requireAuthentication; document that bare expo-local-authentication is UX-only (fails MASTG event-bound tests) [22][23][8].
  8. Do not implement TLS pinning in-core; accept a custom fetch and document react-native-ssl-public-key-pinning; hard-fail on http: outside dev [14][47].
  9. Keep background location trail and audio recording as separately-installed optional modules with store-declaration templates included; default foreground-only [40][42][44].
  10. Never call the screenshot listener API (needs READ_MEDIA_IMAGES ≤ Android 13, Play-restricted) [21][41].
  11. Implement enumerator profiles with logout-without-loss and admin PIN, and read Android managed configurations for MDM-pushed settings; publish an "ODK-style admin lock" recipe [16][29][30].
  12. Publish securityReport(), MASTG mapping, SBOM, CAIQ v4 self-assessment and a DPIA template as the vendor pack; state SOC 2 scope as license/update services only [48].

Sources (accessed 2026-08-15)

  1. OWASP MASVS releases — https://github.com/OWASP/owasp-masvs/releases
  2. MASVS-STORAGE-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-STORAGE-1.md
  3. MASVS-STORAGE-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-STORAGE-2.md
  4. MASVS-RESILIENCE-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-RESILIENCE-1.md
  5. MASVS-PLATFORM-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-PLATFORM-2.md
  6. MASVS-AUTH-2 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-AUTH-2.md
  7. MASVS-PRIVACY-1 — https://raw.githubusercontent.com/OWASP/owasp-masvs/master/controls/MASVS-PRIVACY-1.md
  8. OWASP MASTG tests index — https://mas.owasp.org/MASTG/tests/
  9. MASTG-TEST-0216 — https://mas.owasp.org/MASTG/tests/android/MASVS-STORAGE/MASTG-TEST-0216/
  10. OWASP ASVS releases (v5.0.0, 2025-05-30) — https://github.com/OWASP/ASVS/releases
  11. Android Auto Backup — https://developer.android.com/identity/data/autobackup
  12. Android security risk: backup leaks — https://developer.android.com/privacy-and-security/risks/backup-leaks
  13. Android WindowManager.LayoutParams#FLAG_SECUREhttps://developer.android.com/reference/android/view/WindowManager.LayoutParams#FLAG_SECURE
  14. Android Network Security Configuration — https://developer.android.com/privacy-and-security/security-config
  15. Android lock task mode — https://developer.android.com/work/dpc/dedicated-devices/lock-task-mode
  16. Android managed configurations — https://developer.android.com/work/managed-configurations
  17. Android multiple users on dedicated devices — https://developer.android.com/work/dpc/dedicated-devices/multiple-users
  18. Apple Platform Security: Data Protection classes — https://support.apple.com/guide/security/data-protection-classes-secb010e978a/web
  19. Apple: Restricting keychain item accessibility — https://developer.apple.com/documentation/security/keychain_services/keychain_items/restricting_keychain_item_accessibility
  20. Apple QA1719 / isExcludedFromBackupKey (page not machine-readable at fetch time; unverified today) — https://developer.apple.com/library/archive/qa/qa1719/_index.html
  21. Expo ScreenCapture (SDK 57) — https://docs.expo.dev/versions/latest/sdk/screen-capture/
  22. Expo SecureStore (SDK 57) — https://docs.expo.dev/versions/latest/sdk/securestore/
  23. Expo LocalAuthentication (SDK 57) — https://docs.expo.dev/versions/latest/sdk/local-authentication/
  24. Expo BuildProperties (SDK 57) — https://docs.expo.dev/versions/latest/sdk/build-properties/
  25. Expo SQLite (SDK 57) — https://docs.expo.dev/versions/latest/sdk/sqlite/
  26. Expo FileSystem (SDK 57) — https://docs.expo.dev/versions/latest/sdk/filesystem/
  27. Expo app config reference (android.allowBackup) — https://docs.expo.dev/versions/latest/config/app/
  28. Expo Clipboard (SDK 57) — https://docs.expo.dev/versions/latest/sdk/clipboard/
  29. ODK Collect settings / Access Control — https://docs.getodk.org/collect-settings/
  30. ODK Collect settings QR import/export — https://docs.getodk.org/collect-import-export/
  31. SQLite PRAGMA secure_deletehttps://www.sqlite.org/pragma.html#pragma_secure_delete
  32. SQLCipher design — https://www.zetetic.net/sqlcipher/design/
  33. DOMPurify README (v3.4.13) — https://github.com/cure53/DOMPurify
  34. react-native-webview Reference — https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md
  35. react-native-markdown-display README (unmaintained notice) — https://github.com/iamacup/react-native-markdown-display
  36. React Native TextInput (0.87) — https://reactnative.dev/docs/textinput
  37. OWASP Prototype Pollution Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Prototype_Pollution_Prevention_Cheat_Sheet.html
  38. Sentry React Native: sensitive data — https://docs.sentry.io/platforms/react-native/data-management/sensitive-data/
  39. Sentry React Native: screenshots — https://docs.sentry.io/platforms/react-native/enriching-events/screenshots/
  40. Google Play: location permissions policy — https://support.google.com/googleplay/android-developer/answer/9799150
  41. Google Play: photo and video permissions policy — https://support.google.com/googleplay/android-developer/answer/14115180
  42. Google Play: foreground service permissions (Android 14+) — https://support.google.com/googleplay/android-developer/answer/13392821
  43. Google Play: Data safety section — https://support.google.com/googleplay/android-developer/answer/10787469
  44. Apple App Store Review Guidelines (2.5.4, 5.1.1, 5.1.2, 5.1.5) — https://developer.apple.com/app-store/review/guidelines/
  45. MDN: Storage quotas and eviction criteria — https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria
  46. MDN: Clear-Site-Data — https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Clear-Site-Data
  47. react-native-ssl-public-key-pinning — https://github.com/frw/react-native-ssl-public-key-pinning
  48. CSA STAR / CAIQ v4 — https://cloudsecurityalliance.org/star

Not retrievable today (403/404/auth-walled), flagged unverified where referenced: EDUCAUSE HECVAT, Shared Assessments SIG, Apple FileProtectionType/isExcludedFromBackupKey developer pages, AndroidX EncryptedSharedPreferences reference, markdown-it docs/security.md, UNHCR/WFP vendor security questionnaires.