From cfae9af250deaa99648c263431cb9b67c873c8cc Mon Sep 17 00:00:00 2001 From: Sagar Ghag Date: Thu, 16 Jul 2026 21:10:38 +0530 Subject: [PATCH 1/3] docs(compliance): drop false anti-spoofing claims; add Privacy & compliance section - README no longer claims PAD against photos/recordings/deepfakes/3D masks (it has none); states plainly it is active challenge-response only, not ISO/IEC 30107 PAD (removes FTC Act 5 / EU UCPD deceptive-claim exposure) - remove NSMicrophoneUsageDescription instruction (mic is unused; App Store 5.1.1 risk) - add Privacy & compliance section: on-device-only, ML Kit disclosure, no encryption/ retention by the library, consent is the consuming app's duty (GDPR/BIPA/PIPL/DPDP) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 12f5484..c33dc8e 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,16 @@ - [Native Setup](#flutterSetupNativeSetup) - [iOS](#flutterSetupNativeiOS) - [Android](#flutterSetupNativeAndroid) +- [Privacy & compliance](#privacy) - [Contributors](#contributors) ## What is the Livelyness Detection? -Liveness detection for face recognition in biometrics is a technique where an algorithm detects if the person in front of the camera is alive and real. The algorithm is able to recognize a live person from presentation attacks - where a bad actor, or fraud perpetrator, uses someone else's physical characteristics or biometric data (known as "spoofs") for impersonation. +This package runs a short sequence of **active liveness challenges** — blink, turn left/right, smile — detected **on-device** via Google ML Kit face detection, and captures a selfie once the challenge steps pass. All processing stays on the device; no images or biometric data are sent anywhere by this package. -`Spoofing` attempts using printed photos, recordings, deep fake pictures, and 3D masks poses a serious threat. `Facial Liveness Detection` incorporates specialized features to identify biometric spoofing attacks, which could be an imitation emulating a person’s unique biometrics scanned through the biometric detector to deceive or bypass the identification and authentication steps provided by the system. Even though face recognition can reliably answer the question, “Is this the right person?” but not the question, “Is this a live person?” This is where liveness detection technology plays a significant role in fraud detection and mitigation. Face biometric matching must be able to detect spoofs in order to be trusted, and to maintain the integrity of biometric data. +> **⚠️ This is active challenge-response, NOT certified anti-spoofing.** The package does **not** implement [ISO/IEC 30107](https://www.iso.org/standard/79520.html) presentation-attack detection (PAD). It performs no texture, depth, motion, or reflection analysis, so a **printed photo, screen replay, recorded video, or 3D mask can defeat the gesture challenge.** Do **not** use it as the sole control for KYC, identity verification, payments, or any security-critical authentication. Pair it with a dedicated PAD/anti-spoofing solution and your own server-side risk checks, and obtain the user's consent before capture (see [Privacy & compliance](#privacy)). @@ -80,10 +81,10 @@ Next comes the native setup on both android and iOS ```xml NSCameraUsageDescription Camera Access for Scanning - NSMicrophoneUsageDescription - Microphone for playing instructions audio. ``` +> Only the camera permission is required — this package neither records nor plays audio (`enableAudio: false`), so do **not** add `NSMicrophoneUsageDescription`. Declaring an unused microphone purpose string can cause App Store review rejection (Guideline 5.1.1). + 4. Open the `ios/Runner/Podfile` and uncomment the second line. ```yaml @@ -143,6 +144,25 @@ A call to a single line function will return a temporary path to the captured im + + +## Privacy & compliance + +This package captures **biometric data** (a face selfie and derived facial landmarks) — a special/sensitive data category under most privacy laws. Read this before shipping to production. + +**What this package does** + +- **On-device only.** Face detection runs locally via Google ML Kit; the package makes **no network calls** and transmits no images or biometric data. +- Uses [Google ML Kit Face Detection](https://developers.google.com/ml-kit/vision/face-detection) — your app must comply with Google's ML Kit terms and disclose its use. +- The captured selfie is written **unencrypted** to the OS temporary directory and returned as a file path. **The package never deletes it** — the file persists until you remove it or the OS evicts the cache. + +**What you (the consuming app) are responsible for** + +- **Obtain consent before capture.** This package does not collect consent and its info screen is *instructional only* — **not** lawful-basis consent. You must establish a lawful basis: GDPR Art. 9(2)(a) *explicit* consent, Illinois BIPA §15(b) informed *written* release, China PIPL *separate* consent, India DPDP notice+consent, etc. +- **Delete the captured image** as soon as it is no longer needed, and define a retention/destruction schedule (BIPA §15(a); GDPR storage limitation, Art. 5(1)(e)). +- **Do not treat this as anti-spoofing/PAD.** A capture where `didCaptureAutomatically == false` (the manual button shown after a timeout) has passed **no** liveness check — do not accept it as verified. +- If you deploy in the EU for identity purposes, review your EU AI Act obligations (transparency, robustness, human oversight). + ## Contributors From 5adbec3b6b0df837b26a2b3b7009c552450349ba Mon Sep 17 00:00:00 2001 From: Sagar Ghag Date: Thu, 16 Jul 2026 21:16:06 +0530 Subject: [PATCH 2/3] fix(liveness): require a real right-turn; remove dead anti-spoof stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - V1 turnRight used `>` so a forward-facing head (yaw ~0) auto-passed instantly; now requires yaw more negative than the threshold magnitude (mirrors V2). Root-cause fix, robust whether or not a HeadTurnDetectionThreshold is configured. - delete golden-ratio (V1) and facial-symmetry (V2) computations + calculateSymmetry: they were only print()ed, never gated pass/fail — keeping them implied anti-spoofing the package does not have. - guard the V2 image-error debugPrint behind kDebugMode (the one log that reached release). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../m7_livelyness_detection_screen.dart | 29 ++------ .../m7_livelyness_detection_screen_v2.dart | 67 +------------------ 2 files changed, 8 insertions(+), 88 deletions(-) diff --git a/lib/src/screens/m7_livelyness_detection_screen.dart b/lib/src/screens/m7_livelyness_detection_screen.dart index 9ea188f..50943a7 100644 --- a/lib/src/screens/m7_livelyness_detection_screen.dart +++ b/lib/src/screens/m7_livelyness_detection_screen.dart @@ -364,28 +364,6 @@ class _MLivelyness7DetectionScreenState if (_isProcessingStep) { return; } - final faceWidth = face.boundingBox.width; - final Point? leftEyePosition = face - .getContour( - FaceContourType.leftEye, - ) - ?.points - .elementAt(8); - final Point? rightEyePosition = face - .getContour( - FaceContourType.rightEye, - ) - ?.points - .elementAt(0); - if (leftEyePosition != null && rightEyePosition != null) { - final goldenRatio = (faceWidth / - leftEyePosition.distanceTo( - rightEyePosition, - )); - if (kDebugMode) { - print("Golden Ratio: $goldenRatio"); - } - } switch (step) { case LivelynessStep.blink: final BlinkDetectionThreshold? blinkThreshold = @@ -420,8 +398,11 @@ class _MLivelyness7DetectionScreenState LivelynessDetection.instance.thresholdConfig.firstWhereOrNull( (p0) => p0 is HeadTurnDetectionThreshold, ) as HeadTurnDetectionThreshold?; - if ((face.headEulerAngleY ?? 0) > - (headTurnThreshold?.rotationAngle ?? -50)) { + // turnRight: yaw is negative when the head turns right, so require the + // angle to be MORE negative than the (magnitude of the) threshold. + // Using `>` here made a forward-facing head (yaw ~0) pass instantly. + if ((face.headEulerAngleY ?? 0) < + -((headTurnThreshold?.rotationAngle ?? 50).abs())) { _startProcessing(); await _completeStep(step: step); } diff --git a/lib/src/screens/m7_livelyness_detection_screen_v2.dart b/lib/src/screens/m7_livelyness_detection_screen_v2.dart index 6b0d772..1922b04 100644 --- a/lib/src/screens/m7_livelyness_detection_screen_v2.dart +++ b/lib/src/screens/m7_livelyness_detection_screen_v2.dart @@ -136,7 +136,9 @@ class _LivelynessDetectionScreenAndroidState () => _isProcessing = false, ); } - debugPrint("...sending image resulted error $error"); + if (kDebugMode) { + debugPrint("...sending image resulted error $error"); + } } } @@ -147,56 +149,6 @@ class _LivelynessDetectionScreenAndroidState return; } final Face firstFace = faces.first; - final landmarks = firstFace.landmarks; - // Get landmark positions for relevant facial features - final Point? leftEye = landmarks[FaceLandmarkType.leftEye]?.position; - final Point? rightEye = - landmarks[FaceLandmarkType.rightEye]?.position; - final Point? leftCheek = - landmarks[FaceLandmarkType.leftCheek]?.position; - final Point? rightCheek = - landmarks[FaceLandmarkType.rightCheek]?.position; - final Point? leftEar = landmarks[FaceLandmarkType.leftEar]?.position; - final Point? rightEar = - landmarks[FaceLandmarkType.rightEar]?.position; - final Point? leftMouth = - landmarks[FaceLandmarkType.leftMouth]?.position; - final Point? rightMouth = - landmarks[FaceLandmarkType.rightMouth]?.position; - - // Calculate symmetry values based on corresponding landmark positions - final Map symmetry = {}; - final eyeSymmetry = calculateSymmetry( - leftEye, - rightEye, - ); - symmetry['eyeSymmetry'] = eyeSymmetry; - - final cheekSymmetry = calculateSymmetry( - leftCheek, - rightCheek, - ); - symmetry['cheekSymmetry'] = cheekSymmetry; - - final earSymmetry = calculateSymmetry( - leftEar, - rightEar, - ); - symmetry['earSymmetry'] = earSymmetry; - - final mouthSymmetry = calculateSymmetry( - leftMouth, - rightMouth, - ); - symmetry['mouthSymmetry'] = mouthSymmetry; - double total = 0.0; - symmetry.forEach((key, value) { - total += value; - }); - final double average = total / symmetry.length; - if (kDebugMode) { - print("Face Symmetry: $average"); - } if (_isProcessingStep && _steps[_stepsKey.currentState?.currentIndex ?? 0].step == LivelynessStep.blink) { @@ -521,17 +473,4 @@ class _LivelynessDetectionScreenAndroidState ], ); } - - double calculateSymmetry( - Point? leftPosition, Point? rightPosition) { - if (leftPosition != null && rightPosition != null) { - final double dx = (rightPosition.x - leftPosition.x).abs().toDouble(); - final double dy = (rightPosition.y - leftPosition.y).abs().toDouble(); - final distance = Offset(dx, dy).distance; - - return distance; - } - - return 0.0; - } } From e2e34faab720bd56c994f17faa4c436847539e93 Mon Sep 17 00:00:00 2001 From: Sagar Ghag Date: Thu, 16 Jul 2026 21:16:06 +0530 Subject: [PATCH 3/3] chore(deps): drop unused image + plugin_platform_interface; delete dead plugin scaffold - `image` (^4.0.15) declared but never imported (pulled archive/xml/petitparser/crypto) - `plugin_platform_interface` + livelyness_detection_{method_channel,platform_interface}.dart are untouched flutter-create boilerplate (getPlatformVersion), never registered natively - shrinks the supply-chain / attack surface of a biometric package Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/index.dart | 2 -- lib/livelyness_detection_method_channel.dart | 18 ----------- ...velyness_detection_platform_interface.dart | 30 ------------------- pubspec.yaml | 2 -- 4 files changed, 52 deletions(-) delete mode 100644 lib/livelyness_detection_method_channel.dart delete mode 100644 lib/livelyness_detection_platform_interface.dart diff --git a/lib/index.dart b/lib/index.dart index 260f1c6..7a1de8b 100644 --- a/lib/index.dart +++ b/lib/index.dart @@ -16,5 +16,3 @@ export 'package:rxdart/rxdart.dart'; export 'package:uuid/uuid.dart'; export './src/index.dart'; -export 'livelyness_detection_method_channel.dart'; -export 'livelyness_detection_platform_interface.dart'; diff --git a/lib/livelyness_detection_method_channel.dart b/lib/livelyness_detection_method_channel.dart deleted file mode 100644 index 128302d..0000000 --- a/lib/livelyness_detection_method_channel.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'livelyness_detection_platform_interface.dart'; - -/// An implementation of [LivelynessDetectionPlatform] that uses method channels. -class MethodChannelLivelynessDetection extends LivelynessDetectionPlatform { - /// The method channel used to interact with the native platform. - @visibleForTesting - final methodChannel = const MethodChannel('livelyness_detection'); - - @override - Future getPlatformVersion() async { - final version = - await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } -} diff --git a/lib/livelyness_detection_platform_interface.dart b/lib/livelyness_detection_platform_interface.dart deleted file mode 100644 index 9ecf29d..0000000 --- a/lib/livelyness_detection_platform_interface.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -import 'livelyness_detection_method_channel.dart'; - -abstract class LivelynessDetectionPlatform extends PlatformInterface { - /// Constructs a LivelynessDetectionPlatform. - LivelynessDetectionPlatform() : super(token: _token); - - static final Object _token = Object(); - - static LivelynessDetectionPlatform _instance = - MethodChannelLivelynessDetection(); - - /// The default instance of [LivelynessDetectionPlatform] to use. - /// - /// Defaults to [MethodChannelLivelynessDetection]. - static LivelynessDetectionPlatform get instance => _instance; - - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [LivelynessDetectionPlatform] when - /// they register themselves. - static set instance(LivelynessDetectionPlatform instance) { - PlatformInterface.verifyToken(instance, _token); - _instance = instance; - } - - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 4221497..b41adf9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,10 +18,8 @@ dependencies: flutter: sdk: flutter google_mlkit_face_detection: ^0.11.0 - image: ^4.0.15 lottie: ^3.1.2 path_provider: ^2.0.14 - plugin_platform_interface: ^2.1.3 rxdart: ^0.28.0 uuid: ^4.4.2