File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
@-webkit-keyframes bitwardenfill {
|
||||
0% {
|
||||
-webkit-transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
-webkit-transform: scale(1.2, 1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes bitwardenfill {
|
||||
0% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.2, 1.2);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
span[data-bwautofill].com-bitwarden-browser-animated-fill {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.com-bitwarden-browser-animated-fill {
|
||||
animation: bitwardenfill 200ms ease-in-out 0ms 1;
|
||||
-webkit-animation: bitwardenfill 200ms ease-in-out 0ms 1;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
.com-bitwarden-browser-animated-fill {
|
||||
animation: none;
|
||||
-webkit-animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ./src/autofill/enums/autofill-port.enum.ts
|
||||
const AutofillPort = {
|
||||
InjectedScript: "autofill-injected-script-port",
|
||||
};
|
||||
|
||||
|
||||
;// ./src/autofill/utils/index.ts
|
||||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random string of characters.
|
||||
*
|
||||
* @param length - The length of the random string to generate.
|
||||
*/
|
||||
function generateRandomChars(length) {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz";
|
||||
const randomChars = [];
|
||||
const randomBytes = new Uint8Array(length);
|
||||
globalThis.crypto.getRandomValues(randomBytes);
|
||||
for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) {
|
||||
const byte = randomBytes[byteIndex];
|
||||
randomChars.push(chars[byte % chars.length]);
|
||||
}
|
||||
return randomChars.join("");
|
||||
}
|
||||
/**
|
||||
* Polyfills the requestIdleCallback API with a setTimeout fallback.
|
||||
*
|
||||
* @param callback - The callback function to run when the browser is idle.
|
||||
* @param options - The options to pass to the requestIdleCallback function.
|
||||
*/
|
||||
function requestIdleCallbackPolyfill(callback, options) {
|
||||
if ("requestIdleCallback" in globalThis) {
|
||||
return globalThis.requestIdleCallback(() => callback(), options);
|
||||
}
|
||||
return globalThis.setTimeout(() => callback(), 1);
|
||||
}
|
||||
/**
|
||||
* Polyfills the cancelIdleCallback API with a clearTimeout fallback.
|
||||
*
|
||||
* @param id - The ID of the idle callback to cancel.
|
||||
*/
|
||||
function cancelIdleCallbackPolyfill(id) {
|
||||
if ("cancelIdleCallback" in globalThis) {
|
||||
return globalThis.cancelIdleCallback(id);
|
||||
}
|
||||
return globalThis.clearTimeout(id);
|
||||
}
|
||||
/**
|
||||
* Generates a random string of characters that formatted as a custom element name.
|
||||
*/
|
||||
function generateRandomCustomElementName() {
|
||||
const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters
|
||||
const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens
|
||||
const hyphenIndices = [];
|
||||
while (hyphenIndices.length < numHyphens) {
|
||||
const index = Math.floor(Math.random() * (length - 1)) + 1;
|
||||
if (!hyphenIndices.includes(index)) {
|
||||
hyphenIndices.push(index);
|
||||
}
|
||||
}
|
||||
hyphenIndices.sort((a, b) => a - b);
|
||||
let randomString = "";
|
||||
let prevIndex = 0;
|
||||
for (let index = 0; index < hyphenIndices.length; index++) {
|
||||
const hyphenIndex = hyphenIndices[index];
|
||||
randomString = randomString + generateRandomChars(hyphenIndex - prevIndex) + "-";
|
||||
prevIndex = hyphenIndex;
|
||||
}
|
||||
randomString += generateRandomChars(length - prevIndex);
|
||||
return randomString;
|
||||
}
|
||||
/**
|
||||
* Builds a DOM element from an SVG string.
|
||||
*
|
||||
* @param svgString - The SVG string to build the DOM element from.
|
||||
* @param ariaHidden - Determines whether the SVG should be hidden from screen readers.
|
||||
*/
|
||||
function buildSvgDomElement(svgString, ariaHidden = true) {
|
||||
const domParser = new DOMParser();
|
||||
const svgDom = domParser.parseFromString(svgString, "image/svg+xml");
|
||||
const domElement = svgDom.documentElement;
|
||||
domElement.setAttribute("aria-hidden", `${ariaHidden}`);
|
||||
return domElement;
|
||||
}
|
||||
/**
|
||||
* Sends a message to the extension.
|
||||
*
|
||||
* @param command - The command to send.
|
||||
* @param options - The options to send with the command.
|
||||
*/
|
||||
function sendExtensionMessage(command_1) {
|
||||
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
|
||||
if (typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.sendMessage !== "undefined") {
|
||||
return browser.runtime.sendMessage(Object.assign({ command }, options));
|
||||
}
|
||||
return new Promise((resolve) => chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(null);
|
||||
}
|
||||
resolve(response);
|
||||
}));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sets CSS styles on an element.
|
||||
*
|
||||
* @param element - The element to set the styles on.
|
||||
* @param styles - The styles to set on the element.
|
||||
* @param priority - Determines whether the styles should be set as important.
|
||||
*/
|
||||
function setElementStyles(element, styles, priority) {
|
||||
if (!element || !styles || !Object.keys(styles).length) {
|
||||
return;
|
||||
}
|
||||
for (const styleProperty in styles) {
|
||||
element.style.setProperty(styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case
|
||||
styles[styleProperty], priority ? "important" : undefined);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets up a long-lived connection with the extension background
|
||||
* and triggers an onDisconnect event if the extension context
|
||||
* is invalidated.
|
||||
*
|
||||
* @param callback - Callback export function to run when the extension disconnects
|
||||
*/
|
||||
function setupExtensionDisconnectAction(callback) {
|
||||
const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript });
|
||||
const onDisconnectCallback = (disconnectedPort) => {
|
||||
callback(disconnectedPort);
|
||||
port.onDisconnect.removeListener(onDisconnectCallback);
|
||||
};
|
||||
port.onDisconnect.addListener(onDisconnectCallback);
|
||||
}
|
||||
/**
|
||||
* Handles setup of the extension disconnect action for the autofill init class
|
||||
* in both instances where the overlay might or might not be initialized.
|
||||
*
|
||||
* @param windowContext - The global window context
|
||||
*/
|
||||
function setupAutofillInitDisconnectAction(windowContext) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
return;
|
||||
}
|
||||
const onDisconnectCallback = () => {
|
||||
windowContext.bitwardenAutofillInit.destroy();
|
||||
delete windowContext.bitwardenAutofillInit;
|
||||
};
|
||||
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a fillable form field.
|
||||
* This is determined by whether the element is a form field and not a span.
|
||||
*
|
||||
* @param formFieldElement - The form field element to check.
|
||||
*/
|
||||
function elementIsFillableFormField(formFieldElement) {
|
||||
return !elementIsSpanElement(formFieldElement);
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is an instance of a specific tag name.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
* @param tagName - The tag name to check against.
|
||||
*/
|
||||
function elementIsInstanceOf(element, tagName) {
|
||||
return nodeIsElement(element) && element.tagName.toLowerCase() === tagName;
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a span element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsSpanElement(element) {
|
||||
return elementIsInstanceOf(element, "span");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is an input field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsInputElement(element) {
|
||||
return elementIsInstanceOf(element, "input");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a select field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsSelectElement(element) {
|
||||
return elementIsInstanceOf(element, "select");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a textarea field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsTextAreaElement(element) {
|
||||
return elementIsInstanceOf(element, "textarea");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a form element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsFormElement(element) {
|
||||
return elementIsInstanceOf(element, "form");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a label element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsLabelElement(element) {
|
||||
return elementIsInstanceOf(element, "label");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a description details `dd` element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsDescriptionDetailsElement(element) {
|
||||
return elementIsInstanceOf(element, "dd");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a description term `dt` element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsDescriptionTermElement(element) {
|
||||
return elementIsInstanceOf(element, "dt");
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is an HTML element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsElement(node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return (node === null || node === void 0 ? void 0 : node.nodeType) === Node.ELEMENT_NODE;
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is an input element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsInputElement(node) {
|
||||
return nodeIsElement(node) && elementIsInputElement(node);
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is a form element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsFormElement(node) {
|
||||
return nodeIsElement(node) && elementIsFormElement(node);
|
||||
}
|
||||
function nodeIsTypeSubmitElement(node) {
|
||||
return nodeIsElement(node) && getPropertyOrAttribute(node, "type") === "submit";
|
||||
}
|
||||
function nodeIsButtonElement(node) {
|
||||
return (nodeIsElement(node) &&
|
||||
(elementIsInstanceOf(node, "button") ||
|
||||
getPropertyOrAttribute(node, "type") === "button"));
|
||||
}
|
||||
function nodeIsAnchorElement(node) {
|
||||
return nodeIsElement(node) && elementIsInstanceOf(node, "a");
|
||||
}
|
||||
/**
|
||||
* Returns a boolean representing the attribute value of an element.
|
||||
*
|
||||
* @param element
|
||||
* @param attributeName
|
||||
* @param checkString
|
||||
*/
|
||||
function getAttributeBoolean(element, attributeName, checkString = false) {
|
||||
if (checkString) {
|
||||
return getPropertyOrAttribute(element, attributeName) === "true";
|
||||
}
|
||||
return Boolean(getPropertyOrAttribute(element, attributeName));
|
||||
}
|
||||
/**
|
||||
* Get the value of a property or attribute from a FormFieldElement.
|
||||
*
|
||||
* @param element
|
||||
* @param attributeName
|
||||
*/
|
||||
function getPropertyOrAttribute(element, attributeName) {
|
||||
if (attributeName in element) {
|
||||
return element[attributeName];
|
||||
}
|
||||
return element.getAttribute(attributeName);
|
||||
}
|
||||
/**
|
||||
* Throttles a callback function to run at most once every `limit` milliseconds.
|
||||
*
|
||||
* @param callback - The callback function to throttle.
|
||||
* @param limit - The time in milliseconds to throttle the callback.
|
||||
*/
|
||||
function throttle(callback, limit) {
|
||||
let waitingDelay = false;
|
||||
return function (...args) {
|
||||
if (!waitingDelay) {
|
||||
callback.apply(this, args);
|
||||
waitingDelay = true;
|
||||
globalThis.setTimeout(() => (waitingDelay = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Debounces a callback function to run after a delay of `delay` milliseconds.
|
||||
*
|
||||
* @param callback - The callback function to debounce.
|
||||
* @param delay - The time in milliseconds to debounce the callback.
|
||||
* @param immediate - Determines whether the callback should run immediately.
|
||||
*/
|
||||
function debounce(callback, delay, immediate) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
const callImmediately = !!immediate && !timeout;
|
||||
if (timeout) {
|
||||
globalThis.clearTimeout(timeout);
|
||||
}
|
||||
timeout = globalThis.setTimeout(() => {
|
||||
timeout = null;
|
||||
if (!callImmediately) {
|
||||
callback.apply(this, args);
|
||||
}
|
||||
}, delay);
|
||||
if (callImmediately) {
|
||||
callback.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Gathers and normalizes keywords from a potential submit button element. Used
|
||||
* to verify if the element submits a login or change password form.
|
||||
*
|
||||
* @param element - The element to gather keywords from.
|
||||
*/
|
||||
function getSubmitButtonKeywordsSet(element) {
|
||||
const keywords = [
|
||||
element.textContent,
|
||||
element.getAttribute("type"),
|
||||
element.getAttribute("value"),
|
||||
element.getAttribute("aria-label"),
|
||||
element.getAttribute("aria-labelledby"),
|
||||
element.getAttribute("aria-describedby"),
|
||||
element.getAttribute("title"),
|
||||
element.getAttribute("id"),
|
||||
element.getAttribute("name"),
|
||||
element.getAttribute("class"),
|
||||
];
|
||||
const keywordsSet = new Set();
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
if (typeof keywords[i] === "string") {
|
||||
// Iterate over all keywords metadata and split them by non-letter characters.
|
||||
// This ensures we check against individual words and not the entire string.
|
||||
keywords[i]
|
||||
.toLowerCase()
|
||||
.replace(/[-\s]/g, "")
|
||||
.split(/[^\p{L}]+/gu)
|
||||
.forEach((keyword) => {
|
||||
if (keyword) {
|
||||
keywordsSet.add(keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return keywordsSet;
|
||||
}
|
||||
/**
|
||||
* Generates the origin and subdomain match patterns for the URL.
|
||||
*
|
||||
* @param url - The URL of the tab
|
||||
*/
|
||||
function generateDomainMatchPatterns(url) {
|
||||
try {
|
||||
const extensionUrlPattern = /^(chrome|chrome-extension|moz-extension|safari-web-extension):\/\/\/?/;
|
||||
if (extensionUrlPattern.test(url)) {
|
||||
return [];
|
||||
}
|
||||
// Add protocol to URL if it is missing to allow for parsing the hostname correctly
|
||||
const urlPattern = /^(https?|file):\/\/\/?/;
|
||||
if (!urlPattern.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
let protocolGlob = "*://";
|
||||
if (url.startsWith("file:///")) {
|
||||
protocolGlob = "*:///"; // File URLs require three slashes to be a valid match pattern
|
||||
}
|
||||
const parsedUrl = new URL(url);
|
||||
const originMatchPattern = `${protocolGlob}${parsedUrl.hostname}/*`;
|
||||
const splitHost = parsedUrl.hostname.split(".");
|
||||
const domain = splitHost.slice(-2).join(".");
|
||||
const subDomainMatchPattern = `${protocolGlob}*.${domain}/*`;
|
||||
return [originMatchPattern, subDomainMatchPattern];
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines if the status code of the web response is invalid. An invalid status code is
|
||||
* any status code that is not in the 200-299 range.
|
||||
*
|
||||
* @param statusCode - The status code of the web response
|
||||
*/
|
||||
function isInvalidResponseStatusCode(statusCode) {
|
||||
return statusCode < 200 || statusCode >= 300;
|
||||
}
|
||||
/**
|
||||
* Determines if the current context is within a sandboxed iframe.
|
||||
*/
|
||||
function currentlyInSandboxedIframe() {
|
||||
var _a, _b;
|
||||
if (String(self.origin).toLowerCase() === "null" || globalThis.location.hostname === "") {
|
||||
return true;
|
||||
}
|
||||
const sandbox = (_b = (_a = globalThis.frameElement) === null || _a === void 0 ? void 0 : _a.getAttribute) === null || _b === void 0 ? void 0 : _b.call(_a, "sandbox");
|
||||
// No frameElement or sandbox attribute means not sandboxed
|
||||
if (sandbox === null || sandbox === undefined) {
|
||||
return false;
|
||||
}
|
||||
// An empty string means fully sandboxed
|
||||
if (sandbox === "") {
|
||||
return true;
|
||||
}
|
||||
const tokens = new Set(sandbox.toLowerCase().split(" "));
|
||||
return !["allow-scripts", "allow-same-origin"].every((token) => tokens.has(token));
|
||||
}
|
||||
/**
|
||||
* This object allows us to map a special character to a key name. The key name is used
|
||||
* in gathering the i18n translation of the written version of the special character.
|
||||
*/
|
||||
const specialCharacterToKeyMap = {
|
||||
" ": "spaceCharacterDescriptor",
|
||||
"~": "tildeCharacterDescriptor",
|
||||
"`": "backtickCharacterDescriptor",
|
||||
"!": "exclamationCharacterDescriptor",
|
||||
"@": "atSignCharacterDescriptor",
|
||||
"#": "hashSignCharacterDescriptor",
|
||||
$: "dollarSignCharacterDescriptor",
|
||||
"%": "percentSignCharacterDescriptor",
|
||||
"^": "caretCharacterDescriptor",
|
||||
"&": "ampersandCharacterDescriptor",
|
||||
"*": "asteriskCharacterDescriptor",
|
||||
"(": "parenLeftCharacterDescriptor",
|
||||
")": "parenRightCharacterDescriptor",
|
||||
"-": "hyphenCharacterDescriptor",
|
||||
_: "underscoreCharacterDescriptor",
|
||||
"+": "plusCharacterDescriptor",
|
||||
"=": "equalsCharacterDescriptor",
|
||||
"{": "braceLeftCharacterDescriptor",
|
||||
"}": "braceRightCharacterDescriptor",
|
||||
"[": "bracketLeftCharacterDescriptor",
|
||||
"]": "bracketRightCharacterDescriptor",
|
||||
"|": "pipeCharacterDescriptor",
|
||||
"\\": "backSlashCharacterDescriptor",
|
||||
":": "colonCharacterDescriptor",
|
||||
";": "semicolonCharacterDescriptor",
|
||||
'"': "doubleQuoteCharacterDescriptor",
|
||||
"'": "singleQuoteCharacterDescriptor",
|
||||
"<": "lessThanCharacterDescriptor",
|
||||
">": "greaterThanCharacterDescriptor",
|
||||
",": "commaCharacterDescriptor",
|
||||
".": "periodCharacterDescriptor",
|
||||
"?": "questionCharacterDescriptor",
|
||||
"/": "forwardSlashCharacterDescriptor",
|
||||
};
|
||||
/**
|
||||
* Determines if the current rect values are not all 0.
|
||||
*/
|
||||
function rectHasSize(rect) {
|
||||
if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Checks if all the values corresponding to the specified keys in an object are null.
|
||||
* If no keys are specified, checks all keys in the object.
|
||||
*
|
||||
* @param obj - The object to check.
|
||||
* @param keys - An optional array of keys to check in the object. Defaults to all keys.
|
||||
* @returns Returns true if all values for the specified keys (or all keys if none are provided) are null; otherwise, false.
|
||||
*/
|
||||
function areKeyValuesNull(obj, keys) {
|
||||
const keysToCheck = keys && keys.length > 0 ? keys : Object.keys(obj);
|
||||
return keysToCheck.every((key) => obj[key] == null);
|
||||
}
|
||||
|
||||
;// ./src/autofill/content/autofiller.ts
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", loadAutofiller);
|
||||
}
|
||||
else {
|
||||
loadAutofiller();
|
||||
}
|
||||
function loadAutofiller() {
|
||||
let pageHref = null;
|
||||
let filledThisHref = false;
|
||||
let delayFillTimeout;
|
||||
let doFillInterval;
|
||||
const handleExtensionDisconnect = () => {
|
||||
clearDoFillInterval();
|
||||
clearDelayFillTimeout();
|
||||
};
|
||||
const handleExtensionMessage = (message) => {
|
||||
if (message.command === "fillForm" && pageHref === message.url) {
|
||||
filledThisHref = true;
|
||||
}
|
||||
};
|
||||
setupExtensionEventListeners();
|
||||
triggerUserFillOnLoad();
|
||||
function triggerUserFillOnLoad() {
|
||||
clearDoFillInterval();
|
||||
doFillInterval = setInterval(() => doFillIfNeeded(), 500);
|
||||
}
|
||||
function doFillIfNeeded(force = false) {
|
||||
if (force || pageHref !== window.location.href) {
|
||||
if (!force) {
|
||||
// Some websites are slow and rendering all page content. Try to fill again later
|
||||
// if we haven't already.
|
||||
filledThisHref = false;
|
||||
clearDelayFillTimeout();
|
||||
delayFillTimeout = window.setTimeout(() => {
|
||||
if (!filledThisHref) {
|
||||
doFillIfNeeded(true);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
pageHref = window.location.href;
|
||||
const msg = {
|
||||
command: "bgCollectPageDetails",
|
||||
sender: "autofiller",
|
||||
};
|
||||
void chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
function clearDoFillInterval() {
|
||||
if (doFillInterval) {
|
||||
window.clearInterval(doFillInterval);
|
||||
}
|
||||
}
|
||||
function clearDelayFillTimeout() {
|
||||
if (delayFillTimeout) {
|
||||
window.clearTimeout(delayFillTimeout);
|
||||
}
|
||||
}
|
||||
function setupExtensionEventListeners() {
|
||||
setupExtensionDisconnectAction(handleExtensionDisconnect);
|
||||
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ../../libs/common/src/vault/enums/vault-messages.enum.ts
|
||||
const VaultMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
/** @deprecated use {@link OpenBrowserExtensionToUrl} */
|
||||
OpenAtRiskPasswords: "openAtRiskPasswords",
|
||||
OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl",
|
||||
PopupOpened: "popupOpened",
|
||||
};
|
||||
|
||||
|
||||
;// ./src/autofill/content/content-message-handler.ts
|
||||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handlers for window messages from the content script.
|
||||
* NOTE: These handlers should be above the event listener to ensure they are defined before being used.
|
||||
*/
|
||||
const windowMessageHandlers = {
|
||||
authResult: ({ data, referrer }) => handleAuthResultMessage(data, referrer),
|
||||
webAuthnResult: ({ data, referrer }) => handleWebAuthnResultMessage(data, referrer),
|
||||
[VaultMessages.checkBwInstalled]: () => handleExtensionInstallCheck(),
|
||||
duoResult: ({ data, referrer }) => handleDuoResultMessage(data, referrer),
|
||||
[VaultMessages.OpenAtRiskPasswords]: () => handleOpenAtRiskPasswordsMessage(),
|
||||
[VaultMessages.OpenBrowserExtensionToUrl]: ({ data }) => handleOpenBrowserExtensionToUrlMessage(data),
|
||||
};
|
||||
/**
|
||||
* IMPORTANT: Safari seems to have a bug where it doesn't properly handle
|
||||
* window message events from content scripts when the listener these events
|
||||
* is registered within a class. This is why these listeners are registered
|
||||
* at the top level of this file.
|
||||
*/
|
||||
window.addEventListener("message", handleWindowMessageEvent, false);
|
||||
chrome.runtime.onMessage.addListener(handleExtensionMessage);
|
||||
setupExtensionDisconnectAction(() => {
|
||||
window.removeEventListener("message", handleWindowMessageEvent);
|
||||
chrome.runtime.onMessage.removeListener(handleExtensionMessage);
|
||||
});
|
||||
/**
|
||||
* Handles the post to the web vault showing the extension has been installed
|
||||
*/
|
||||
function handleExtensionInstallCheck() {
|
||||
window.postMessage({ command: VaultMessages.HasBwInstalled });
|
||||
}
|
||||
/**
|
||||
* Handles the auth result message from the window.
|
||||
*
|
||||
* @param data - Data from the window message
|
||||
* @param referrer - The referrer of the window
|
||||
*/
|
||||
function handleAuthResultMessage(data, referrer) {
|
||||
const { command, lastpass, code, state } = data;
|
||||
sendExtensionRuntimeMessage({ command, code, state, lastpass, referrer });
|
||||
}
|
||||
/**
|
||||
* Handles the Duo 2FA result message from the window.
|
||||
*
|
||||
* @param data - Data from the window message
|
||||
* @param referrer - The referrer of the window
|
||||
*/
|
||||
function handleDuoResultMessage(data, referrer) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const { command, code, state } = data;
|
||||
sendExtensionRuntimeMessage({ command, code, state, referrer });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles the webauthn result message from the window.
|
||||
*
|
||||
* @param data - Data from the window message
|
||||
* @param referrer - The referrer of the window
|
||||
*/
|
||||
function handleWebAuthnResultMessage(data, referrer) {
|
||||
const { command, remember } = data;
|
||||
sendExtensionRuntimeMessage({ command, data: data.data, remember, referrer });
|
||||
}
|
||||
/** @deprecated use {@link handleOpenBrowserExtensionToUrlMessage} */
|
||||
function handleOpenAtRiskPasswordsMessage() {
|
||||
sendExtensionRuntimeMessage({ command: VaultMessages.OpenAtRiskPasswords });
|
||||
}
|
||||
function handleOpenBrowserExtensionToUrlMessage({ url }) {
|
||||
sendExtensionRuntimeMessage({ command: VaultMessages.OpenBrowserExtensionToUrl, url });
|
||||
}
|
||||
/**
|
||||
* Handles window message events, validating source and extracting referrer for security.
|
||||
*
|
||||
* @param event - The window message event
|
||||
*/
|
||||
function handleWindowMessageEvent(event) {
|
||||
const { source, data, origin } = event;
|
||||
if (source !== window || !(data === null || data === void 0 ? void 0 : data.command)) {
|
||||
return;
|
||||
}
|
||||
// Extract hostname from event.origin for secure referrer validation in background script
|
||||
let referrer;
|
||||
// Sandboxed iframe or opaque origin support
|
||||
if (origin === "null") {
|
||||
referrer = "null";
|
||||
}
|
||||
else {
|
||||
try {
|
||||
const originUrl = new URL(origin);
|
||||
referrer = originUrl.hostname;
|
||||
}
|
||||
catch (_a) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const handler = windowMessageHandlers[data.command];
|
||||
if (handler) {
|
||||
handler({ data, referrer });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Commands to forward from this script to the extension background.
|
||||
*/
|
||||
const forwardCommands = new Set([
|
||||
"bgUnlockPopoutOpened",
|
||||
"addToLockedVaultPendingNotifications",
|
||||
"unlockCompleted",
|
||||
"addedCipher",
|
||||
]);
|
||||
/**
|
||||
* Handles messages from the extension. Currently, this is
|
||||
* used to forward messages from the background context to
|
||||
* other scripts within the extension.
|
||||
*
|
||||
* @param message - The message from the extension
|
||||
*/
|
||||
function handleExtensionMessage(message) {
|
||||
if (forwardCommands.has(message.command)) {
|
||||
sendExtensionRuntimeMessage(message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sends a message to the extension runtime, and ignores
|
||||
* any potential promises that should be handled using
|
||||
* the `void` operator.
|
||||
*
|
||||
* @param message - The message to send to the extension runtime
|
||||
*/
|
||||
function sendExtensionRuntimeMessage(message) {
|
||||
void chrome.runtime.sendMessage(message);
|
||||
}
|
||||
/**
|
||||
* Duplicate implementation of the same named method within `apps/browser/src/autofill/utils/index.ts`.
|
||||
* This is done due to some strange observed compilation behavior present when importing the method from
|
||||
* the utils file.
|
||||
*
|
||||
* TODO: Investigate why webpack tree shaking is not removing other methods when importing from the utils file.
|
||||
* Possible cause can be seen below:
|
||||
* @see https://stackoverflow.com/questions/71679366/webpack5-does-not-seem-to-tree-shake-unused-exports
|
||||
*
|
||||
* @param callback - Callback function to run when the extension disconnects
|
||||
*/
|
||||
function setupExtensionDisconnectAction(callback) {
|
||||
const port = chrome.runtime.connect({ name: "autofill-injected-script-port" });
|
||||
const onDisconnectCallback = (disconnectedPort) => {
|
||||
callback(disconnectedPort);
|
||||
port.onDisconnect.removeListener(onDisconnectCallback);
|
||||
};
|
||||
port.onDisconnect.addListener(onDisconnectCallback);
|
||||
}
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,68 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
const inputTags = ["input", "textarea", "select"];
|
||||
const labelTags = ["label", "span"];
|
||||
const attributeKeys = ["id", "name", "label-aria", "placeholder"];
|
||||
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
|
||||
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
|
||||
let clickedElement = null;
|
||||
// Find the best attribute to be used as the Name for an element in a custom field.
|
||||
function getClickedElementIdentifier() {
|
||||
var _a, _b;
|
||||
if (clickedElement == null) {
|
||||
return invalidElement;
|
||||
}
|
||||
const clickedTag = clickedElement.nodeName.toLowerCase();
|
||||
let inputElement = null;
|
||||
// Try to identify the input element (which may not be the clicked element)
|
||||
if (labelTags.includes(clickedTag)) {
|
||||
let inputId;
|
||||
if (clickedTag === "label") {
|
||||
inputId = clickedElement.getAttribute("for");
|
||||
}
|
||||
else {
|
||||
inputId = (_a = clickedElement.closest("label")) === null || _a === void 0 ? void 0 : _a.getAttribute("for");
|
||||
}
|
||||
if (inputId) {
|
||||
inputElement = document.getElementById(inputId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
inputElement = clickedElement;
|
||||
}
|
||||
if (inputElement == null || !inputTags.includes(inputElement.nodeName.toLowerCase())) {
|
||||
return invalidElement;
|
||||
}
|
||||
for (const attributeKey of attributeKeys) {
|
||||
const attributeValue = inputElement.getAttribute(attributeKey);
|
||||
const selector = "[" + attributeKey + '="' + attributeValue + '"]';
|
||||
if (!isNullOrEmpty(attributeValue) && ((_b = document.querySelectorAll(selector)) === null || _b === void 0 ? void 0 : _b.length) === 1) {
|
||||
return attributeValue;
|
||||
}
|
||||
}
|
||||
return noUniqueIdentifier;
|
||||
}
|
||||
function isNullOrEmpty(s) {
|
||||
return s == null || s === "";
|
||||
}
|
||||
// We only have access to the element that's been clicked when the context menu is first opened.
|
||||
// Remember it for use later.
|
||||
document.addEventListener("contextmenu", (event) => {
|
||||
clickedElement = event.target;
|
||||
});
|
||||
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
|
||||
chrome.runtime.onMessage.addListener((event, _sender, sendResponse) => {
|
||||
if (event.command === "getClickedElement") {
|
||||
const identifier = getClickedElementIdentifier();
|
||||
if (sendResponse) {
|
||||
sendResponse(identifier);
|
||||
}
|
||||
void chrome.runtime.sendMessage({
|
||||
command: "getClickedElementResponse",
|
||||
sender: "contextMenuHandler",
|
||||
identifier: identifier,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,773 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ./src/autofill/enums/autofill-port.enum.ts
|
||||
const autofill_port_enum_AutofillPort = {
|
||||
InjectedScript: "autofill-injected-script-port",
|
||||
};
|
||||
|
||||
|
||||
;// ./src/autofill/utils/index.ts
|
||||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a random string of characters.
|
||||
*
|
||||
* @param length - The length of the random string to generate.
|
||||
*/
|
||||
function generateRandomChars(length) {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz";
|
||||
const randomChars = [];
|
||||
const randomBytes = new Uint8Array(length);
|
||||
globalThis.crypto.getRandomValues(randomBytes);
|
||||
for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) {
|
||||
const byte = randomBytes[byteIndex];
|
||||
randomChars.push(chars[byte % chars.length]);
|
||||
}
|
||||
return randomChars.join("");
|
||||
}
|
||||
/**
|
||||
* Polyfills the requestIdleCallback API with a setTimeout fallback.
|
||||
*
|
||||
* @param callback - The callback function to run when the browser is idle.
|
||||
* @param options - The options to pass to the requestIdleCallback function.
|
||||
*/
|
||||
function requestIdleCallbackPolyfill(callback, options) {
|
||||
if ("requestIdleCallback" in globalThis) {
|
||||
return globalThis.requestIdleCallback(() => callback(), options);
|
||||
}
|
||||
return globalThis.setTimeout(() => callback(), 1);
|
||||
}
|
||||
/**
|
||||
* Polyfills the cancelIdleCallback API with a clearTimeout fallback.
|
||||
*
|
||||
* @param id - The ID of the idle callback to cancel.
|
||||
*/
|
||||
function cancelIdleCallbackPolyfill(id) {
|
||||
if ("cancelIdleCallback" in globalThis) {
|
||||
return globalThis.cancelIdleCallback(id);
|
||||
}
|
||||
return globalThis.clearTimeout(id);
|
||||
}
|
||||
/**
|
||||
* Generates a random string of characters that formatted as a custom element name.
|
||||
*/
|
||||
function generateRandomCustomElementName() {
|
||||
const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters
|
||||
const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens
|
||||
const hyphenIndices = [];
|
||||
while (hyphenIndices.length < numHyphens) {
|
||||
const index = Math.floor(Math.random() * (length - 1)) + 1;
|
||||
if (!hyphenIndices.includes(index)) {
|
||||
hyphenIndices.push(index);
|
||||
}
|
||||
}
|
||||
hyphenIndices.sort((a, b) => a - b);
|
||||
let randomString = "";
|
||||
let prevIndex = 0;
|
||||
for (let index = 0; index < hyphenIndices.length; index++) {
|
||||
const hyphenIndex = hyphenIndices[index];
|
||||
randomString = randomString + generateRandomChars(hyphenIndex - prevIndex) + "-";
|
||||
prevIndex = hyphenIndex;
|
||||
}
|
||||
randomString += generateRandomChars(length - prevIndex);
|
||||
return randomString;
|
||||
}
|
||||
/**
|
||||
* Builds a DOM element from an SVG string.
|
||||
*
|
||||
* @param svgString - The SVG string to build the DOM element from.
|
||||
* @param ariaHidden - Determines whether the SVG should be hidden from screen readers.
|
||||
*/
|
||||
function buildSvgDomElement(svgString, ariaHidden = true) {
|
||||
const domParser = new DOMParser();
|
||||
const svgDom = domParser.parseFromString(svgString, "image/svg+xml");
|
||||
const domElement = svgDom.documentElement;
|
||||
domElement.setAttribute("aria-hidden", `${ariaHidden}`);
|
||||
return domElement;
|
||||
}
|
||||
/**
|
||||
* Sends a message to the extension.
|
||||
*
|
||||
* @param command - The command to send.
|
||||
* @param options - The options to send with the command.
|
||||
*/
|
||||
function sendExtensionMessage(command_1) {
|
||||
return __awaiter(this, arguments, void 0, function* (command, options = {}) {
|
||||
if (typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.sendMessage !== "undefined") {
|
||||
return browser.runtime.sendMessage(Object.assign({ command }, options));
|
||||
}
|
||||
return new Promise((resolve) => chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(null);
|
||||
}
|
||||
resolve(response);
|
||||
}));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sets CSS styles on an element.
|
||||
*
|
||||
* @param element - The element to set the styles on.
|
||||
* @param styles - The styles to set on the element.
|
||||
* @param priority - Determines whether the styles should be set as important.
|
||||
*/
|
||||
function setElementStyles(element, styles, priority) {
|
||||
if (!element || !styles || !Object.keys(styles).length) {
|
||||
return;
|
||||
}
|
||||
for (const styleProperty in styles) {
|
||||
element.style.setProperty(styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case
|
||||
styles[styleProperty], priority ? "important" : undefined);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets up a long-lived connection with the extension background
|
||||
* and triggers an onDisconnect event if the extension context
|
||||
* is invalidated.
|
||||
*
|
||||
* @param callback - Callback export function to run when the extension disconnects
|
||||
*/
|
||||
function setupExtensionDisconnectAction(callback) {
|
||||
const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript });
|
||||
const onDisconnectCallback = (disconnectedPort) => {
|
||||
callback(disconnectedPort);
|
||||
port.onDisconnect.removeListener(onDisconnectCallback);
|
||||
};
|
||||
port.onDisconnect.addListener(onDisconnectCallback);
|
||||
}
|
||||
/**
|
||||
* Handles setup of the extension disconnect action for the autofill init class
|
||||
* in both instances where the overlay might or might not be initialized.
|
||||
*
|
||||
* @param windowContext - The global window context
|
||||
*/
|
||||
function setupAutofillInitDisconnectAction(windowContext) {
|
||||
if (!windowContext.bitwardenAutofillInit) {
|
||||
return;
|
||||
}
|
||||
const onDisconnectCallback = () => {
|
||||
windowContext.bitwardenAutofillInit.destroy();
|
||||
delete windowContext.bitwardenAutofillInit;
|
||||
};
|
||||
setupExtensionDisconnectAction(onDisconnectCallback);
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a fillable form field.
|
||||
* This is determined by whether the element is a form field and not a span.
|
||||
*
|
||||
* @param formFieldElement - The form field element to check.
|
||||
*/
|
||||
function elementIsFillableFormField(formFieldElement) {
|
||||
return !elementIsSpanElement(formFieldElement);
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is an instance of a specific tag name.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
* @param tagName - The tag name to check against.
|
||||
*/
|
||||
function elementIsInstanceOf(element, tagName) {
|
||||
return nodeIsElement(element) && element.tagName.toLowerCase() === tagName;
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a span element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsSpanElement(element) {
|
||||
return elementIsInstanceOf(element, "span");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is an input field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsInputElement(element) {
|
||||
return elementIsInstanceOf(element, "input");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a select field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsSelectElement(element) {
|
||||
return elementIsInstanceOf(element, "select");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a textarea field.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsTextAreaElement(element) {
|
||||
return elementIsInstanceOf(element, "textarea");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a form element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsFormElement(element) {
|
||||
return elementIsInstanceOf(element, "form");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a label element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsLabelElement(element) {
|
||||
return elementIsInstanceOf(element, "label");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a description details `dd` element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsDescriptionDetailsElement(element) {
|
||||
return elementIsInstanceOf(element, "dd");
|
||||
}
|
||||
/**
|
||||
* Identifies whether an element is a description term `dt` element.
|
||||
*
|
||||
* @param element - The element to check.
|
||||
*/
|
||||
function elementIsDescriptionTermElement(element) {
|
||||
return elementIsInstanceOf(element, "dt");
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is an HTML element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsElement(node) {
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
return (node === null || node === void 0 ? void 0 : node.nodeType) === Node.ELEMENT_NODE;
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is an input element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsInputElement(node) {
|
||||
return nodeIsElement(node) && elementIsInputElement(node);
|
||||
}
|
||||
/**
|
||||
* Identifies whether a node is a form element.
|
||||
*
|
||||
* @param node - The node to check.
|
||||
*/
|
||||
function nodeIsFormElement(node) {
|
||||
return nodeIsElement(node) && elementIsFormElement(node);
|
||||
}
|
||||
function nodeIsTypeSubmitElement(node) {
|
||||
return nodeIsElement(node) && getPropertyOrAttribute(node, "type") === "submit";
|
||||
}
|
||||
function nodeIsButtonElement(node) {
|
||||
return (nodeIsElement(node) &&
|
||||
(elementIsInstanceOf(node, "button") ||
|
||||
getPropertyOrAttribute(node, "type") === "button"));
|
||||
}
|
||||
function nodeIsAnchorElement(node) {
|
||||
return nodeIsElement(node) && elementIsInstanceOf(node, "a");
|
||||
}
|
||||
/**
|
||||
* Returns a boolean representing the attribute value of an element.
|
||||
*
|
||||
* @param element
|
||||
* @param attributeName
|
||||
* @param checkString
|
||||
*/
|
||||
function getAttributeBoolean(element, attributeName, checkString = false) {
|
||||
if (checkString) {
|
||||
return getPropertyOrAttribute(element, attributeName) === "true";
|
||||
}
|
||||
return Boolean(getPropertyOrAttribute(element, attributeName));
|
||||
}
|
||||
/**
|
||||
* Get the value of a property or attribute from a FormFieldElement.
|
||||
*
|
||||
* @param element
|
||||
* @param attributeName
|
||||
*/
|
||||
function getPropertyOrAttribute(element, attributeName) {
|
||||
if (attributeName in element) {
|
||||
return element[attributeName];
|
||||
}
|
||||
return element.getAttribute(attributeName);
|
||||
}
|
||||
/**
|
||||
* Throttles a callback function to run at most once every `limit` milliseconds.
|
||||
*
|
||||
* @param callback - The callback function to throttle.
|
||||
* @param limit - The time in milliseconds to throttle the callback.
|
||||
*/
|
||||
function throttle(callback, limit) {
|
||||
let waitingDelay = false;
|
||||
return function (...args) {
|
||||
if (!waitingDelay) {
|
||||
callback.apply(this, args);
|
||||
waitingDelay = true;
|
||||
globalThis.setTimeout(() => (waitingDelay = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Debounces a callback function to run after a delay of `delay` milliseconds.
|
||||
*
|
||||
* @param callback - The callback function to debounce.
|
||||
* @param delay - The time in milliseconds to debounce the callback.
|
||||
* @param immediate - Determines whether the callback should run immediately.
|
||||
*/
|
||||
function debounce(callback, delay, immediate) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
const callImmediately = !!immediate && !timeout;
|
||||
if (timeout) {
|
||||
globalThis.clearTimeout(timeout);
|
||||
}
|
||||
timeout = globalThis.setTimeout(() => {
|
||||
timeout = null;
|
||||
if (!callImmediately) {
|
||||
callback.apply(this, args);
|
||||
}
|
||||
}, delay);
|
||||
if (callImmediately) {
|
||||
callback.apply(this, args);
|
||||
}
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Gathers and normalizes keywords from a potential submit button element. Used
|
||||
* to verify if the element submits a login or change password form.
|
||||
*
|
||||
* @param element - The element to gather keywords from.
|
||||
*/
|
||||
function getSubmitButtonKeywordsSet(element) {
|
||||
const keywords = [
|
||||
element.textContent,
|
||||
element.getAttribute("type"),
|
||||
element.getAttribute("value"),
|
||||
element.getAttribute("aria-label"),
|
||||
element.getAttribute("aria-labelledby"),
|
||||
element.getAttribute("aria-describedby"),
|
||||
element.getAttribute("title"),
|
||||
element.getAttribute("id"),
|
||||
element.getAttribute("name"),
|
||||
element.getAttribute("class"),
|
||||
];
|
||||
const keywordsSet = new Set();
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
if (typeof keywords[i] === "string") {
|
||||
// Iterate over all keywords metadata and split them by non-letter characters.
|
||||
// This ensures we check against individual words and not the entire string.
|
||||
keywords[i]
|
||||
.toLowerCase()
|
||||
.replace(/[-\s]/g, "")
|
||||
.split(/[^\p{L}]+/gu)
|
||||
.forEach((keyword) => {
|
||||
if (keyword) {
|
||||
keywordsSet.add(keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return keywordsSet;
|
||||
}
|
||||
/**
|
||||
* Generates the origin and subdomain match patterns for the URL.
|
||||
*
|
||||
* @param url - The URL of the tab
|
||||
*/
|
||||
function generateDomainMatchPatterns(url) {
|
||||
try {
|
||||
const extensionUrlPattern = /^(chrome|chrome-extension|moz-extension|safari-web-extension):\/\/\/?/;
|
||||
if (extensionUrlPattern.test(url)) {
|
||||
return [];
|
||||
}
|
||||
// Add protocol to URL if it is missing to allow for parsing the hostname correctly
|
||||
const urlPattern = /^(https?|file):\/\/\/?/;
|
||||
if (!urlPattern.test(url)) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
let protocolGlob = "*://";
|
||||
if (url.startsWith("file:///")) {
|
||||
protocolGlob = "*:///"; // File URLs require three slashes to be a valid match pattern
|
||||
}
|
||||
const parsedUrl = new URL(url);
|
||||
const originMatchPattern = `${protocolGlob}${parsedUrl.hostname}/*`;
|
||||
const splitHost = parsedUrl.hostname.split(".");
|
||||
const domain = splitHost.slice(-2).join(".");
|
||||
const subDomainMatchPattern = `${protocolGlob}*.${domain}/*`;
|
||||
return [originMatchPattern, subDomainMatchPattern];
|
||||
}
|
||||
catch (_a) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Determines if the status code of the web response is invalid. An invalid status code is
|
||||
* any status code that is not in the 200-299 range.
|
||||
*
|
||||
* @param statusCode - The status code of the web response
|
||||
*/
|
||||
function isInvalidResponseStatusCode(statusCode) {
|
||||
return statusCode < 200 || statusCode >= 300;
|
||||
}
|
||||
/**
|
||||
* Determines if the current context is within a sandboxed iframe.
|
||||
*/
|
||||
function currentlyInSandboxedIframe() {
|
||||
var _a, _b;
|
||||
if (String(self.origin).toLowerCase() === "null" || globalThis.location.hostname === "") {
|
||||
return true;
|
||||
}
|
||||
const sandbox = (_b = (_a = globalThis.frameElement) === null || _a === void 0 ? void 0 : _a.getAttribute) === null || _b === void 0 ? void 0 : _b.call(_a, "sandbox");
|
||||
// No frameElement or sandbox attribute means not sandboxed
|
||||
if (sandbox === null || sandbox === undefined) {
|
||||
return false;
|
||||
}
|
||||
// An empty string means fully sandboxed
|
||||
if (sandbox === "") {
|
||||
return true;
|
||||
}
|
||||
const tokens = new Set(sandbox.toLowerCase().split(" "));
|
||||
return !["allow-scripts", "allow-same-origin"].every((token) => tokens.has(token));
|
||||
}
|
||||
/**
|
||||
* This object allows us to map a special character to a key name. The key name is used
|
||||
* in gathering the i18n translation of the written version of the special character.
|
||||
*/
|
||||
const specialCharacterToKeyMap = {
|
||||
" ": "spaceCharacterDescriptor",
|
||||
"~": "tildeCharacterDescriptor",
|
||||
"`": "backtickCharacterDescriptor",
|
||||
"!": "exclamationCharacterDescriptor",
|
||||
"@": "atSignCharacterDescriptor",
|
||||
"#": "hashSignCharacterDescriptor",
|
||||
$: "dollarSignCharacterDescriptor",
|
||||
"%": "percentSignCharacterDescriptor",
|
||||
"^": "caretCharacterDescriptor",
|
||||
"&": "ampersandCharacterDescriptor",
|
||||
"*": "asteriskCharacterDescriptor",
|
||||
"(": "parenLeftCharacterDescriptor",
|
||||
")": "parenRightCharacterDescriptor",
|
||||
"-": "hyphenCharacterDescriptor",
|
||||
_: "underscoreCharacterDescriptor",
|
||||
"+": "plusCharacterDescriptor",
|
||||
"=": "equalsCharacterDescriptor",
|
||||
"{": "braceLeftCharacterDescriptor",
|
||||
"}": "braceRightCharacterDescriptor",
|
||||
"[": "bracketLeftCharacterDescriptor",
|
||||
"]": "bracketRightCharacterDescriptor",
|
||||
"|": "pipeCharacterDescriptor",
|
||||
"\\": "backSlashCharacterDescriptor",
|
||||
":": "colonCharacterDescriptor",
|
||||
";": "semicolonCharacterDescriptor",
|
||||
'"': "doubleQuoteCharacterDescriptor",
|
||||
"'": "singleQuoteCharacterDescriptor",
|
||||
"<": "lessThanCharacterDescriptor",
|
||||
">": "greaterThanCharacterDescriptor",
|
||||
",": "commaCharacterDescriptor",
|
||||
".": "periodCharacterDescriptor",
|
||||
"?": "questionCharacterDescriptor",
|
||||
"/": "forwardSlashCharacterDescriptor",
|
||||
};
|
||||
/**
|
||||
* Determines if the current rect values are not all 0.
|
||||
*/
|
||||
function rectHasSize(rect) {
|
||||
if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Checks if all the values corresponding to the specified keys in an object are null.
|
||||
* If no keys are specified, checks all keys in the object.
|
||||
*
|
||||
* @param obj - The object to check.
|
||||
* @param keys - An optional array of keys to check in the object. Defaults to all keys.
|
||||
* @returns Returns true if all values for the specified keys (or all keys if none are provided) are null; otherwise, false.
|
||||
*/
|
||||
function areKeyValuesNull(obj, keys) {
|
||||
const keysToCheck = keys && keys.length > 0 ? keys : Object.keys(obj);
|
||||
return keysToCheck.every((key) => obj[key] == null);
|
||||
}
|
||||
|
||||
;// ./src/autofill/fido2/enums/fido2-port-name.enum.ts
|
||||
const Fido2PortName = {
|
||||
InjectedScript: "fido2-injected-content-script-port",
|
||||
};
|
||||
|
||||
;// ./src/autofill/fido2/content/messaging/message.ts
|
||||
const MessageTypes = {
|
||||
CredentialCreationRequest: 0,
|
||||
CredentialCreationResponse: 1,
|
||||
CredentialGetRequest: 2,
|
||||
CredentialGetResponse: 3,
|
||||
AbortRequest: 4,
|
||||
DisconnectRequest: 5,
|
||||
ReconnectRequest: 6,
|
||||
AbortResponse: 7,
|
||||
ErrorResponse: 8,
|
||||
};
|
||||
|
||||
;// ./src/autofill/fido2/content/messaging/messenger.ts
|
||||
var messenger_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
const SENDER = "bitwarden-webauthn";
|
||||
/**
|
||||
* A class that handles communication between the page and content script. It converts
|
||||
* the browser's broadcasting API into a request/response API with support for seamlessly
|
||||
* handling aborts and exceptions across separate execution contexts.
|
||||
*/
|
||||
class Messenger {
|
||||
/**
|
||||
* Creates a messenger that uses the browser's `window.postMessage` API to initiate
|
||||
* requests in the content script. Every request will then create it's own
|
||||
* `MessageChannel` through which all subsequent communication will be sent through.
|
||||
*
|
||||
* @param window the window object to use for communication
|
||||
* @returns a `Messenger` instance
|
||||
*/
|
||||
static forDOMCommunication(window) {
|
||||
const windowOrigin = window.location.origin;
|
||||
return new Messenger({
|
||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
||||
addEventListener: (listener) => window.addEventListener("message", listener),
|
||||
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
||||
});
|
||||
}
|
||||
constructor(broadcastChannel) {
|
||||
this.broadcastChannel = broadcastChannel;
|
||||
this.messageEventListener = null;
|
||||
this.onDestroy = new EventTarget();
|
||||
this.messengerId = this.generateUniqueId();
|
||||
this.messageEventListener = this.createMessageEventListener();
|
||||
this.broadcastChannel.addEventListener(this.messageEventListener);
|
||||
}
|
||||
/**
|
||||
* Sends a request to the content script and returns the response.
|
||||
* AbortController signals will be forwarded to the content script.
|
||||
*
|
||||
* @param request data to send to the content script
|
||||
* @param abortSignal the abort controller that might be used to abort the request
|
||||
* @returns the response from the content script
|
||||
*/
|
||||
request(request, abortSignal) {
|
||||
return messenger_awaiter(this, void 0, void 0, function* () {
|
||||
const requestChannel = new MessageChannel();
|
||||
const { port1: localPort, port2: remotePort } = requestChannel;
|
||||
try {
|
||||
const promise = new Promise((resolve) => {
|
||||
localPort.onmessage = (event) => resolve(event.data);
|
||||
});
|
||||
const abortListener = () => localPort.postMessage({
|
||||
metadata: { SENDER },
|
||||
type: MessageTypes.AbortRequest,
|
||||
});
|
||||
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener("abort", abortListener);
|
||||
this.broadcastChannel.postMessage(Object.assign(Object.assign({}, request), { SENDER, senderId: this.messengerId }), remotePort);
|
||||
const response = yield promise;
|
||||
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener("abort", abortListener);
|
||||
if (response.type === MessageTypes.ErrorResponse) {
|
||||
const error = new Error();
|
||||
Object.assign(error, JSON.parse(response.error));
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
finally {
|
||||
localPort.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
createMessageEventListener() {
|
||||
return (event) => messenger_awaiter(this, void 0, void 0, function* () {
|
||||
var _a;
|
||||
const windowOrigin = window.location.origin;
|
||||
if (event.origin !== windowOrigin || !this.handler) {
|
||||
return;
|
||||
}
|
||||
const message = event.data;
|
||||
const port = (_a = event.ports) === null || _a === void 0 ? void 0 : _a[0];
|
||||
if ((message === null || message === void 0 ? void 0 : message.SENDER) !== SENDER || message.senderId == this.messengerId || port == null) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
port.onmessage = (event) => {
|
||||
if (event.data.type === MessageTypes.AbortRequest) {
|
||||
abortController.abort();
|
||||
}
|
||||
};
|
||||
const onDestroyListener = () => abortController.abort();
|
||||
this.onDestroy.addEventListener("destroy", onDestroyListener);
|
||||
try {
|
||||
const handlerResponse = yield this.handler(message, abortController);
|
||||
port.postMessage(Object.assign(Object.assign({}, handlerResponse), { SENDER }));
|
||||
}
|
||||
catch (error) {
|
||||
port.postMessage({
|
||||
SENDER,
|
||||
type: MessageTypes.ErrorResponse,
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.onDestroy.removeEventListener("destroy", onDestroyListener);
|
||||
port.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Cleans up the messenger by removing the message event listener
|
||||
*/
|
||||
destroy() {
|
||||
return messenger_awaiter(this, void 0, void 0, function* () {
|
||||
this.onDestroy.dispatchEvent(new Event("destroy"));
|
||||
if (this.messageEventListener) {
|
||||
yield this.sendDisconnectCommand();
|
||||
this.broadcastChannel.removeEventListener(this.messageEventListener);
|
||||
this.messageEventListener = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
sendDisconnectCommand() {
|
||||
return messenger_awaiter(this, void 0, void 0, function* () {
|
||||
yield this.request({ type: MessageTypes.DisconnectRequest });
|
||||
});
|
||||
}
|
||||
generateUniqueId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
;// ./src/autofill/fido2/content/fido2-content-script.ts
|
||||
var fido2_content_script_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
(function (globalContext) {
|
||||
const shouldExecuteContentScript = globalContext.document.contentType === "text/html" &&
|
||||
(globalContext.document.location.protocol === "https:" ||
|
||||
(globalContext.document.location.protocol === "http:" &&
|
||||
globalContext.document.location.hostname === "localhost"));
|
||||
if (!shouldExecuteContentScript) {
|
||||
return;
|
||||
}
|
||||
// Initialization logic, set up the messenger and connect a port to the background script.
|
||||
const messenger = Messenger.forDOMCommunication(globalContext.window);
|
||||
messenger.handler = handleFido2Message;
|
||||
const port = chrome.runtime.connect({ name: Fido2PortName.InjectedScript });
|
||||
port.onDisconnect.addListener(handlePortOnDisconnect);
|
||||
/**
|
||||
* Handles FIDO2 credential requests and returns the result.
|
||||
*
|
||||
* @param message - The message to handle.
|
||||
* @param abortController - The abort controller used to handle exit conditions from the FIDO2 request.
|
||||
*/
|
||||
function handleFido2Message(message, abortController) {
|
||||
return fido2_content_script_awaiter(this, void 0, void 0, function* () {
|
||||
const requestId = Date.now().toString();
|
||||
const abortHandler = () => sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId });
|
||||
abortController.signal.addEventListener("abort", abortHandler);
|
||||
try {
|
||||
if (message.type === MessageTypes.CredentialCreationRequest) {
|
||||
return handleCredentialCreationRequestMessage(requestId, message.data);
|
||||
}
|
||||
if (message.type === MessageTypes.CredentialGetRequest) {
|
||||
return handleCredentialGetRequestMessage(requestId, message.data);
|
||||
}
|
||||
if (message.type === MessageTypes.AbortRequest) {
|
||||
return sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId });
|
||||
}
|
||||
}
|
||||
finally {
|
||||
abortController.signal.removeEventListener("abort", abortHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles the credential creation request message and returns the result.
|
||||
*
|
||||
* @param requestId - The request ID of the message.
|
||||
* @param data - Data associated with the credential request.
|
||||
*/
|
||||
function handleCredentialCreationRequestMessage(requestId, data) {
|
||||
return fido2_content_script_awaiter(this, void 0, void 0, function* () {
|
||||
return respondToCredentialRequest("fido2RegisterCredentialRequest", MessageTypes.CredentialCreationResponse, requestId, data);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles the credential get request message and returns the result.
|
||||
*
|
||||
* @param requestId - The request ID of the message.
|
||||
* @param data - Data associated with the credential request.
|
||||
*/
|
||||
function handleCredentialGetRequestMessage(requestId, data) {
|
||||
return fido2_content_script_awaiter(this, void 0, void 0, function* () {
|
||||
return respondToCredentialRequest("fido2GetCredentialRequest", MessageTypes.CredentialGetResponse, requestId, data);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Sends a message to the extension to handle the
|
||||
* credential request and returns the result.
|
||||
*
|
||||
* @param command - The command to send to the extension.
|
||||
* @param type - The type of message, either CredentialCreationResponse or CredentialGetResponse.
|
||||
* @param requestId - The request ID of the message.
|
||||
* @param messageData - Data associated with the credential request.
|
||||
*/
|
||||
function respondToCredentialRequest(command, type, requestId, messageData) {
|
||||
return fido2_content_script_awaiter(this, void 0, void 0, function* () {
|
||||
const data = Object.assign(Object.assign({}, messageData), { origin: globalContext.location.origin, sameOriginWithAncestors: globalContext.self === globalContext.top });
|
||||
const result = yield sendExtensionMessage(command, { data, requestId });
|
||||
if (result && result.error !== undefined) {
|
||||
return Promise.reject(result.error);
|
||||
}
|
||||
return Promise.resolve({ type, result });
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Handles the disconnect event of the port. Calls
|
||||
* to the messenger to destroy and tear down the
|
||||
* implemented page-script.js logic.
|
||||
*/
|
||||
function handlePortOnDisconnect() {
|
||||
void messenger.destroy();
|
||||
}
|
||||
})(globalThis);
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,640 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ../../libs/common/src/platform/services/fido2/fido2-utils.ts
|
||||
// @ts-strict-ignore
|
||||
class Fido2Utils {
|
||||
static createResultToJson(result) {
|
||||
return {
|
||||
id: result.credentialId,
|
||||
rawId: result.credentialId,
|
||||
response: {
|
||||
clientDataJSON: result.clientDataJSON,
|
||||
authenticatorData: result.authData,
|
||||
transports: result.transports,
|
||||
publicKey: result.publicKey,
|
||||
publicKeyAlgorithm: result.publicKeyAlgorithm,
|
||||
attestationObject: result.attestationObject,
|
||||
},
|
||||
authenticatorAttachment: "platform",
|
||||
clientExtensionResults: result.extensions,
|
||||
type: "public-key",
|
||||
};
|
||||
}
|
||||
static getResultToJson(result) {
|
||||
return {
|
||||
id: result.credentialId,
|
||||
rawId: result.credentialId,
|
||||
response: {
|
||||
clientDataJSON: result.clientDataJSON,
|
||||
authenticatorData: result.authenticatorData,
|
||||
signature: result.signature,
|
||||
userHandle: result.userHandle,
|
||||
},
|
||||
authenticatorAttachment: "platform",
|
||||
clientExtensionResults: {},
|
||||
type: "public-key",
|
||||
};
|
||||
}
|
||||
static bufferToString(bufferSource) {
|
||||
return Fido2Utils.fromBufferToB64(Fido2Utils.bufferSourceToUint8Array(bufferSource))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
}
|
||||
static stringToBuffer(str) {
|
||||
return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)).buffer;
|
||||
}
|
||||
static bufferSourceToUint8Array(bufferSource) {
|
||||
if (Fido2Utils.isArrayBuffer(bufferSource)) {
|
||||
return new Uint8Array(bufferSource);
|
||||
}
|
||||
else {
|
||||
return new Uint8Array(bufferSource.buffer, bufferSource.byteOffset, bufferSource.byteLength);
|
||||
}
|
||||
}
|
||||
/** Utility function to identify type of bufferSource. Necessary because of differences between runtimes */
|
||||
static isArrayBuffer(bufferSource) {
|
||||
return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined;
|
||||
}
|
||||
static fromB64toUrlB64(b64Str) {
|
||||
return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
}
|
||||
static fromBufferToB64(buffer) {
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
let binary = "";
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return globalThis.btoa(binary);
|
||||
}
|
||||
static fromB64ToArray(str) {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
const binaryString = globalThis.atob(str);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
static fromUrlB64ToB64(urlB64Str) {
|
||||
let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
switch (output.length % 4) {
|
||||
case 0:
|
||||
break;
|
||||
case 2:
|
||||
output += "==";
|
||||
break;
|
||||
case 3:
|
||||
output += "=";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Illegal base64url string!");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
/**
|
||||
* This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle
|
||||
* @param userHandle
|
||||
*/
|
||||
static cipherHasNoOtherPasskeys(cipher, userHandle) {
|
||||
if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle);
|
||||
}
|
||||
}
|
||||
|
||||
;// ./src/autofill/fido2/utils/webauthn-utils.ts
|
||||
|
||||
class WebauthnUtils {
|
||||
static mapCredentialCreationOptions(options, fallbackSupported) {
|
||||
var _a, _b, _c, _d, _e;
|
||||
const keyOptions = options.publicKey;
|
||||
if (keyOptions == undefined) {
|
||||
throw new Error("Public-key options not found");
|
||||
}
|
||||
return {
|
||||
attestation: keyOptions.attestation,
|
||||
authenticatorSelection: {
|
||||
requireResidentKey: (_a = keyOptions.authenticatorSelection) === null || _a === void 0 ? void 0 : _a.requireResidentKey,
|
||||
residentKey: (_b = keyOptions.authenticatorSelection) === null || _b === void 0 ? void 0 : _b.residentKey,
|
||||
userVerification: (_c = keyOptions.authenticatorSelection) === null || _c === void 0 ? void 0 : _c.userVerification,
|
||||
},
|
||||
challenge: Fido2Utils.bufferToString(keyOptions.challenge),
|
||||
excludeCredentials: (_d = keyOptions.excludeCredentials) === null || _d === void 0 ? void 0 : _d.map((credential) => ({
|
||||
id: Fido2Utils.bufferToString(credential.id),
|
||||
transports: credential.transports,
|
||||
type: credential.type,
|
||||
})),
|
||||
extensions: {
|
||||
credProps: (_e = keyOptions.extensions) === null || _e === void 0 ? void 0 : _e.credProps,
|
||||
},
|
||||
pubKeyCredParams: keyOptions.pubKeyCredParams
|
||||
.map((params) => ({
|
||||
// Fix for spec-deviation: Sites using KeycloakJS send `kp.alg` as a string
|
||||
alg: Number(params.alg),
|
||||
type: params.type,
|
||||
}))
|
||||
.filter((params) => !isNaN(params.alg)),
|
||||
rp: {
|
||||
id: keyOptions.rp.id,
|
||||
name: keyOptions.rp.name,
|
||||
},
|
||||
user: {
|
||||
id: Fido2Utils.bufferToString(keyOptions.user.id),
|
||||
displayName: keyOptions.user.displayName,
|
||||
name: keyOptions.user.name,
|
||||
},
|
||||
timeout: keyOptions.timeout,
|
||||
fallbackSupported,
|
||||
};
|
||||
}
|
||||
static mapCredentialRegistrationResult(result) {
|
||||
const credential = {
|
||||
id: result.credentialId,
|
||||
rawId: Fido2Utils.stringToBuffer(result.credentialId),
|
||||
type: "public-key",
|
||||
authenticatorAttachment: "platform",
|
||||
response: {
|
||||
clientDataJSON: Fido2Utils.stringToBuffer(result.clientDataJSON),
|
||||
attestationObject: Fido2Utils.stringToBuffer(result.attestationObject),
|
||||
getAuthenticatorData() {
|
||||
return Fido2Utils.stringToBuffer(result.authData);
|
||||
},
|
||||
getPublicKey() {
|
||||
return Fido2Utils.stringToBuffer(result.publicKey);
|
||||
},
|
||||
getPublicKeyAlgorithm() {
|
||||
return result.publicKeyAlgorithm;
|
||||
},
|
||||
getTransports() {
|
||||
return result.transports;
|
||||
},
|
||||
},
|
||||
getClientExtensionResults: () => ({
|
||||
credProps: result.extensions.credProps,
|
||||
}),
|
||||
toJSON: () => Fido2Utils.createResultToJson(result),
|
||||
};
|
||||
// Modify prototype chains to fix `instanceof` calls.
|
||||
// This makes these objects indistinguishable from the native classes.
|
||||
// Unfortunately PublicKeyCredential does not have a javascript constructor so `extends` does not work here.
|
||||
Object.setPrototypeOf(credential.response, AuthenticatorAttestationResponse.prototype);
|
||||
Object.setPrototypeOf(credential, PublicKeyCredential.prototype);
|
||||
return credential;
|
||||
}
|
||||
static mapCredentialRequestOptions(options, fallbackSupported) {
|
||||
var _a, _b;
|
||||
const keyOptions = options.publicKey;
|
||||
if (keyOptions == undefined) {
|
||||
throw new Error("Public-key options not found");
|
||||
}
|
||||
return {
|
||||
allowedCredentialIds: (_b = (_a = keyOptions.allowCredentials) === null || _a === void 0 ? void 0 : _a.map((c) => Fido2Utils.bufferToString(c.id))) !== null && _b !== void 0 ? _b : [],
|
||||
challenge: Fido2Utils.bufferToString(keyOptions.challenge),
|
||||
rpId: keyOptions.rpId,
|
||||
userVerification: keyOptions.userVerification,
|
||||
timeout: keyOptions.timeout,
|
||||
mediation: options.mediation,
|
||||
fallbackSupported,
|
||||
};
|
||||
}
|
||||
static mapCredentialAssertResult(result) {
|
||||
const credential = {
|
||||
id: result.credentialId,
|
||||
rawId: Fido2Utils.stringToBuffer(result.credentialId),
|
||||
type: "public-key",
|
||||
response: {
|
||||
authenticatorData: Fido2Utils.stringToBuffer(result.authenticatorData),
|
||||
clientDataJSON: Fido2Utils.stringToBuffer(result.clientDataJSON),
|
||||
signature: Fido2Utils.stringToBuffer(result.signature),
|
||||
userHandle: Fido2Utils.stringToBuffer(result.userHandle),
|
||||
},
|
||||
getClientExtensionResults: () => ({}),
|
||||
authenticatorAttachment: "platform",
|
||||
toJSON: () => Fido2Utils.getResultToJson(result),
|
||||
};
|
||||
// Modify prototype chains to fix `instanceof` calls.
|
||||
// This makes these objects indistinguishable from the native classes.
|
||||
// Unfortunately PublicKeyCredential does not have a javascript constructor so `extends` does not work here.
|
||||
Object.setPrototypeOf(credential.response, AuthenticatorAssertionResponse.prototype);
|
||||
Object.setPrototypeOf(credential, PublicKeyCredential.prototype);
|
||||
return credential;
|
||||
}
|
||||
}
|
||||
|
||||
;// ./src/autofill/fido2/content/messaging/message.ts
|
||||
const MessageTypes = {
|
||||
CredentialCreationRequest: 0,
|
||||
CredentialCreationResponse: 1,
|
||||
CredentialGetRequest: 2,
|
||||
CredentialGetResponse: 3,
|
||||
AbortRequest: 4,
|
||||
DisconnectRequest: 5,
|
||||
ReconnectRequest: 6,
|
||||
AbortResponse: 7,
|
||||
ErrorResponse: 8,
|
||||
};
|
||||
|
||||
;// ./src/autofill/fido2/content/messaging/messenger.ts
|
||||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
const SENDER = "bitwarden-webauthn";
|
||||
/**
|
||||
* A class that handles communication between the page and content script. It converts
|
||||
* the browser's broadcasting API into a request/response API with support for seamlessly
|
||||
* handling aborts and exceptions across separate execution contexts.
|
||||
*/
|
||||
class Messenger {
|
||||
/**
|
||||
* Creates a messenger that uses the browser's `window.postMessage` API to initiate
|
||||
* requests in the content script. Every request will then create it's own
|
||||
* `MessageChannel` through which all subsequent communication will be sent through.
|
||||
*
|
||||
* @param window the window object to use for communication
|
||||
* @returns a `Messenger` instance
|
||||
*/
|
||||
static forDOMCommunication(window) {
|
||||
const windowOrigin = window.location.origin;
|
||||
return new Messenger({
|
||||
postMessage: (message, port) => window.postMessage(message, windowOrigin, [port]),
|
||||
addEventListener: (listener) => window.addEventListener("message", listener),
|
||||
removeEventListener: (listener) => window.removeEventListener("message", listener),
|
||||
});
|
||||
}
|
||||
constructor(broadcastChannel) {
|
||||
this.broadcastChannel = broadcastChannel;
|
||||
this.messageEventListener = null;
|
||||
this.onDestroy = new EventTarget();
|
||||
this.messengerId = this.generateUniqueId();
|
||||
this.messageEventListener = this.createMessageEventListener();
|
||||
this.broadcastChannel.addEventListener(this.messageEventListener);
|
||||
}
|
||||
/**
|
||||
* Sends a request to the content script and returns the response.
|
||||
* AbortController signals will be forwarded to the content script.
|
||||
*
|
||||
* @param request data to send to the content script
|
||||
* @param abortSignal the abort controller that might be used to abort the request
|
||||
* @returns the response from the content script
|
||||
*/
|
||||
request(request, abortSignal) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const requestChannel = new MessageChannel();
|
||||
const { port1: localPort, port2: remotePort } = requestChannel;
|
||||
try {
|
||||
const promise = new Promise((resolve) => {
|
||||
localPort.onmessage = (event) => resolve(event.data);
|
||||
});
|
||||
const abortListener = () => localPort.postMessage({
|
||||
metadata: { SENDER },
|
||||
type: MessageTypes.AbortRequest,
|
||||
});
|
||||
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener("abort", abortListener);
|
||||
this.broadcastChannel.postMessage(Object.assign(Object.assign({}, request), { SENDER, senderId: this.messengerId }), remotePort);
|
||||
const response = yield promise;
|
||||
abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener("abort", abortListener);
|
||||
if (response.type === MessageTypes.ErrorResponse) {
|
||||
const error = new Error();
|
||||
Object.assign(error, JSON.parse(response.error));
|
||||
throw error;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
finally {
|
||||
localPort.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
createMessageEventListener() {
|
||||
return (event) => __awaiter(this, void 0, void 0, function* () {
|
||||
var _a;
|
||||
const windowOrigin = window.location.origin;
|
||||
if (event.origin !== windowOrigin || !this.handler) {
|
||||
return;
|
||||
}
|
||||
const message = event.data;
|
||||
const port = (_a = event.ports) === null || _a === void 0 ? void 0 : _a[0];
|
||||
if ((message === null || message === void 0 ? void 0 : message.SENDER) !== SENDER || message.senderId == this.messengerId || port == null) {
|
||||
return;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
port.onmessage = (event) => {
|
||||
if (event.data.type === MessageTypes.AbortRequest) {
|
||||
abortController.abort();
|
||||
}
|
||||
};
|
||||
const onDestroyListener = () => abortController.abort();
|
||||
this.onDestroy.addEventListener("destroy", onDestroyListener);
|
||||
try {
|
||||
const handlerResponse = yield this.handler(message, abortController);
|
||||
port.postMessage(Object.assign(Object.assign({}, handlerResponse), { SENDER }));
|
||||
}
|
||||
catch (error) {
|
||||
port.postMessage({
|
||||
SENDER,
|
||||
type: MessageTypes.ErrorResponse,
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error)),
|
||||
});
|
||||
}
|
||||
finally {
|
||||
this.onDestroy.removeEventListener("destroy", onDestroyListener);
|
||||
port.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Cleans up the messenger by removing the message event listener
|
||||
*/
|
||||
destroy() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
this.onDestroy.dispatchEvent(new Event("destroy"));
|
||||
if (this.messageEventListener) {
|
||||
yield this.sendDisconnectCommand();
|
||||
this.broadcastChannel.removeEventListener(this.messageEventListener);
|
||||
this.messageEventListener = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
sendDisconnectCommand() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield this.request({ type: MessageTypes.DisconnectRequest });
|
||||
});
|
||||
}
|
||||
generateUniqueId() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
}
|
||||
}
|
||||
|
||||
;// ./src/autofill/fido2/content/fido2-page-script.ts
|
||||
var fido2_page_script_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
|
||||
|
||||
(function (globalContext) {
|
||||
if (globalContext.document.currentScript) {
|
||||
globalContext.document.currentScript.parentNode.removeChild(globalContext.document.currentScript);
|
||||
}
|
||||
const shouldExecuteContentScript = globalContext.document.contentType === "text/html" &&
|
||||
(globalContext.document.location.protocol === "https:" ||
|
||||
(globalContext.document.location.protocol === "http:" &&
|
||||
globalContext.document.location.hostname === "localhost"));
|
||||
if (!shouldExecuteContentScript) {
|
||||
return;
|
||||
}
|
||||
const BrowserPublicKeyCredential = globalContext.PublicKeyCredential;
|
||||
const BrowserNavigatorCredentials = navigator.credentials;
|
||||
const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse;
|
||||
const browserNativeWebauthnSupport = globalContext.PublicKeyCredential != undefined;
|
||||
let browserNativeWebauthnPlatformAuthenticatorSupport = false;
|
||||
if (!browserNativeWebauthnSupport) {
|
||||
// Polyfill webauthn support
|
||||
try {
|
||||
// credentials are read-only if supported, use type-casting to force assignment
|
||||
navigator.credentials = {
|
||||
create() {
|
||||
return fido2_page_script_awaiter(this, void 0, void 0, function* () {
|
||||
throw new Error("Webauthn not supported in this browser.");
|
||||
});
|
||||
},
|
||||
get() {
|
||||
return fido2_page_script_awaiter(this, void 0, void 0, function* () {
|
||||
throw new Error("Webauthn not supported in this browser.");
|
||||
});
|
||||
},
|
||||
};
|
||||
globalContext.PublicKeyCredential = class PolyfillPublicKeyCredential {
|
||||
static isUserVerifyingPlatformAuthenticatorAvailable() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
};
|
||||
globalContext.AuthenticatorAttestationResponse =
|
||||
class PolyfillAuthenticatorAttestationResponse {
|
||||
};
|
||||
}
|
||||
catch (_a) {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
else {
|
||||
void BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then((available) => {
|
||||
browserNativeWebauthnPlatformAuthenticatorSupport = available;
|
||||
if (!available) {
|
||||
// Polyfill platform authenticator support
|
||||
globalContext.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
const browserCredentials = {
|
||||
create: navigator.credentials.create.bind(navigator.credentials),
|
||||
get: navigator.credentials.get.bind(navigator.credentials),
|
||||
};
|
||||
const messenger = Messenger.forDOMCommunication(window);
|
||||
let waitForFocusTimeout;
|
||||
let focusListenerHandler;
|
||||
navigator.credentials.create = createWebAuthnCredential;
|
||||
navigator.credentials.get = getWebAuthnCredential;
|
||||
/**
|
||||
* Creates a new webauthn credential.
|
||||
*
|
||||
* @param options Options for creating new credentials.
|
||||
* @returns Promise that resolves to the new credential object.
|
||||
*/
|
||||
function createWebAuthnCredential(options) {
|
||||
return fido2_page_script_awaiter(this, void 0, void 0, function* () {
|
||||
var _a, _b;
|
||||
if (!isWebauthnCall(options)) {
|
||||
return yield browserCredentials.create(options);
|
||||
}
|
||||
const authenticatorAttachmentIsPlatform = ((_b = (_a = options === null || options === void 0 ? void 0 : options.publicKey) === null || _a === void 0 ? void 0 : _a.authenticatorSelection) === null || _b === void 0 ? void 0 : _b.authenticatorAttachment) === "platform";
|
||||
const fallbackSupported = (authenticatorAttachmentIsPlatform && browserNativeWebauthnPlatformAuthenticatorSupport) ||
|
||||
(!authenticatorAttachmentIsPlatform && browserNativeWebauthnSupport);
|
||||
try {
|
||||
const response = yield messenger.request({
|
||||
type: MessageTypes.CredentialCreationRequest,
|
||||
data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported),
|
||||
}, options === null || options === void 0 ? void 0 : options.signal);
|
||||
if (response.type !== MessageTypes.CredentialCreationResponse) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
return WebauthnUtils.mapCredentialRegistrationResult(response.result);
|
||||
}
|
||||
catch (error) {
|
||||
if (error && error.fallbackRequested && fallbackSupported) {
|
||||
yield waitForFocus();
|
||||
return yield browserCredentials.create(options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Retrieves a webauthn credential.
|
||||
*
|
||||
* @param options Options for creating new credentials.
|
||||
* @returns Promise that resolves to the new credential object.
|
||||
*/
|
||||
function getWebAuthnCredential(options) {
|
||||
return fido2_page_script_awaiter(this, void 0, void 0, function* () {
|
||||
if (!isWebauthnCall(options)) {
|
||||
return yield browserCredentials.get(options);
|
||||
}
|
||||
const abortSignal = (options === null || options === void 0 ? void 0 : options.signal) || new AbortController().signal;
|
||||
const fallbackSupported = browserNativeWebauthnSupport;
|
||||
if ((options === null || options === void 0 ? void 0 : options.mediation) && options.mediation === "conditional") {
|
||||
const internalAbortControllers = [new AbortController(), new AbortController()];
|
||||
const bitwardenResponse = (internalAbortController) => fido2_page_script_awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
const abortListener = () => messenger.request({
|
||||
type: MessageTypes.AbortRequest,
|
||||
abortedRequestId: abortSignal.toString(),
|
||||
});
|
||||
internalAbortController.signal.addEventListener("abort", abortListener);
|
||||
const response = yield messenger.request({
|
||||
type: MessageTypes.CredentialGetRequest,
|
||||
data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported),
|
||||
}, internalAbortController.signal);
|
||||
internalAbortController.signal.removeEventListener("abort", abortListener);
|
||||
if (response.type !== MessageTypes.CredentialGetResponse) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
return WebauthnUtils.mapCredentialAssertResult(response.result);
|
||||
}
|
||||
catch (_a) {
|
||||
// Ignoring error
|
||||
}
|
||||
});
|
||||
const browserResponse = (internalAbortController) => browserCredentials.get(Object.assign(Object.assign({}, options), { signal: internalAbortController.signal }));
|
||||
const abortListener = () => {
|
||||
internalAbortControllers.forEach((controller) => controller.abort());
|
||||
};
|
||||
abortSignal.addEventListener("abort", abortListener);
|
||||
const response = yield Promise.race([
|
||||
bitwardenResponse(internalAbortControllers[0]),
|
||||
browserResponse(internalAbortControllers[1]),
|
||||
]);
|
||||
abortSignal.removeEventListener("abort", abortListener);
|
||||
internalAbortControllers.forEach((controller) => controller.abort());
|
||||
return response;
|
||||
}
|
||||
try {
|
||||
const response = yield messenger.request({
|
||||
type: MessageTypes.CredentialGetRequest,
|
||||
data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported),
|
||||
}, options === null || options === void 0 ? void 0 : options.signal);
|
||||
if (response.type !== MessageTypes.CredentialGetResponse) {
|
||||
throw new Error("Something went wrong.");
|
||||
}
|
||||
return WebauthnUtils.mapCredentialAssertResult(response.result);
|
||||
}
|
||||
catch (error) {
|
||||
if (error && error.fallbackRequested && fallbackSupported) {
|
||||
yield waitForFocus();
|
||||
return yield browserCredentials.get(options);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
function isWebauthnCall(options) {
|
||||
return options && "publicKey" in options;
|
||||
}
|
||||
/**
|
||||
* Wait for window to be focused.
|
||||
* Safari doesn't allow scripts to trigger webauthn when window is not focused.
|
||||
*
|
||||
* @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms.
|
||||
* @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes.
|
||||
* @returns Promise that resolves when window is focused, or rejects if timeout is reached.
|
||||
*/
|
||||
function waitForFocus() {
|
||||
return fido2_page_script_awaiter(this, arguments, void 0, function* (fallbackWait = 500, timeout = 5 * 60 * 1000) {
|
||||
try {
|
||||
if (globalContext.top.document.hasFocus()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (_a) {
|
||||
// Cannot access window.top due to cross-origin frame, fallback to waiting
|
||||
return yield new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait));
|
||||
}
|
||||
const focusPromise = new Promise((resolve) => {
|
||||
focusListenerHandler = () => resolve();
|
||||
globalContext.top.addEventListener("focus", focusListenerHandler);
|
||||
});
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
waitForFocusTimeout = globalContext.setTimeout(() => reject(new DOMException("The operation either timed out or was not allowed.", "AbortError")), timeout);
|
||||
});
|
||||
try {
|
||||
yield Promise.race([focusPromise, timeoutPromise]);
|
||||
}
|
||||
finally {
|
||||
clearWaitForFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
function clearWaitForFocus() {
|
||||
globalContext.top.removeEventListener("focus", focusListenerHandler);
|
||||
if (waitForFocusTimeout) {
|
||||
globalContext.clearTimeout(waitForFocusTimeout);
|
||||
}
|
||||
}
|
||||
function destroy() {
|
||||
try {
|
||||
if (browserNativeWebauthnSupport) {
|
||||
navigator.credentials.create = browserCredentials.create;
|
||||
navigator.credentials.get = browserCredentials.get;
|
||||
}
|
||||
else {
|
||||
navigator.credentials = BrowserNavigatorCredentials;
|
||||
globalContext.PublicKeyCredential = BrowserPublicKeyCredential;
|
||||
globalContext.AuthenticatorAttestationResponse = BrowserAuthenticatorAttestationResponse;
|
||||
}
|
||||
clearWaitForFocus();
|
||||
void messenger.destroy();
|
||||
}
|
||||
catch (_a) {
|
||||
/** empty */
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets up a listener to handle cleanup or reconnection when the extension's
|
||||
* context changes due to being reloaded or unloaded.
|
||||
*/
|
||||
messenger.handler = (message) => {
|
||||
const type = message.type;
|
||||
// Handle cleanup for disconnect request
|
||||
if (type === MessageTypes.DisconnectRequest) {
|
||||
destroy();
|
||||
}
|
||||
};
|
||||
})(globalThis);
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,52 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
// UNUSED EXPORTS: sendExtensionMessage
|
||||
|
||||
;// ../../libs/common/src/platform/ipc/ipc-message.ts
|
||||
function isIpcMessage(message) {
|
||||
return message.type === "bitwarden-ipc-message";
|
||||
}
|
||||
|
||||
;// ./src/platform/ipc/content/ipc-content-script.ts
|
||||
// TODO: This content script should be dynamically reloaded when the extension is updated,
|
||||
// to avoid "Extension context invalidated." errors.
|
||||
|
||||
// Web -> Background
|
||||
function sendExtensionMessage(message) {
|
||||
if (typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.sendMessage !== "undefined") {
|
||||
void browser.runtime.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
void chrome.runtime.sendMessage(message);
|
||||
}
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return;
|
||||
}
|
||||
if (isIpcMessage(event.data)) {
|
||||
sendExtensionMessage(event.data);
|
||||
}
|
||||
});
|
||||
// Background -> Web
|
||||
function setupMessageListener() {
|
||||
function listener(message) {
|
||||
if (isIpcMessage(message)) {
|
||||
void window.postMessage(message);
|
||||
}
|
||||
}
|
||||
if (typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.onMessage !== "undefined") {
|
||||
browser.runtime.onMessage.addListener(listener);
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax -- This doesn't run in the popup but in the content script
|
||||
chrome.runtime.onMessage.addListener(listener);
|
||||
}
|
||||
setupMessageListener();
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,22 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ../../libs/common/src/vault/enums/vault-messages.enum.ts
|
||||
const VaultMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
/** @deprecated use {@link OpenBrowserExtensionToUrl} */
|
||||
OpenAtRiskPasswords: "openAtRiskPasswords",
|
||||
OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl",
|
||||
PopupOpened: "popupOpened",
|
||||
};
|
||||
|
||||
|
||||
;// ./src/vault/content/send-on-installed-message.ts
|
||||
|
||||
(function (globalContext) {
|
||||
globalContext.postMessage({ command: VaultMessages.HasBwInstalled });
|
||||
})(window);
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,23 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
/******/ "use strict";
|
||||
|
||||
;// ../../libs/common/src/vault/enums/vault-messages.enum.ts
|
||||
const VaultMessages = {
|
||||
HasBwInstalled: "hasBwInstalled",
|
||||
checkBwInstalled: "checkIfBWExtensionInstalled",
|
||||
/** @deprecated use {@link OpenBrowserExtensionToUrl} */
|
||||
OpenAtRiskPasswords: "openAtRiskPasswords",
|
||||
OpenBrowserExtensionToUrl: "openBrowserExtensionToUrl",
|
||||
PopupOpened: "popupOpened",
|
||||
};
|
||||
|
||||
|
||||
;// ./src/vault/content/send-popup-open-message.ts
|
||||
|
||||
(function (globalContext) {
|
||||
// Send a message to the window that the popup opened
|
||||
globalContext.postMessage({ command: VaultMessages.PopupOpened });
|
||||
})(window);
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
@@ -0,0 +1,7 @@
|
||||
/******/ (function() { // webpackBootstrap
|
||||
(function () {
|
||||
void chrome.runtime.sendMessage({ command: "triggerAutofillScriptInjection" });
|
||||
})();
|
||||
|
||||
/******/ })()
|
||||
;
|
||||
Reference in New Issue
Block a user