From 34cbeeae1b33de1aa056e92f98952786332a2c30 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Tue, 25 Aug 2020 19:02:15 -0700 Subject: [PATCH] feat(android): add tapjacking prevention features - Added boolean property "filterTouchesWhenObscured" to all Ti.UI.View dervied types. - Added event "touchfiltered" event to Ti.UI.Button. - Added boolean property "obscured" to all touch related events. * Will only be true if "filterTouchesWhenObscured" is false. Fixes TIMOB-28080 --- .../titanium/ui/widget/TiUIButton.java | 11 ++ .../java/org/appcelerator/titanium/TiC.java | 15 +++ .../titanium/proxy/TiViewProxy.java | 2 + .../appcelerator/titanium/view/TiUIView.java | 51 ++++++++ apidoc/Titanium/UI/Button.yml | 30 +++++ apidoc/Titanium/UI/View.yml | 122 +++++++++++++++++- tests/Resources/ti.ui.view.test.js | 13 ++ 7 files changed, 243 insertions(+), 1 deletion(-) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButton.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButton.java index a79bc05874d..2be4ac163f1 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButton.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIButton.java @@ -27,6 +27,7 @@ import android.graphics.drawable.Drawable; import android.text.TextUtils; import android.view.Gravity; +import android.view.MotionEvent; import androidx.appcompat.widget.AppCompatButton; public class TiUIButton extends TiUIView @@ -45,6 +46,16 @@ public TiUIButton(final TiViewProxy proxy) super(proxy); Log.d(TAG, "Creating a button", Log.DEBUG_MODE); AppCompatButton btn = new AppCompatButton(proxy.getActivity()) { + @Override + public boolean onFilterTouchEventForSecurity(MotionEvent event) + { + boolean isTouchAllowed = super.onFilterTouchEventForSecurity(event); + if (!isTouchAllowed) { + fireEvent(TiC.EVENT_TOUCH_FILTERED, dictFromEvent(event)); + } + return isTouchAllowed; + } + @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiC.java b/android/titanium/src/java/org/appcelerator/titanium/TiC.java index f471536dca4..afba01efad2 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiC.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiC.java @@ -370,6 +370,11 @@ public class TiC */ public static final String EVENT_PROPERTY_MESSAGE = "message"; + /** + * @module.api + */ + public static final String EVENT_PROPERTY_OBSCURED = "obscured"; + /** * @module.api */ @@ -684,6 +689,11 @@ public class TiC */ public static final String EVENT_TOUCH_END = "touchend"; + /** + * @module.api + */ + public static final String EVENT_TOUCH_FILTERED = "touchfiltered"; + /** * @module.api */ @@ -1687,6 +1697,11 @@ public class TiC */ public static final String PROPERTY_FILTER_CASE_INSENSITIVE = "filterCaseInsensitive"; + /** + * @module.api + */ + public static final String PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED = "filterTouchesWhenObscured"; + /** * @module.api */ diff --git a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java index a9351a0480b..76fa4bcb2ac 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java +++ b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiViewProxy.java @@ -79,6 +79,7 @@ // others TiC.PROPERTY_FOCUSABLE, TiC.PROPERTY_TOUCH_ENABLED, + TiC.PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED, TiC.PROPERTY_VISIBLE, TiC.PROPERTY_ENABLED, TiC.PROPERTY_OPACITY, @@ -126,6 +127,7 @@ public TiViewProxy() pendingAnimationLock = new Object(); defaultValues.put(TiC.PROPERTY_TOUCH_ENABLED, true); + defaultValues.put(TiC.PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED, false); defaultValues.put(TiC.PROPERTY_SOUND_EFFECTS_ENABLED, true); defaultValues.put(TiC.PROPERTY_BACKGROUND_REPEAT, false); defaultValues.put(TiC.PROPERTY_VISIBLE, true); diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java index d6614bccba1..5a76f0f3207 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiUIView.java @@ -758,6 +758,8 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP } else if (key.equals(TiC.PROPERTY_TOUCH_ENABLED)) { nativeView.setEnabled(TiConvert.toBoolean(newValue)); doSetClickable(TiConvert.toBoolean(newValue)); + } else if (key.equals(TiC.PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED)) { + setFilterTouchesWhenObscured(TiConvert.toBoolean(newValue, false)); } else if (key.equals(TiC.PROPERTY_VISIBLE)) { newValue = (newValue == null) ? false : newValue; this.setVisibility(TiConvert.toBoolean(newValue) ? View.VISIBLE : View.INVISIBLE); @@ -987,6 +989,11 @@ public void processProperties(KrollDict d) applyTouchFeedback((colorString != null) ? TiConvert.toColor(colorString) : null); } + if (d.containsKey(TiC.PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED) && !nativeViewNull) { + setFilterTouchesWhenObscured( + TiConvert.toBoolean(d.get(TiC.PROPERTY_FILTER_TOUCHES_WHEN_OBSCURED), false)); + } + if (d.containsKey(TiC.PROPERTY_HIDDEN_BEHAVIOR) && !nativeViewNull) { Object hidden = d.get(TiC.PROPERTY_HIDDEN_BEHAVIOR); if (hidden != null) { @@ -1516,6 +1523,43 @@ private void handleBorderProperty(String property, Object value) motionEvents.put(MotionEvent.ACTION_CANCEL, TiC.EVENT_TOUCH_CANCEL); } + private void setFilterTouchesWhenObscured(boolean isEnabled) + { + // Validate. + if (this.nativeView == null) { + return; + } + + // Enable/disable tapjacking filter. + this.nativeView.setFilterTouchesWhenObscured(isEnabled); + + // Android 4.4.2 and older has a bug where the above method sets it to the opposite. + // Google fixed it in Android 4.4.3, but we can't detect that patch version via API Level. + if ((Build.VERSION.SDK_INT < 21) && (isEnabled != this.nativeView.getFilterTouchesWhenObscured())) { + this.nativeView.setFilterTouchesWhenObscured(!isEnabled); + } + } + + /** + * Determines if touch event was obscurred by an overlapping translucent window belonging to another app. + * This is used for security purposes to detect "tapjacking". + * @param event The touch event to be analyzed. Can be null. + * @return Returns true if touch event was obscurred. Returns false if not or if given a null argument. + */ + private boolean wasObscured(MotionEvent event) + { + if (event != null) { + int flags = event.getFlags(); + if ((flags & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) { + return true; + } + if ((Build.VERSION.SDK_INT >= 29) && ((flags & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) { + return true; + } + } + return false; + } + protected KrollDict dictFromEvent(MotionEvent e) { TiDimension xDimension = new TiDimension((double) e.getX(), TiDimension.TYPE_LEFT); @@ -1525,6 +1569,7 @@ protected KrollDict dictFromEvent(MotionEvent e) data.put(TiC.EVENT_PROPERTY_X, xDimension.getAsDefault(this.nativeView)); data.put(TiC.EVENT_PROPERTY_Y, yDimension.getAsDefault(this.nativeView)); data.put(TiC.EVENT_PROPERTY_FORCE, (double) e.getPressure()); + data.put(TiC.EVENT_PROPERTY_OBSCURED, wasObscured(e)); data.put(TiC.EVENT_PROPERTY_SIZE, (double) e.getSize()); data.put(TiC.EVENT_PROPERTY_SOURCE, proxy); return data; @@ -1548,6 +1593,11 @@ protected KrollDict dictFromEvent(KrollDict dictToCopy) } else { data.put(TiC.EVENT_PROPERTY_FORCE, (double) 0); } + if (dictToCopy.containsKey(TiC.EVENT_PROPERTY_OBSCURED)) { + data.put(TiC.EVENT_PROPERTY_OBSCURED, dictToCopy.get(TiC.EVENT_PROPERTY_OBSCURED)); + } else { + data.put(TiC.EVENT_PROPERTY_OBSCURED, false); + } if (dictToCopy.containsKey(TiC.EVENT_PROPERTY_SIZE)) { data.put(TiC.EVENT_PROPERTY_SIZE, dictToCopy.get(TiC.EVENT_PROPERTY_SIZE)); } else { @@ -1708,6 +1758,7 @@ public boolean onTouch(View view, MotionEvent event) TiDimension yDimension = new TiDimension((double) event.getY(), TiDimension.TYPE_TOP); lastUpEvent.put(TiC.EVENT_PROPERTY_X, xDimension.getAsDefault(view)); lastUpEvent.put(TiC.EVENT_PROPERTY_Y, yDimension.getAsDefault(view)); + lastUpEvent.put(TiC.EVENT_PROPERTY_OBSCURED, wasObscured(event)); } if (proxy != null && proxy.hierarchyHasListener(TiC.EVENT_PINCH)) { diff --git a/apidoc/Titanium/UI/Button.yml b/apidoc/Titanium/UI/Button.yml index 548aee4a54b..e447dda7f62 100644 --- a/apidoc/Titanium/UI/Button.yml +++ b/apidoc/Titanium/UI/Button.yml @@ -96,6 +96,36 @@ excludes: properties: [children] methods: [add, remove, removeAllChildren, replaceAt] since: "0.8" + +events: + - name: touchfiltered + summary: Fired when the device detects a swipe gesture against the view. + description: | + If the button's [filterTouchesWhenObscured](Titanium.UI.View.filterTouchesWhenObscured) property + is set `true`, then this event will be fired if the touch event was discarded because another app's + overlapping window obscured it. + + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + + You can use this event to display an alert dialog explaining why the button's action has been disabled. + Especially if the overlapping window is completely invisible. + platforms: [android] + since: "9.3.0" + properties: + - name: x + summary: X coordinate of the event from the `source` view's coordinate system. + type: Number + + - name: y + summary: Y coordinate of the event from the `source` view's coordinate system. + type: Number + + - name: obscured + type: Boolean + summary: Always `true` since the touch event passed through another app's overlapping window. + properties: - name: attributedString summary: Specify an attributed string for the label. diff --git a/apidoc/Titanium/UI/View.yml b/apidoc/Titanium/UI/View.yml index b15023da326..bb9c83be89a 100644 --- a/apidoc/Titanium/UI/View.yml +++ b/apidoc/Titanium/UI/View.yml @@ -238,6 +238,16 @@ events: summary: Y coordinate of the event from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the click passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: dblclick summary: Fired when the device detects a double click against the view. properties: @@ -247,7 +257,15 @@ events: - name: y summary: Y coordinate of the event from the `source` view's coordinate system. type: Number - + - name: obscured + type: Boolean + summary: Returns `true` if the double click passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" - name: doubletap summary: Fired when the device detects a double tap against the view. @@ -259,6 +277,15 @@ events: - name: y summary: Y coordinate of the event from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the double tap passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" - name: focus summary: Fired when the view element gains focus. @@ -317,6 +344,16 @@ events: summary: Y coordinate of the event from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the long press passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: pinch summary: Fired when the device detects a pinch gesture. description: | @@ -441,6 +478,16 @@ events: summary: Y coordinate of the event from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the single tap passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: swipe summary: Fired when the device detects a swipe gesture against the view. platforms: [android, iphone, ipad, macos] @@ -457,6 +504,16 @@ events: summary: Y coordinate of the event's endpoint from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the swipe passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: touchcancel summary: Fired when a touch event is interrupted by the device. description: | @@ -518,6 +575,16 @@ events: type: Number platforms: [iphone, ipad, macos] + - name: obscured + type: Boolean + summary: Returns `true` if the touch passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: touchend summary: Fired when a touch event is completed. description: | @@ -579,6 +646,16 @@ events: type: Number platforms: [iphone, ipad, macos] + - name: obscured + type: Boolean + summary: Returns `true` if the touch passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: touchmove summary: Fired as soon as the device detects movement of a touch. description: | @@ -639,6 +716,16 @@ events: type: Number platforms: [iphone, ipad, macos] + - name: obscured + type: Boolean + summary: Returns `true` if the touch passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: touchstart summary: Fired as soon as the device detects a touch gesture. properties: @@ -697,6 +784,16 @@ events: type: Number platforms: [iphone, ipad, macos] + - name: obscured + type: Boolean + summary: Returns `true` if the touch passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" + - name: twofingertap summary: Fired when the device detects a two-finger tap against the view. since: { android: "3.0.0" } @@ -709,6 +806,15 @@ events: summary: Y coordinate of the event from the `source` view's coordinate system. type: Number + - name: obscured + type: Boolean + summary: Returns `true` if the tap passed through an overlapping window belonging to another app. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + platforms: [android] + since: "9.3.0" methods: - name: add @@ -1409,6 +1515,20 @@ properties: since: 5.0.0 osver: {android: {min: 5.0}} + - name: filterTouchesWhenObscured + summary: Discards touch related events if another app's system overlay covers the view. + description: | + This is a security feature to protect an app from "tapjacking", where a malicious app can use a + system overlay to intercept touch events in your app or to trick the end-user to tap on UI + in your app intended for the overlay. + + Setting this property to `true` causes touch related events (including "click") to not be fired + if a system overlay overlaps the view. + type: Boolean + default: false + platforms: [android] + since: "9.3.0" + - name: focusable summary: Whether view should be focusable while navigating with the trackball. type: Boolean diff --git a/tests/Resources/ti.ui.view.test.js b/tests/Resources/ti.ui.view.test.js index 323049e9ef7..fd517db07b1 100644 --- a/tests/Resources/ti.ui.view.test.js +++ b/tests/Resources/ti.ui.view.test.js @@ -1315,4 +1315,17 @@ describe('Titanium.UI.View', function () { win.addEventListener('open', () => finish()); win.open(); }); + + it.android('filterTouchesWhenObscured', finish => { + const view1 = Ti.UI.createView(); + should(view1.filterTouchesWhenObscured).be.false(); + const view2 = Ti.UI.createView({ filterTouchesWhenObscured: true }); + should(view2.filterTouchesWhenObscured).be.true(); + + win = Ti.UI.createWindow(); + win.add(view1); + win.add(view2); + win.addEventListener('open', () => finish()); + win.open(); + }); });