Skip to content

Commit

Permalink
Email Protection for Windows + Windows compatibility patches (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
greyivy authored May 25, 2023
1 parent ed6b315 commit 084ba7b
Show file tree
Hide file tree
Showing 21 changed files with 2,535 additions and 190 deletions.
527 changes: 489 additions & 38 deletions dist/autofill-debug.js

Large diffs are not rendered by default.

454 changes: 416 additions & 38 deletions dist/autofill.js

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions integration-test/helpers/mocks.windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {createAvailableInputTypes, withDataType} from './utils.js'
* @typedef {import("../../src/deviceApiCalls/__generated__/validators-ts").AutofillFeatureToggles} AutofillFeatureToggles
* @typedef {import("../../src/deviceApiCalls/__generated__/validators-ts").AvailableInputTypes} AvailableInputTypes
* @typedef {import("../../src/deviceApiCalls/__generated__/validators-ts").GetAutofillDataResponse} GetAutofillDataResponse
* @typedef {import("../../src/deviceApiCalls/__generated__/validators-ts").EmailProtectionGetIsLoggedInResult} EmailProtectionGetIsLoggedInResult
*
* Use this to mock windows message handlers
*
Expand Down Expand Up @@ -68,15 +69,18 @@ export function createWindowsMocks () {
/** @type {CredentialsObject | null} */
getAutofillCredentials: null,
/** @type {null | GetAutofillDataResponse['success']} */
getAutofillData: null

getAutofillData: null,
/** @type {null | EmailProtectionGetIsLoggedInResult['success']} */
emailProtectionGetIsLoggedIn: null
}
/** @type {MockBuilder} */
const builder = {
withPrivateEmail (_email) {
mocks.emailProtectionGetIsLoggedIn = true
return this
},
withPersonalEmail (_email) {
mocks.emailProtectionGetIsLoggedIn = true
return this
},
withEmailProtection (emails) {
Expand Down Expand Up @@ -188,6 +192,10 @@ export function createWindowsMocks () {
},
selectedDetail (request) {
recordCall(request.Name, request.Data, null)
},
emailProtectionGetIsLoggedIn (request) {
recordCall(request.Name, null, mocks.emailProtectionGetIsLoggedIn)
return respond(request.Name, null, mocks.emailProtectionGetIsLoggedIn)
}
}

Expand Down
4 changes: 4 additions & 0 deletions integration-test/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,18 @@ async function addTopAutofillMouseFocus (page, button) {
const coords = await button.boundingBox({timeout: 1000})
const x = coords?.x || 10
const y = coords?.y || 10

await page.evaluate(({x, y}) => {
const event = new CustomEvent('mouseMove', {detail: {x: x + 30, y: y + 10}})
window.dispatchEvent(event)
}, {x, y})
await page.mouse.move(x + 30, y + 10)

await page.evaluate(({x, y}) => {
const moved = new CustomEvent('mouseMove', {detail: {x: x + 50, y: y + 15}})
window.dispatchEvent(moved)
}, {x, y})
await page.mouse.move(x + 50, y + 15)
}

export {createAvailableInputTypes, stripDuckExtension, clickOnIcon, withDataType, addTopAutofillMouseFocus}
33 changes: 33 additions & 0 deletions src/DeviceInterface/AppleOverlayDeviceInterface.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class AppleOverlayDeviceInterface extends AppleDeviceInterface {
*/
overlay = overlayApi(this)

previousX = 0
previousY = 0

/**
* Because we're running inside the Overlay, we always create the HTML
* Tooltip controller.
Expand All @@ -44,6 +47,36 @@ class AppleOverlayDeviceInterface extends AppleDeviceInterface {
})
}

addDeviceListeners () {
/**
* The native side will send a custom event 'mouseMove' to indicate
* that the HTMLTooltip should fake an element being focused.
*
* Note: There's no cleanup required here since the Overlay has a fresh
* page load every time it's opened.
*/
window.addEventListener('mouseMove', (event) => {
// Don't set focus if the mouse hasn't moved ever
// This is to avoid clickjacking where an attacker puts the pulldown under the cursor
// and tricks the user into clicking
if (
(!this.previousX && !this.previousY) || // if no previous coords
(this.previousX === event.detail.x && this.previousY === event.detail.y) // or the mouse hasn't moved
) {
this.previousX = event.detail.x
this.previousY = event.detail.y
return
}

const activeTooltip = this.uiController?.getActiveTooltip?.()
activeTooltip?.focus(event.detail.x, event.detail.y)
this.previousX = event.detail.x
this.previousY = event.detail.y
})

return super.addDeviceListeners()
}

/**
* Since we're running inside the Overlay we can limit what happens here to
* be only things that are needed to power the HTML Tooltip
Expand Down
83 changes: 81 additions & 2 deletions src/DeviceInterface/WindowsInterface.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import InterfacePrototype from './InterfacePrototype.js'
import {OverlayUIController} from '../UI/controllers/OverlayUIController.js'
import {CloseAutofillParentCall, GetAutofillDataCall} from '../deviceApiCalls/__generated__/deviceApiCalls.js'
import { OverlayUIController } from '../UI/controllers/OverlayUIController.js'
import {
CloseAutofillParentCall,
GetAutofillDataCall,
EmailProtectionStoreUserDataCall,
EmailProtectionRemoveUserDataCall,
EmailProtectionGetUserDataCall,
EmailProtectionGetCapabilitiesCall,
EmailProtectionRefreshPrivateAddressCall,
EmailProtectionGetAddressesCall,
EmailProtectionGetIsLoggedInCall
} from '../deviceApiCalls/__generated__/deviceApiCalls.js'

/**
* @typedef {import('../deviceApiCalls/__generated__/validators-ts').GetAutofillDataRequest} GetAutofillDataRequest
*/

const EMAIL_PROTECTION_LOGOUT_MESSAGE = 'EMAIL_PROTECTION_LOGOUT'

export class WindowsInterface extends InterfacePrototype {
ready = false;
/** @type {AbortController|null} */
Expand All @@ -18,6 +30,13 @@ export class WindowsInterface extends InterfacePrototype {
return true
}

async setupAutofill () {
const loggedIn = await this._getIsLoggedIn()
if (loggedIn) {
await this.getAddresses()
}
}

isEnabledViaSettings () {
return Boolean(this.settings.enabled)
}
Expand Down Expand Up @@ -94,4 +113,64 @@ export class WindowsInterface extends InterfacePrototype {
async _closeAutofillParent () {
return this.deviceApi.notify(new CloseAutofillParentCall(null))
}

/**
* Email Protection calls
*/

/**
* @returns {Promise<any>}
*/
getEmailProtectionCapabilities () {
return this.deviceApi.request(new EmailProtectionGetCapabilitiesCall({}))
}

async _getIsLoggedIn () {
const isLoggedIn = await this.deviceApi.request(new EmailProtectionGetIsLoggedInCall({}))

this.isDeviceSignedIn = () => isLoggedIn
return isLoggedIn
}

addLogoutListener (handler) {
// Only deal with logging out if we're in the email web app
if (!this.globalConfig.isDDGDomain) return

windowsInteropAddEventListener('message', (e) => {
if (this.globalConfig.isDDGDomain && e.data === EMAIL_PROTECTION_LOGOUT_MESSAGE) {
handler()
}
})
}

/**
* @returns {Promise<any>}
*/
storeUserData ({ addUserData }) {
return this.deviceApi.request(new EmailProtectionStoreUserDataCall(addUserData))
}
/**
* @returns {Promise<any>}
*/
removeUserData () {
return this.deviceApi.request(new EmailProtectionRemoveUserDataCall({}))
}
/**
* @returns {Promise<any>}
*/
getUserData () {
return this.deviceApi.request(new EmailProtectionGetUserDataCall({}))
}

async refreshAlias () {
const addresses = await this.deviceApi.request(new EmailProtectionRefreshPrivateAddressCall({}))

this.storeLocalAddresses(addresses)
}
async getAddresses () {
const addresses = await this.deviceApi.request(new EmailProtectionGetAddressesCall({}))

this.storeLocalAddresses(addresses)
return addresses
}
}
105 changes: 102 additions & 3 deletions src/DeviceInterface/WindowsOverlayDeviceInterface.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import InterfacePrototype from './InterfacePrototype.js'
import {HTMLTooltipUIController} from '../UI/controllers/HTMLTooltipUIController.js'
import { HTMLTooltipUIController } from '../UI/controllers/HTMLTooltipUIController.js'
import {
EmailProtectionGetAddressesCall,
GetAutofillInitDataCall,
SetSizeCall
EmailProtectionGetIsLoggedInCall,
SetSizeCall,
OpenManagePasswordsCall,
OpenManageCreditCardsCall,
OpenManageIdentitiesCall,
CloseAutofillParentCall
} from '../deviceApiCalls/__generated__/deviceApiCalls.js'
import {overlayApi} from './overlayApi.js'
import { overlayApi } from './overlayApi.js'

/**
* This subclass is designed to separate code that *only* runs inside the
Expand All @@ -25,6 +31,9 @@ export class WindowsOverlayDeviceInterface extends InterfacePrototype {
*/
overlay = overlayApi(this);

previousScreenX = 0;
previousScreenY = 0;

/**
* Because we're running inside the Overlay, we always create the HTML
* Tooltip controller.
Expand All @@ -40,6 +49,7 @@ export class WindowsOverlayDeviceInterface extends InterfacePrototype {
wrapperClass: 'top-autofill',
tooltipPositionClass: () => '.wrapper { transform: none; }',
setSize: (details) => this.deviceApi.notify(new SetSizeCall(details)),
remove: async () => this._closeAutofillParent(),
testMode: this.isTestMode(),
/**
* Note: This is needed because Mutation observer didn't support visibility checks on Windows
Expand All @@ -48,6 +58,62 @@ export class WindowsOverlayDeviceInterface extends InterfacePrototype {
})
}

addDeviceListeners () {
/**
* On Windows (vs. MacOS) we can use the built-in `mousemove`
* event and screen-relative positioning.
*
* Note: There's no cleanup required here since the Overlay has a fresh
* page load every time it's opened.
*/
window.addEventListener('mousemove', (event) => {
// Don't set focus if the mouse hasn't moved ever
// This is to avoid clickjacking where an attacker puts the pulldown under the cursor
// and tricks the user into clicking
if (
(!this.previousScreenX && !this.previousScreenY) || // if no previous coords
(this.previousScreenX === event.screenX && this.previousScreenY === event.screenY) // or the mouse hasn't moved
) {
this.previousScreenX = event.screenX
this.previousScreenY = event.screenY
return
}

const activeTooltip = this.uiController?.getActiveTooltip?.()
activeTooltip?.focus(event.x, event.y)
this.previousScreenX = event.screenX
this.previousScreenY = event.screenY
})

return super.addDeviceListeners()
}

/**
* @returns {Promise<any>}
*/
async _closeAutofillParent () {
return this.deviceApi.notify(new CloseAutofillParentCall(null))
}

/**
* @returns {Promise<any>}
*/
openManagePasswords () {
return this.deviceApi.notify(new OpenManagePasswordsCall({}))
}
/**
* @returns {Promise<any>}
*/
openManageCreditCards () {
return this.deviceApi.notify(new OpenManageCreditCardsCall({}))
}
/**
* @returns {Promise<any>}
*/
openManageIdentities () {
return this.deviceApi.notify(new OpenManageIdentitiesCall({}))
}

/**
* Since we're running inside the Overlay we can limit what happens here to
* be only things that are needed to power the HTML Tooltip
Expand All @@ -56,6 +122,11 @@ export class WindowsOverlayDeviceInterface extends InterfacePrototype {
* @returns {Promise<void>}
*/
async setupAutofill () {
const loggedIn = await this._getIsLoggedIn()
if (loggedIn) {
await this.getAddresses()
}

const response = await this.deviceApi.request(new GetAutofillInitDataCall(null))
// @ts-ignore
this.storeLocalData(response)
Expand All @@ -79,4 +150,32 @@ export class WindowsOverlayDeviceInterface extends InterfacePrototype {
async selectedDetail (data, type) {
return this.overlay.selectedDetail(data, type)
}

/**
* Email Protection calls
*/

async _getIsLoggedIn () {
const isLoggedIn = await this.deviceApi.request(new EmailProtectionGetIsLoggedInCall({}))

this.isDeviceSignedIn = () => isLoggedIn
return isLoggedIn
}

async getAddresses () {
const addresses = await this.deviceApi.request(new EmailProtectionGetAddressesCall({}))

this.storeLocalAddresses(addresses)
return addresses
}

/**
* Gets a single identity obj once the user requests it
* @param {Number} id
* @returns {Promise<{success: IdentityObject|undefined}>}
*/
getAutofillIdentity (id) {
const identity = this.getLocalIdentities().find(({ id: identityId }) => `${identityId}` === `${id}`)
return Promise.resolve({ success: identity })
}
}
29 changes: 0 additions & 29 deletions src/DeviceInterface/overlayApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,6 @@ import {SelectedDetailCall} from '../deviceApiCalls/__generated__/deviceApiCalls
* @param {import("./InterfacePrototype").default} device
*/
export function overlayApi (device) {
let previousX
let previousY

/**
* The native side will send a custom event 'mouseMove' to indicate
* that the HTMLTooltip should fake an element being focused.
*
* Note: There's no cleanup required here since the Overlay has a fresh
* page load every time it's opened.
*/
window.addEventListener('mouseMove', (event) => {
// Don't set focus if the mouse hasn't moved ever
// This is to avoid clickjacking where an attacker puts the pulldown under the cursor
// and tricks the user into clicking
if (
(!previousX && !previousY) || // if no previous coords
(previousX === event.detail.x && previousY === event.detail.y) // or the mouse hasn't moved
) {
previousX = event.detail.x
previousY = event.detail.y
return
}

const activeTooltip = device.uiController?.getActiveTooltip?.()
activeTooltip?.focus(event.detail.x, event.detail.y)
previousX = event.detail.x
previousY = event.detail.y
})

return {
/**
* When we are inside an 'overlay' - the HTML tooltip will be opened immediately
Expand Down
Loading

0 comments on commit 084ba7b

Please sign in to comment.