Skip to content

Data Protection Impact Assessment – QR Viewer

The current version is kept in the repository: github.com/thorsten-l/qr_viewer/blob/main/DPIA.md

App QR Viewer 2.2.0 (build 14), iOS/iPadOS and Android
Code base main, audit on commit b25767c ("release 2.1.2"); measures for F2 and F6 in 2.2.0
Controller / author Thorsten Ludewig, t.ludewig@gmail.com
Date 14 October 2026
Basis Art. 35 GDPR, § 25 TDDDG (German Telecommunications Digital Services Data Protection Act)
Status Assessment complete; F2 and F6 implemented in 2.2.0, other measures open (section 8)

This document combines a technical security audit of the app with a data protection impact assessment (DPIA). It is not legal advice.


1. Summary

QR Viewer is a scanner for QR codes and barcodes. It shows the content first and opens links or other actions only after the user confirms. The app has no account, no server of its own, no advertising and no analytics. The app code itself makes no network connections. All recognition and parsing happens on the device.

Is a DPIA required? Most likely not. The developer does not receive any personal data. Nothing is processed on a large scale or used for profiling. None of the cases in Art. 35(3) GDPR or on the German DSK list applies. This assessment is voluntary and documents why the risk is low.

Result: The risk for data subjects is low. Two points need attention:

  1. On Android, Google ML Kit sends technical usage data to Google (finding F1). This is disclosed in the privacy policy, but under § 25 TDDDG it is questionable whether that is enough without consent.
  2. Up to 2.1.2 the scan history was stored unencrypted and was part of operating system backups (finding F2). Scanned codes can contain Wi-Fi passwords, Matter setup codes, or ticket data with a name and date of birth. Implemented in 2.2.0: the history is encrypted with AES-256-CBC + HMAC-SHA256; the key stays on the device; Android backup is off.

No high residual risk remains. Prior consultation with a supervisory authority under Art. 36 GDPR is not necessary.


2. Scope and method

Examined:

Not examined:

Risk scale: likelihood × severity, each rated low / medium / high, based on the German Standard Data Protection Model (SDM).


3. Description of the processing

3.1 Purpose

The app reads the content of optical codes and shows it in a way people can understand. Examples are links, contacts, Wi-Fi access data, GiroCode bank transfers, 2FA keys, VDV tickets, Deutsche Post codes and Matter setup codes. It protects users from harmful codes ("quishing") because nothing is opened automatically.

3.2 Data subjects and categories of data

Data subject Data Source
User Camera image (live only, not stored) Camera
User Scan history (last 25 contents with time, format, type) Scans
User Settings: scan mode, zoom level App
Third parties in codes Names, phone numbers, email and postal addresses (vCard/MECARD), appointments Code content
Ticket holder Name (plain or masked), date of birth, ticket number, validity VDV ticket (Deutschlandticket)
User / third parties Wi-Fi passwords, 2FA secrets, Matter setup PIN, IBAN/name (GiroCode) Code content
User (Android) Device model, OS version, package name and version, ML Kit performance metrics ML Kit SDK

Special categories under Art. 9 GDPR are not processed on purpose. A code can contain such data by chance, for example a text about health. The app shows it just like any other text.

3.3 Storage locations

Data Location Protection Deletion
History (scan_history_v2) SharedPreferences (Android XML in the app sandbox) / NSUserDefaults (iOS) OS sandbox, device encryption; AES-256-CBC + HMAC-SHA256 since 2.2.0 Single entry, "Clear history", uninstall; max. 25 entries
History key (32 bytes, random) iOS Keychain (AfterFirstUnlockThisDeviceOnly, not synced) / Android Keystore Hardware-backed where available; never in backups Uninstall
2FA keys not stored – –
Scan mode, zoom SharedPreferences / NSUserDefaults Sandbox Uninstall
Camera image Memory only – Immediately
Export files (CSV/JSON), vCard/iCalendar Temporary file from share_plus in the app cache, then the target app Sandbox; in the target app, its own rules apply Cache is cleared by the OS

3.4 Data flows and recipients

# Flow Trigger Recipient
D1 Camera image → barcode recognition Scanning On the device: Apple Vision (iOS) or ML Kit (Android)
D2 ML Kit telemetry Automatic, Android only Google (Firelog/CCT via com.google.android.datatransport)
D3 Open a link User confirms the dialog Browser or app that handles the scheme, then the website
D4 Email, call, SMS, map, authenticator User confirms The matching system app
D5 "Look up online" User confirms DuckDuckGo (GTIN) / Open Library (ISBN)
D6 "Track shipment" User confirms Deutsche Post (deutschepost.de, tracking number in the URL)
D7 Copy, share, contact, calendar, history export User taps Clipboard / app chosen in the system share sheet
D8 Support, manuals User taps GitHub
D9 Backup Operating system iOS: iCloud/device backup, history only as ciphertext. Android: none since 2.2.0 (allowBackup="false")

3.5 Permissions

Platform Permission Origin Use
iOS Camera (NSCameraUsageDescription) App Scanning
Android CAMERA mobile_scanner Scanning
Android INTERNET, ACCESS_NETWORK_STATE ML Kit / datatransport (not used by app code) Telemetry D2
Android DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION AndroidX Internal

The app does not request location, contacts, calendar, storage, microphone or Bluetooth. Contacts and appointments are passed on as .vcf or .ics through the share sheet, so no contacts or calendar permission is needed.



5. Necessity and proportionality

Principle Assessment
Purpose limitation Data is used only to display it and for actions the user starts.
Data minimisation Camera image is not stored. History is limited to 25 entries. 2FA keys are not stored. Tracking parameters are removed from links. There are no unnecessary permissions. But: Wi-Fi passwords and Matter PINs end up in the history.
Storage limitation Only 25 entries; entries can be deleted one by one or all at once.
Transparency Privacy policy in German/English; info button on every data card; a confirmation dialog shows the full target.
Integrity / confidentiality OS sandbox; no network code in the app; no WebView; parsers check lengths. See F2–F5.
Data subject rights The developer holds no data. The user controls everything locally.

6. Security audit – findings

Severity: High / Medium / Low / Info.

ID Severity Finding Recommendation
F1 Medium ML Kit telemetry on Android. mobile_scanner uses com.google.mlkit:barcode-scanning. The release manifest therefore contains INTERNET as well as TransportBackendDiscovery with CctBackendFactory. Google receives device and usage data without the user taking any action. This is disclosed in PRIVACY.md, but there is no consent (§ 25 TDDDG). (a) Have the legal question reviewed. (b) Look for an alternative recognition library without telemetry (e.g. ZXing-based) for a "no network" Android build. (c) At least: make sure the Play "Data safety" section reports the data correctly.
F2 Medium → implemented in 2.2.0 History unencrypted and included in backups. scan_history_v1 is plain JSON in SharedPreferences/NSUserDefaults. Android Auto Backup is active because android:allowBackup is not set (default true). iOS includes UserDefaults in iCloud/device backups. The history can contain Wi-Fi passwords, Matter PINs, GiroCode data (IBAN, name) and VDV tickets (name, date of birth). (a) Exclude history from backups: Android allowBackup="false" or dataExtractionRules/fullBackupContent without sharedpref; on iOS, store the history in a file with isExcludedFromBackup. (b) Like 2FA keys, do not store Wi-Fi passwords and Matter codes, or store them only masked. (c) Optional: a setting "Keep history" on/off.
F3 Low Clipboard without a "sensitive" flag. "Copy" puts passwords and secrets on the clipboard in plain text. Android 13+ shows them in the preview and in keyboard clipboard histories. iOS can sync them through Universal Clipboard. On Android, set ClipDescription.EXTRA_IS_SENSITIVE for password/secret fields (platform channel). On iOS, use UIPasteboard with localOnly and expirationDate.
F4 Low App switcher / screenshots. Results with secrets are visible in the task preview. Neither FLAG_SECURE nor an iOS privacy overlay is used. Since 2.2.0, "Show code" displays the content as a scannable image; anyone nearby can capture it with a camera (e.g. Wi-Fi password, 2FA secret). You start this display deliberately. Optional: hide the preview while the result page shows a password or secret.
F5 Low History export in plain text. CSV/JSON contain all entries without protection. Storage then depends on the target app (cloud, email). The user starts the export on purpose. Mention in the export dialog that sensitive content is included (e.g. Wi-Fi passwords).
F6 Info → fixed in 2.2.0 Privacy policy inaccurate about VDV tickets. PRIVACY.md says "masked name". The parser (decodeTravelerName) also shows names in plain text (First#Last) when the ticket contains them. Update the wording: "name (plain or masked, depending on the ticket)".
F7 Info No app-level privacy manifest on iOS. ios/Runner/PrivacyInfo.xcprivacy is missing. The plugins (shared_preferences_foundation, mobile_scanner, url_launcher_ios, package_info_plus, share_plus, flutter_secure_storage) ship their own, so App Store requirements are met. Optional: add an app manifest with NSPrivacyTracking = false and no collected data types, for completeness.
F8 Info Blocklist instead of allowlist for URL schemes. linkUri blocks file, content, data, javascript, vbscript, intent, about, blob and chrome. Other schemes with // and fido, magnet, bitcoin, lightning, ethereum are allowed, after a confirmation that shows the full target. Acceptable. If needed, switch to an allowlist (http, https, market, itms-apps, …).
F9 Info Parsing untrusted input. The BER parser checks every length and limits the length field to 3 bytes. VDV decoding is wrapped in try/catch, and so are the binary and Deutsche Post parsers. RSA (modPow) works only on data that fits in a QR code (a few KB), so there is no significant risk of a denial-of-service attack. There is no WebView, no dynamic code and no deep links. The only exported components are the launcher activity and the ProfileInstallReceiver, which is protected by the DUMP permission. No action needed. Keep the fuzz/edge-case tests.
F10 Info Dependencies. All direct dependencies are up to date except cupertino_icons (1.0.9 → 2.0.0, icons only). Some transitive packages lag by patch versions. New in 2.2.0: flutter_secure_storage (Keychain/Keystore), pointycastle and barcode_widget (pure Dart, no network, no permissions). No check against vulnerability databases was done. Run flutter pub upgrade regularly. Add osv-scanner to the release checklist.

Existing protective measures (positive)


7. Risks to data subjects

Likelihood (L) and severity (S): low / medium / high.

Risk Cause L S Before measures After measures (section 8)
R1 Google learns about app use ML Kit telemetry (F1) high (Android) low (no content, no images) medium low (with an alternative library) / unchanged (disclosure only)
R2 Someone reads secrets from the history Unlocked or lost device, backup account compromised (F2) low medium low–medium low (encrypted since 2.2.0; on an unlocked device the app still shows the history)
R3 Secrets leak via the clipboard Clipboard history, keyboard apps, Universal Clipboard (F3) low medium low low
R4 Phishing / malware via codes Harmful link in a QR code medium high low (confirmation, full target shown, scheme blocklist) low
R5 Lookup services learn product or tracking numbers User taps "Look up online" / "Track shipment" medium low low low
R6 Shoulder surfing / screenshots Task preview (F4) low low low low
R7 Export ends up in an insecure place Plain-text export (F5) low medium low low

8. Measures plan

Priority Measure Finding Status
1 Encrypt the history (AES-256-CBC + HMAC, key in Keychain/Keystore) and disable Android backup F2 done (2.2.0)
2 Do not store Wi-Fi passwords and Matter setup codes in the history (like 2FA), or mask them F2 open
3 Check and, if needed, align the Play Console "Data safety" with ML Kit telemetry; legal review of § 25 TDDDG F1 open
4 Evaluate an Android recognition library without telemetry F1 open
5 Correct PRIVACY.md on VDV names F6 done (2.2.0)
6 Mark sensitive clipboard content F3 open
7 Note about sensitive content in the history export F5 open
8 Optional: hide the task preview, add an iOS app privacy manifest, use an allowlist for schemes F4, F7, F8 open
9 osv-scanner / flutter pub outdated in the release checklist F10 open

9. Result and review

Role Name Date
Controller Thorsten Ludewig