1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

[EC-183] Mono Repository - Browser (#2531)

This commit is contained in:
Oscar Hinton
2022-05-03 19:38:55 +02:00
committed by GitHub
parent 11ef07fbce
commit 7fe51f83da
381 changed files with 3 additions and 3 deletions

View File

@@ -0,0 +1,15 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Set default charset
[*.{js,ts,scss,html}]
charset = utf-8
indent_style = space
indent_size = 2

View File

@@ -0,0 +1,9 @@
**/build
jslib
webpack.config.js
karma.conf.js
gulpfile.js
src/content/autofill.js
src/scripts/duo.js
**/node_modules

View File

@@ -0,0 +1,32 @@
{
"root": true,
"env": {
"browser": true,
"webextensions": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

21
apps/browser/.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
.vs
.vscode
.idea
.DS_Store
node_modules
npm-debug.log
vwd.webinfo
css/
dist/
dist-safari/
webfonts/
*.crx
*.pem
*.zip
build/
build.safariextension/
coverage/
xcuserdata/
*.hmap
!src/safari/safari/app/popup/index.html
src/safari/safari/app/

1
apps/browser/.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -0,0 +1,15 @@
# Build directories
build
dist
coverage
jslib
# External libraries / auto synced locales
src/_locales
src/scripts/duo.js
src/content/autofill.js
src/safari
# Github Workflows
.github/workflows

View File

@@ -0,0 +1,3 @@
{
"printWidth": 100
}

88
apps/browser/README.md Normal file
View File

@@ -0,0 +1,88 @@
[![Github Workflow build on master](https://github.com/bitwarden/browser/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/bitwarden/browser/actions/workflows/build.yml?query=branch:master)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/bitwarden-browser/localized.svg)](https://crowdin.com/project/bitwarden-browser)
[![Join the chat at https://gitter.im/bitwarden/Lobby](https://badges.gitter.im/bitwarden/Lobby.svg)](https://gitter.im/bitwarden/Lobby)
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
# Bitwarden Browser Extension
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/3C4iKO0.png" width="64" height="64"></a>
<a href="https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/" target="_blank"><img src="https://imgur.com/ihXsdDO.png" width="64" height="64"></a>
<a href="https://microsoftedge.microsoft.com/addons/detail/bitwarden-free-password/jbkfoedolllekgbhcbcoahefnbanhhlh" target="_blank"><img src="https://imgur.com/vMcaXaw.png" width="64" height="64"></a>
<a href="https://addons.opera.com/extensions/details/bitwarden-free-password-manager/" target="_blank"><img src="https://imgur.com/nSJ9htU.png" width="64" height="64"></a>
<a href="https://bitwarden.com/download/" target="_blank"><img src="https://imgur.com/ENbaWUu.png" width="64" height="64"></a>
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/EuDp4vP.png" width="64" height="64"></a>
<a href="https://chrome.google.com/webstore/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb" target="_blank"><img src="https://imgur.com/z8yjLZ2.png" width="64" height="64"></a>
<a href="https://addons.mozilla.org/firefox/addon/bitwarden-password-manager/" target="_blank"><img src="https://imgur.com/MQYBSrD.png" width="64" height="64"></a>
The Bitwarden browser extension is written using the Web Extension API and Angular.
![](https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/browser-chrome.png "My Vault")
# Build/Run
**Requirements**
- [Node.js](https://nodejs.org) v16.13.1 or greater
- NPM v8
- [Gulp](https://gulpjs.com/) (`npm install --global gulp-cli`)
- Chrome (preferred), Opera, or Firefox browser
**Run the app**
```
npm install
npm run build:watch
```
You can now load the extension into your browser through the browser's extension tools page:
- Chrome/Opera:
1. Type `chrome://extensions` in your address bar to bring up the extensions page.
2. Enable developer mode (toggle switch)
3. Click the "Load unpacked extension" button, navigate to the `build` folder of your local extension instance, and click "Ok".
- Firefox
1. Type `about:debugging` in your address bar to bring up the add-ons page.
2. Click the `Load Temporary Add-on` button, navigate to the `build/manifest.json` file, and "Open".
**Desktop communication**
Native Messaging (communication between the desktop application and browser extension) works by having the browser start a lightweight proxy baked into our desktop application.
Out of the box, the desktop application can only communicate with the production browser extension. When you enable browser integration in the desktop application, the application generates manifests which contain the production IDs of the browser extensions. To enable communication between the desktop application and development versions of browser extensions, add the development IDs to the `allowed_extensions` section of the corresponding manifests.
Manifests are located in the `browser` subdirectory of the Bitwarden configuration directory. For instance, on Windows the manifests are located at `C:\Users\<user>\AppData\Roaming\Bitwarden\browsers` and on macOS these are in `Application Support` for various browsers ([for example](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_manifests#manifest_location)). Note that disabling the desktop integration will delete the manifests, and the files will need to be updated again.
# We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
# Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
## Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge cebee8aa81b87cc26157e5bd0f879db254db9319`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 8fe821b9a3f9728bcb02d607ca75add468d380c1`
7. Push
### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

28
apps/browser/crowdin.yml Normal file
View File

@@ -0,0 +1,28 @@
project_id_env: _CROWDIN_PROJECT_ID
api_token_env: CROWDIN_API_TOKEN
preserve_hierarchy: true
files:
- source: /src/_locales/en/messages.json
dest: /src/_locales/en/%original_file_name%
translation: /src/_locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved
languages_mapping:
two_letters_code:
pt-PT: pt_PT
pt-BR: pt_BR
zh-CN: zh_CN
zh-TW: zh_TW
en-GB: en_GB
en-IN: en_IN
- source: /store/locales/en/copy.resx
dest: /store/locales/en/%original_file_name%
translation: /store/locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved
languages_mapping:
two_letters_code:
pt-PT: pt_PT
pt-BR: pt_BR
zh-CN: zh_CN
zh-TW: zh_TW
en-GB: en_GB
en-IN: en_IN

245
apps/browser/gulpfile.js Normal file
View File

@@ -0,0 +1,245 @@
const child = require("child_process");
const fs = require("fs");
const del = require("del");
const gulp = require("gulp");
const filter = require("gulp-filter");
const gulpif = require("gulp-if");
const jeditor = require("gulp-json-editor");
const replace = require("gulp-replace");
const zip = require("gulp-zip");
const manifest = require("./src/manifest.json");
const paths = {
build: "./build/",
dist: "./dist/",
coverage: "./coverage/",
node_modules: "./node_modules/",
popupDir: "./src/popup/",
cssDir: "./src/popup/css/",
safari: "./src/safari/",
};
const filters = {
fonts: [
"!build/popup/fonts/*",
"build/popup/fonts/Open_Sans*.woff",
"build/popup/fonts/bwi-font.woff2",
"build/popup/fonts/bwi-font.woff",
"build/popup/fonts/bwi-font.ttf",
],
safari: ["!build/safari/**/*"],
};
function buildString() {
var build = "";
if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") {
build = `-${process.env.BUILD_NUMBER}`;
}
return build;
}
function distFileName(browserName, ext) {
return `dist-${browserName}${buildString()}.${ext}`;
}
function dist(browserName, manifest) {
return gulp
.src(paths.build + "**/*")
.pipe(filter(["**"].concat(filters.fonts).concat(filters.safari)))
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_" + browserName)))
.pipe(gulpif("manifest.json", jeditor(manifest)))
.pipe(zip(distFileName(browserName, "zip")))
.pipe(gulp.dest(paths.dist));
}
function distFirefox() {
return dist("firefox", (manifest) => {
delete manifest.content_security_policy;
removeShortcuts(manifest);
return manifest;
});
}
function distOpera() {
return dist("opera", (manifest) => {
delete manifest.applications;
delete manifest.content_security_policy;
removeShortcuts(manifest);
return manifest;
});
}
function distChrome() {
return dist("chrome", (manifest) => {
delete manifest.applications;
delete manifest.content_security_policy;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
return manifest;
});
}
function distEdge() {
return dist("edge", (manifest) => {
delete manifest.applications;
delete manifest.content_security_policy;
delete manifest.sidebar_action;
delete manifest.commands._execute_sidebar_action;
return manifest;
});
}
function removeShortcuts(manifest) {
if (manifest.content_scripts && manifest.content_scripts.length > 1) {
const shortcutsScript = manifest.content_scripts[1];
if (shortcutsScript.js.indexOf("content/shortcuts.js") > -1) {
manifest.content_scripts.splice(1, 1);
}
}
}
function distSafariMas(cb) {
return distSafariApp(cb, "mas");
}
function distSafariMasDev(cb) {
return distSafariApp(cb, "masdev");
}
function distSafariDmg(cb) {
return distSafariApp(cb, "dmg");
}
function distSafariApp(cb, subBuildPath) {
const buildPath = paths.dist + "Safari/" + subBuildPath + "/";
const builtAppexPath = buildPath + "build/Release/safari.appex";
const builtAppexFrameworkPath = buildPath + "build/Release/safari.appex/Contents/Frameworks/";
const entitlementsPath = paths.safari + "safari/safari.entitlements";
var args = [
"--verbose",
"--force",
"-o",
"runtime",
"--sign",
"Developer ID Application: 8bit Solutions LLC",
"--entitlements",
entitlementsPath,
];
if (subBuildPath !== "dmg") {
args = [
"--verbose",
"--force",
"--sign",
subBuildPath === "mas"
? "3rd Party Mac Developer Application: 8bit Solutions LLC"
: "6B287DD81FF922D86FD836128B0F62F358B38726",
"--entitlements",
entitlementsPath,
];
}
return del([buildPath + "**/*"])
.then(() => safariCopyAssets(paths.safari + "**/*", buildPath))
.then(() => safariCopyBuild(paths.build + "**/*", buildPath + "safari/app"))
.then(() => {
const proc = child.spawn("xcodebuild", [
"-project",
buildPath + "desktop.xcodeproj",
"-alltargets",
"-configuration",
"Release",
]);
stdOutProc(proc);
return new Promise((resolve) => proc.on("close", resolve));
})
.then(() => {
const libs = fs
.readdirSync(builtAppexFrameworkPath)
.filter((p) => p.endsWith(".dylib"))
.map((p) => builtAppexFrameworkPath + p);
const libPromises = [];
libs.forEach((i) => {
const proc = child.spawn("codesign", args.concat([i]));
stdOutProc(proc);
libPromises.push(new Promise((resolve) => proc.on("close", resolve)));
});
return Promise.all(libPromises);
})
.then(() => {
const proc = child.spawn("codesign", args.concat([builtAppexPath]));
stdOutProc(proc);
return new Promise((resolve) => proc.on("close", resolve));
})
.then(
() => {
return cb;
},
() => {
return cb;
}
);
}
function safariCopyAssets(source, dest) {
return new Promise((resolve, reject) => {
gulp
.src(source)
.on("error", reject)
.pipe(gulpif("safari/Info.plist", replace("0.0.1", manifest.version)))
.pipe(
gulpif("safari/Info.plist", replace("0.0.2", process.env.BUILD_NUMBER || manifest.version))
)
.pipe(gulpif("desktop.xcodeproj/project.pbxproj", replace("../../../build", "../safari/app")))
.pipe(gulp.dest(dest))
.on("end", resolve);
});
}
function safariCopyBuild(source, dest) {
return new Promise((resolve, reject) => {
gulp
.src(source)
.on("error", reject)
.pipe(filter(["**"].concat(filters.fonts)))
.pipe(gulpif("popup/index.html", replace("__BROWSER__", "browser_safari")))
.pipe(
gulpif(
"manifest.json",
jeditor((manifest) => {
delete manifest.optional_permissions;
manifest.permissions.push("nativeMessaging");
return manifest;
})
)
)
.pipe(gulp.dest(dest))
.on("end", resolve);
});
}
function stdOutProc(proc) {
proc.stdout.on("data", (data) => console.log(data.toString()));
proc.stderr.on("data", (data) => console.error(data.toString()));
}
function ciCoverage(cb) {
return gulp
.src(paths.coverage + "**/*")
.pipe(filter(["**", "!coverage/coverage*.zip"]))
.pipe(zip(`coverage${buildString()}.zip`))
.pipe(gulp.dest(paths.coverage));
}
exports["dist:firefox"] = distFirefox;
exports["dist:chrome"] = distChrome;
exports["dist:opera"] = distOpera;
exports["dist:edge"] = distEdge;
exports["dist:safari"] = gulp.parallel(distSafariMas, distSafariMasDev, distSafariDmg);
exports["dist:safari:mas"] = distSafariMas;
exports["dist:safari:masdev"] = distSafariMasDev;
exports["dist:safari:dmg"] = distSafariDmg;
exports.dist = gulp.parallel(distFirefox, distChrome, distOpera, distEdge);
exports["ci:coverage"] = ciCoverage;
exports.ci = ciCoverage;

1
apps/browser/jslib Submodule

Submodule apps/browser/jslib added at e40e7de808

View File

@@ -0,0 +1,71 @@
const path = require("path");
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: "",
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ["jasmine", "webpack"],
// list of files / patterns to load in the browser
files: [{ pattern: "src/**/*.spec.ts", watch: false }],
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
"src/**/*.ts": "webpack",
},
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ["progress", "kjhtml"],
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ["Chrome"],
// Concurrency level
// how many browser should be started simultaneous
concurrency: Infinity,
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
webpack: {
mode: "production",
resolve: {
extensions: [".js", ".ts", ".tsx"],
alias: {
"jslib-common": path.join(__dirname, "jslib/common/src"),
"jslib-angular": path.join(__dirname, "jslib/angular/src"),
},
},
module: {
rules: [{ test: /\.tsx?$/, loader: "ts-loader" }],
},
stats: {
colors: true,
modules: true,
reasons: true,
errorDetails: true,
},
devtool: "inline-source-map",
},
});
};

21466
apps/browser/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

111
apps/browser/package.json Normal file
View File

@@ -0,0 +1,111 @@
{
"name": "@bitwarden/browser",
"version": "0.0.0",
"scripts": {
"sub:init": "git submodule update --init --recursive",
"sub:update": "git submodule update --remote",
"sub:pull": "git submodule foreach git pull origin master",
"preinstall": "npm run sub:init",
"symlink:win": "rmdir /S /Q .\\jslib && cmd /c mklink /J .\\jslib ..\\jslib",
"symlink:mac": "npm run symlink:lin",
"symlink:lin": "rm -rf ./jslib && ln -s ../jslib ./jslib",
"build": "webpack",
"build:watch": "webpack --watch",
"build:prod": "cross-env NODE_ENV=production webpack",
"build:prod:watch": "cross-env NODE_ENV=production webpack --watch",
"clean:l10n": "git push origin --delete l10n_master",
"dist": "npm run build:prod && gulp dist",
"dist:firefox": "npm run build:prod && gulp dist:firefox",
"dist:opera": "npm run build:prod && gulp dist:opera",
"dist:safari": "npm run build:prod && gulp dist:safari",
"dist:safari:mas": "npm run build:prod && gulp dist:safari:mas",
"dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev",
"dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint . --fix",
"test": "karma start --single-run",
"test:watch": "karma start",
"prettier": "prettier --write .",
"prepare": "husky install"
},
"devDependencies": {
"@angular/compiler-cli": "^12.2.13",
"@ngtools/webpack": "^12.2.13",
"@types/chrome": "^0.0.139",
"@types/firefox-webext-browser": "^82.0.0",
"@types/jasmine": "^3.7.6",
"@types/mousetrap": "^1.6.8",
"@types/node": "^16.11.12",
"@typescript-eslint/eslint-plugin": "^5.12.1",
"@typescript-eslint/parser": "^5.12.1",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^6.5.1",
"del": "^6.0.0",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.4.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"gulp": "^4.0.2",
"gulp-filter": "^7.0.0",
"gulp-if": "^3.0.0",
"gulp-json-editor": "^2.5.5",
"gulp-replace": "^1.1.0",
"gulp-zip": "^5.1.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"jasmine-core": "^3.7.1",
"jasmine-spec-reporter": "^7.0.0",
"karma": "^6.3.2",
"karma-chrome-launcher": "^3.1.0",
"karma-cli": "^2.0.0",
"karma-jasmine": "^4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-webpack": "^5.0.0",
"lint-staged": "^12.1.3",
"mini-css-extract-plugin": "^2.4.5",
"prettier": "^2.5.1",
"process": "^0.11.10",
"sass": "^1.34.1",
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"tapable": "^1.1.3",
"ts-loader": "^9.2.5",
"typescript": "4.3.5",
"url": "^0.11.0",
"util": "^0.12.4",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"
},
"dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common",
"core-js": "^3.11.0",
"date-input-polyfill": "^2.14.0",
"mousetrap": "^1.6.5",
"ngx-toastr": "14.1.4",
"nord": "^0.2.1",
"sweetalert2": "^10.16.6",
"web-animations-js": "^2.3.2"
},
"engines": {
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
<html>
<head>
<meta charset="UTF-8" />
</head>
<body></body>
</html>

View File

@@ -0,0 +1,6 @@
import MainBackground from "./background/main.background";
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
bitwardenMain.bootstrap().then(() => {
// Finished bootstrapping
});

View File

@@ -0,0 +1,115 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { BrowserApi } from "../browser/browserApi";
import MainBackground from "./main.background";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class CommandsBackground {
private isSafari: boolean;
private isVivaldi: boolean;
constructor(
private main: MainBackground,
private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService,
private vaultTimeoutService: VaultTimeoutService,
private authService: AuthService
) {
this.isSafari = this.platformUtilsService.isSafari();
this.isVivaldi = this.platformUtilsService.isVivaldi();
}
async init() {
BrowserApi.messageListener(
"commands.background",
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") {
await this.processCommand(
msg.data.commandToRetry.msg.command,
msg.data.commandToRetry.sender
);
}
if (this.isVivaldi && msg.command === "keyboardShortcutTriggered" && msg.shortcut) {
await this.processCommand(msg.shortcut, sender);
}
}
);
if (!this.isVivaldi && chrome && chrome.commands) {
chrome.commands.onCommand.addListener(async (command: string) => {
await this.processCommand(command);
});
}
}
private async processCommand(command: string, sender?: chrome.runtime.MessageSender) {
switch (command) {
case "generate_password":
await this.generatePasswordToClipboard();
break;
case "autofill_login":
await this.autoFillLogin(sender ? sender.tab : null);
break;
case "open_popup":
await this.openPopup();
break;
case "lock_vault":
await this.vaultTimeoutService.lock(true);
break;
default:
break;
}
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
}
private async autoFillLogin(tab?: chrome.tabs.Tab) {
if (!tab) {
tab = await BrowserApi.getTabFromCurrentWindowId();
}
if (tab == null) {
return;
}
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: "autofill_login" },
sender: { tab: tab },
},
target: "commands.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd");
}
private async openPopup() {
// Chrome APIs cannot open popup
if (!this.isSafari) {
return;
}
this.main.openPopup();
}
}

View File

@@ -0,0 +1,142 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TotpService } from "jslib-common/abstractions/totp.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { EventType } from "jslib-common/enums/eventType";
import { CipherView } from "jslib-common/models/view/cipherView";
import { BrowserApi } from "../browser/browserApi";
import MainBackground from "./main.background";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class ContextMenusBackground {
private readonly noopCommandSuffix = "noop";
private contextMenus: any;
constructor(
private main: MainBackground,
private cipherService: CipherService,
private passwordGenerationService: PasswordGenerationService,
private platformUtilsService: PlatformUtilsService,
private authService: AuthService,
private eventService: EventService,
private totpService: TotpService
) {
this.contextMenus = chrome.contextMenus;
}
async init() {
if (!this.contextMenus) {
return;
}
this.contextMenus.onClicked.addListener(
async (info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => {
if (info.menuItemId === "generate-password") {
await this.generatePasswordToClipboard();
} else if (info.menuItemId === "copy-identifier") {
await this.getClickedElement(tab, info.frameId);
} else if (
info.parentMenuItemId === "autofill" ||
info.parentMenuItemId === "copy-username" ||
info.parentMenuItemId === "copy-password" ||
info.parentMenuItemId === "copy-totp"
) {
await this.cipherAction(tab, info);
}
}
);
BrowserApi.messageListener(
"contextmenus.background",
async (msg: any, sender: chrome.runtime.MessageSender, sendResponse: any) => {
if (msg.command === "unlockCompleted" && msg.data.target === "contextmenus.background") {
await this.cipherAction(
msg.data.commandToRetry.sender.tab,
msg.data.commandToRetry.msg.data
);
}
}
);
}
private async generatePasswordToClipboard() {
const options = (await this.passwordGenerationService.getOptions())[0];
const password = await this.passwordGenerationService.generatePassword(options);
this.platformUtilsService.copyToClipboard(password, { window: window });
this.passwordGenerationService.addHistory(password);
}
private async getClickedElement(tab: chrome.tabs.Tab, frameId: number) {
if (tab == null) {
return;
}
BrowserApi.tabSendMessage(tab, { command: "getClickedElement" }, { frameId: frameId });
}
private async cipherAction(tab: chrome.tabs.Tab, info: chrome.contextMenus.OnClickData) {
const id = info.menuItemId.split("_")[1];
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: { command: this.noopCommandSuffix, data: info },
sender: { tab: tab },
},
target: "contextmenus.background",
};
await BrowserApi.tabSendMessageData(
tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
BrowserApi.tabSendMessageData(tab, "promptForLogin");
return;
}
let cipher: CipherView;
if (id === this.noopCommandSuffix) {
const ciphers = await this.cipherService.getAllDecryptedForUrl(tab.url);
cipher = ciphers.find((c) => c.reprompt === CipherRepromptType.None);
} else {
const ciphers = await this.cipherService.getAllDecrypted();
cipher = ciphers.find((c) => c.id === id);
}
if (cipher == null) {
return;
}
if (info.parentMenuItemId === "autofill") {
await this.startAutofillPage(tab, cipher);
} else if (info.parentMenuItemId === "copy-username") {
this.platformUtilsService.copyToClipboard(cipher.login.username, { window: window });
} else if (info.parentMenuItemId === "copy-password") {
this.platformUtilsService.copyToClipboard(cipher.login.password, { window: window });
this.eventService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (info.parentMenuItemId === "copy-totp") {
const totpValue = await this.totpService.getCode(cipher.login.totp);
this.platformUtilsService.copyToClipboard(totpValue, { window: window });
}
}
private async startAutofillPage(tab: chrome.tabs.Tab, cipher: CipherView) {
this.main.loginToAutoFill = cipher;
if (tab == null) {
return;
}
BrowserApi.tabSendMessage(tab, {
command: "collectPageDetails",
tab: tab,
sender: "contextMenu",
});
}
}

View File

@@ -0,0 +1,72 @@
import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { StateService } from "../services/abstractions/state.service";
const IdleInterval = 60 * 5; // 5 minutes
export default class IdleBackground {
private idle: any;
private idleTimer: number = null;
private idleState = "active";
constructor(
private vaultTimeoutService: VaultTimeoutService,
private stateService: StateService,
private notificationsService: NotificationsService
) {
this.idle = chrome.idle || (browser != null ? browser.idle : null);
}
async init() {
if (!this.idle) {
return;
}
const idleHandler = (newState: string) => {
if (newState === "active") {
this.notificationsService.reconnectFromActivity();
} else {
this.notificationsService.disconnectFromInactivity();
}
};
if (this.idle.onStateChanged && this.idle.setDetectionInterval) {
this.idle.setDetectionInterval(IdleInterval);
this.idle.onStateChanged.addListener(idleHandler);
} else {
this.pollIdle(idleHandler);
}
if (this.idle.onStateChanged) {
this.idle.onStateChanged.addListener(async (newState: string) => {
if (newState === "locked") {
// If the screen is locked or the screensaver activates
const timeout = await this.stateService.getVaultTimeout();
if (timeout === -2) {
// On System Lock vault timeout option
const action = await this.stateService.getVaultTimeoutAction();
if (action === "logOut") {
await this.vaultTimeoutService.logOut();
} else {
await this.vaultTimeoutService.lock(true);
}
}
}
});
}
}
private pollIdle(handler: (newState: string) => void) {
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idle.queryState(IdleInterval, (state: string) => {
if (state !== this.idleState) {
this.idleState = state;
handler(state);
}
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
});
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
import NotificationQueueMessage from "./notificationQueueMessage";
export default class AddChangePasswordQueueMessage extends NotificationQueueMessage {
cipherId: string;
newPassword: string;
}

View File

@@ -0,0 +1,7 @@
import NotificationQueueMessage from "./notificationQueueMessage";
export default class AddLoginQueueMessage extends NotificationQueueMessage {
username: string;
password: string;
uri: string;
}

View File

@@ -0,0 +1,5 @@
export default class AddLoginRuntimeMessage {
username: string;
password: string;
url: string;
}

View File

@@ -0,0 +1,5 @@
export default class ChangePasswordRuntimeMessage {
currentPassword: string;
newPassword: string;
url: string;
}

View File

@@ -0,0 +1,8 @@
export default interface IconDetails {
path: {
19: string;
38: string;
};
// Chrome does not support windowId, only Firefox
windowId?: number;
}

View File

@@ -0,0 +1,7 @@
export default class LockedVaultPendingNotificationsItem {
commandToRetry: {
msg: any;
sender: chrome.runtime.MessageSender;
};
target: string;
}

View File

@@ -0,0 +1,9 @@
import { NotificationQueueMessageType } from "./notificationQueueMessageType";
export default class NotificationQueueMessage {
type: NotificationQueueMessageType;
domain: string;
tabId: number;
expires: Date;
wasVaultLocked: boolean;
}

View File

@@ -0,0 +1,4 @@
export enum NotificationQueueMessageType {
AddLogin = 0,
ChangePassword = 1,
}

View File

@@ -0,0 +1,379 @@
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { Utils } from "jslib-common/misc/utils";
import { EncString } from "jslib-common/models/domain/encString";
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
import { BrowserApi } from "../browser/browserApi";
import RuntimeBackground from "./runtime.background";
const MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = "sha1";
type Message = {
command: string;
// Filled in by this service
userId?: string;
timestamp?: number;
// Used for sharing secret
publicKey?: string;
};
type OuterMessage = {
message: Message | EncString;
appId: string;
};
type ReceiveMessage = {
timestamp: number;
command: string;
response?: any;
// Unlock key
keyB64?: string;
};
type ReceiveMessageOuter = {
command: string;
appId: string;
// Should only have one of these.
message?: EncString;
sharedSecret?: string;
};
export class NativeMessagingBackground {
private connected = false;
private connecting: boolean;
private port: browser.runtime.Port | chrome.runtime.Port;
private resolver: any = null;
private privateKey: ArrayBuffer = null;
private publicKey: ArrayBuffer = null;
private secureSetupResolve: any = null;
private sharedSecret: SymmetricCryptoKey;
private appId: string;
private validatingFingerprint: boolean;
constructor(
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private runtimeBackground: RuntimeBackground,
private i18nService: I18nService,
private messagingService: MessagingService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService,
private stateService: StateService,
private logService: LogService,
private authService: AuthService
) {
this.stateService.setBiometricFingerprintValidated(false);
if (chrome?.permissions?.onAdded) {
// Reload extension to activate nativeMessaging
chrome.permissions.onAdded.addListener((permissions) => {
BrowserApi.reloadExtension(null);
});
}
}
async connect() {
this.appId = await this.appIdService.getAppId();
this.stateService.setBiometricFingerprintValidated(false);
return new Promise<void>((resolve, reject) => {
this.port = BrowserApi.connectNative("com.8bit.bitwarden");
this.connecting = true;
const connectedCallback = () => {
this.connected = true;
this.connecting = false;
resolve();
};
// Safari has a bundled native component which is always available, no need to
// check if the desktop app is running.
if (this.platformUtilsService.isSafari()) {
connectedCallback();
}
this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => {
switch (message.command) {
case "connected":
connectedCallback();
break;
case "disconnected":
if (this.connecting) {
reject("startDesktop");
}
this.connected = false;
this.port.disconnect();
break;
case "setupEncryption": {
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
return;
}
const encrypted = Utils.fromB64ToArray(message.sharedSecret);
const decrypted = await this.cryptoFunctionService.rsaDecrypt(
encrypted.buffer,
this.privateKey,
EncryptionAlgorithm
);
if (this.validatingFingerprint) {
this.validatingFingerprint = false;
this.stateService.setBiometricFingerprintValidated(true);
}
this.sharedSecret = new SymmetricCryptoKey(decrypted);
this.secureSetupResolve();
break;
}
case "invalidateEncryption":
// Ignore since it belongs to another device
if (message.appId !== this.appId) {
return;
}
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.messagingService.send("showDialog", {
text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
break;
case "verifyFingerprint": {
if (this.sharedSecret == null) {
this.validatingFingerprint = true;
this.showFingerprintDialog();
}
break;
}
case "wrongUserId":
this.showWrongUserDialog();
break;
default:
// Ignore since it belongs to another device
if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) {
return;
}
this.onMessage(message.message);
}
});
this.port.onDisconnect.addListener((p: any) => {
let error;
if (BrowserApi.isWebExtensionsApi) {
error = p.error.message;
} else {
error = chrome.runtime.lastError.message;
}
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
const reason = error != null ? "desktopIntegrationDisabled" : null;
reject(reason);
});
});
}
showWrongUserDialog() {
this.messagingService.send("showDialog", {
text: this.i18nService.t("nativeMessagingWrongUserDesc"),
title: this.i18nService.t("nativeMessagingWrongUserTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
}
async send(message: Message) {
if (!this.connected) {
await this.connect();
}
message.userId = await this.stateService.getUserId();
message.timestamp = Date.now();
if (this.platformUtilsService.isSafari()) {
this.postMessage(message as any);
} else {
this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) });
}
}
async encryptMessage(message: Message) {
if (this.sharedSecret == null) {
await this.secureCommunication();
}
return await this.cryptoService.encrypt(JSON.stringify(message), this.sharedSecret);
}
getResponse(): Promise<any> {
return new Promise((resolve, reject) => {
this.resolver = resolve;
});
}
private postMessage(message: OuterMessage) {
// Wrap in try-catch to when the port disconnected without triggering `onDisconnect`.
try {
this.port.postMessage(message);
} catch (e) {
this.logService.error("NativeMessaging port disconnected, disconnecting.");
this.sharedSecret = null;
this.privateKey = null;
this.connected = false;
this.messagingService.send("showDialog", {
text: this.i18nService.t("nativeMessagingInvalidEncryptionDesc"),
title: this.i18nService.t("nativeMessagingInvalidEncryptionTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
}
}
private async onMessage(rawMessage: ReceiveMessage | EncString) {
let message = rawMessage as ReceiveMessage;
if (!this.platformUtilsService.isSafari()) {
message = JSON.parse(
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecret)
);
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
this.logService.error("NativeMessage is to old, ignoring.");
return;
}
switch (message.command) {
case "biometricUnlock": {
await this.stateService.setBiometricAwaitingAcceptance(null);
if (message.response === "not enabled") {
this.messagingService.send("showDialog", {
text: this.i18nService.t("biometricsNotEnabledDesc"),
title: this.i18nService.t("biometricsNotEnabledTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
break;
} else if (message.response === "not supported") {
this.messagingService.send("showDialog", {
text: this.i18nService.t("biometricsNotSupportedDesc"),
title: this.i18nService.t("biometricsNotSupportedTitle"),
confirmText: this.i18nService.t("ok"),
type: "error",
});
break;
}
const enabled = await this.stateService.getBiometricUnlock();
if (enabled === null || enabled === false) {
if (message.response === "unlocked") {
await this.stateService.setBiometricUnlock(true);
}
break;
}
// Ignore unlock if already unlocked
if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) {
break;
}
if (message.response === "unlocked") {
await this.cryptoService.setKey(
new SymmetricCryptoKey(Utils.fromB64ToArray(message.keyB64).buffer)
);
// Verify key is correct by attempting to decrypt a secret
try {
await this.cryptoService.getFingerprint(await this.stateService.getUserId());
} catch (e) {
this.logService.error("Unable to verify key: " + e);
await this.cryptoService.clearKey();
this.showWrongUserDialog();
// Exit early
if (this.resolver) {
this.resolver(message);
}
return;
}
await this.stateService.setBiometricLocked(false);
this.runtimeBackground.processMessage({ command: "unlocked" }, null, null);
}
break;
}
default:
this.logService.error("NativeMessage, got unknown command: " + message.command);
break;
}
if (this.resolver) {
this.resolver(message);
}
}
private async secureCommunication() {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
this.publicKey = publicKey;
this.privateKey = privateKey;
this.sendUnencrypted({
command: "setupEncryption",
publicKey: Utils.fromBufferToB64(publicKey),
userId: await this.stateService.getUserId(),
});
return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
}
private async sendUnencrypted(message: Message) {
if (!this.connected) {
await this.connect();
}
message.timestamp = Date.now();
this.postMessage({ appId: this.appId, message: message });
}
private async showFingerprintDialog() {
const fingerprint = (
await this.cryptoService.getFingerprint(await this.stateService.getUserId(), this.publicKey)
).join(" ");
this.messagingService.send("showDialog", {
html: `${this.i18nService.t(
"desktopIntegrationVerificationText"
)}<br><br><strong>${fingerprint}</strong>`,
title: this.i18nService.t("desktopSyncVerificationTitle"),
confirmText: this.i18nService.t("ok"),
type: "warning",
});
}
}

View File

@@ -0,0 +1,449 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { CipherType } from "jslib-common/enums/cipherType";
import { PolicyType } from "jslib-common/enums/policyType";
import { Utils } from "jslib-common/misc/utils";
import { CipherView } from "jslib-common/models/view/cipherView";
import { LoginUriView } from "jslib-common/models/view/loginUriView";
import { LoginView } from "jslib-common/models/view/loginView";
import { BrowserApi } from "../browser/browserApi";
import { AutofillService } from "../services/abstractions/autofill.service";
import { StateService } from "../services/abstractions/state.service";
import AddChangePasswordQueueMessage from "./models/addChangePasswordQueueMessage";
import AddLoginQueueMessage from "./models/addLoginQueueMessage";
import AddLoginRuntimeMessage from "./models/addLoginRuntimeMessage";
import ChangePasswordRuntimeMessage from "./models/changePasswordRuntimeMessage";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
import { NotificationQueueMessageType } from "./models/notificationQueueMessageType";
export default class NotificationBackground {
private notificationQueue: (AddLoginQueueMessage | AddChangePasswordQueueMessage)[] = [];
constructor(
private autofillService: AutofillService,
private cipherService: CipherService,
private authService: AuthService,
private policyService: PolicyService,
private folderService: FolderService,
private stateService: StateService
) {}
async init() {
if (chrome.runtime == null) {
return;
}
BrowserApi.messageListener(
"notification.background",
async (msg: any, sender: chrome.runtime.MessageSender) => {
await this.processMessage(msg, sender);
}
);
this.cleanupNotificationQueue();
}
async processMessage(msg: any, sender: chrome.runtime.MessageSender) {
switch (msg.command) {
case "unlockCompleted":
if (msg.data.target !== "notification.background") {
return;
}
await this.processMessage(msg.data.commandToRetry.msg, msg.data.commandToRetry.sender);
break;
case "bgGetDataForTab":
await this.getDataForTab(sender.tab, msg.responseCommand);
break;
case "bgCloseNotificationBar":
await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar");
break;
case "bgAdjustNotificationBar":
await BrowserApi.tabSendMessageData(sender.tab, "adjustNotificationBar", msg.data);
break;
case "bgAddLogin":
await this.addLogin(msg.login, sender.tab);
break;
case "bgChangedPassword":
await this.changedPassword(msg.data, sender.tab);
break;
case "bgAddClose":
case "bgChangeClose":
this.removeTabFromNotificationQueue(sender.tab);
break;
case "bgAddSave":
case "bgChangeSave":
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
const retryMessage: LockedVaultPendingNotificationsItem = {
commandToRetry: {
msg: msg,
sender: sender,
},
target: "notification.background",
};
await BrowserApi.tabSendMessageData(
sender.tab,
"addToLockedVaultPendingNotifications",
retryMessage
);
await BrowserApi.tabSendMessageData(sender.tab, "promptForLogin");
return;
}
await this.saveOrUpdateCredentials(sender.tab, msg.folder);
break;
case "bgNeverSave":
await this.saveNever(sender.tab);
break;
case "collectPageDetailsResponse":
switch (msg.sender) {
case "notificationBar": {
const forms = this.autofillService.getFormsWithPasswordFields(msg.details);
await BrowserApi.tabSendMessageData(msg.tab, "notificationBarPageDetails", {
details: msg.details,
forms: forms,
});
break;
}
default:
break;
}
break;
default:
break;
}
}
async checkNotificationQueue(tab: chrome.tabs.Tab = null): Promise<void> {
if (this.notificationQueue.length === 0) {
return;
}
if (tab != null) {
this.doNotificationQueueCheck(tab);
return;
}
const currentTab = await BrowserApi.getTabFromCurrentWindow();
if (currentTab != null) {
this.doNotificationQueueCheck(currentTab);
}
}
private cleanupNotificationQueue() {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (this.notificationQueue[i].expires < new Date()) {
this.notificationQueue.splice(i, 1);
}
}
setTimeout(() => this.cleanupNotificationQueue(), 2 * 60 * 1000); // check every 2 minutes
}
private doNotificationQueueCheck(tab: chrome.tabs.Tab): void {
if (tab == null) {
return;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain == null) {
return;
}
for (let i = 0; i < this.notificationQueue.length; i++) {
if (
this.notificationQueue[i].tabId !== tab.id ||
this.notificationQueue[i].domain !== tabDomain
) {
continue;
}
if (this.notificationQueue[i].type === NotificationQueueMessageType.AddLogin) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
type: "add",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
} else if (this.notificationQueue[i].type === NotificationQueueMessageType.ChangePassword) {
BrowserApi.tabSendMessageData(tab, "openNotificationBar", {
type: "change",
typeData: {
isVaultLocked: this.notificationQueue[i].wasVaultLocked,
},
});
}
break;
}
}
private removeTabFromNotificationQueue(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
if (this.notificationQueue[i].tabId === tab.id) {
this.notificationQueue.splice(i, 1);
}
}
}
private async addLogin(loginInfo: AddLoginRuntimeMessage, tab: chrome.tabs.Tab) {
const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.LoggedOut) {
return;
}
const loginDomain = Utils.getDomain(loginInfo.url);
if (loginDomain == null) {
return;
}
let normalizedUsername = loginInfo.username;
if (normalizedUsername != null) {
normalizedUsername = normalizedUsername.toLowerCase();
}
const disabledAddLogin = await this.stateService.getDisableAddLoginNotification();
if (authStatus === AuthenticationStatus.Locked) {
if (disabledAddLogin) {
return;
}
if (!(await this.allowPersonalOwnership())) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab, true);
return;
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername
);
if (usernameMatches.length === 0) {
if (disabledAddLogin) {
return;
}
if (!(await this.allowPersonalOwnership())) {
return;
}
this.pushAddLoginToQueue(loginDomain, loginInfo, tab);
} else if (
usernameMatches.length === 1 &&
usernameMatches[0].login.password !== loginInfo.password
) {
const disabledChangePassword =
await this.stateService.getDisableChangedPasswordNotification();
if (disabledChangePassword) {
return;
}
this.pushChangePasswordToQueue(usernameMatches[0].id, loginDomain, loginInfo.password, tab);
}
}
private async pushAddLoginToQueue(
loginDomain: string,
loginInfo: AddLoginRuntimeMessage,
tab: chrome.tabs.Tab,
isVaultLocked = false
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddLoginQueueMessage = {
type: NotificationQueueMessageType.AddLogin,
username: loginInfo.username,
password: loginInfo.password,
domain: loginDomain,
uri: loginInfo.url,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
}
private async changedPassword(changeData: ChangePasswordRuntimeMessage, tab: chrome.tabs.Tab) {
const loginDomain = Utils.getDomain(changeData.url);
if (loginDomain == null) {
return;
}
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
this.pushChangePasswordToQueue(null, loginDomain, changeData.newPassword, tab, true);
return;
}
let id: string = null;
const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url);
if (changeData.currentPassword != null) {
const passwordMatches = ciphers.filter(
(c) => c.login.password === changeData.currentPassword
);
if (passwordMatches.length === 1) {
id = passwordMatches[0].id;
}
} else if (ciphers.length === 1) {
id = ciphers[0].id;
}
if (id != null) {
this.pushChangePasswordToQueue(id, loginDomain, changeData.newPassword, tab);
}
}
private async pushChangePasswordToQueue(
cipherId: string,
loginDomain: string,
newPassword: string,
tab: chrome.tabs.Tab,
isVaultLocked = false
) {
// remove any old messages for this tab
this.removeTabFromNotificationQueue(tab);
const message: AddChangePasswordQueueMessage = {
type: NotificationQueueMessageType.ChangePassword,
cipherId: cipherId,
newPassword: newPassword,
domain: loginDomain,
tabId: tab.id,
expires: new Date(new Date().getTime() + 5 * 60000), // 5 minutes
wasVaultLocked: isVaultLocked,
};
this.notificationQueue.push(message);
await this.checkNotificationQueue(tab);
}
private async saveOrUpdateCredentials(tab: chrome.tabs.Tab, folderId?: string) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
queueMessage.tabId !== tab.id ||
(queueMessage.type !== NotificationQueueMessageType.AddLogin &&
queueMessage.type !== NotificationQueueMessageType.ChangePassword)
) {
continue;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const changePasswordMessage = queueMessage as AddChangePasswordQueueMessage;
const cipher = await this.getDecryptedCipherById(changePasswordMessage.cipherId);
if (cipher == null) {
return;
}
await this.updateCipher(cipher, changePasswordMessage.newPassword);
return;
}
if (queueMessage.type === NotificationQueueMessageType.AddLogin) {
if (!queueMessage.wasVaultLocked) {
await this.createNewCipher(queueMessage as AddLoginQueueMessage, folderId);
BrowserApi.tabSendMessageData(tab, "addedCipher");
return;
}
// If the vault was locked, check if a cipher needs updating instead of creating a new one
const addLoginMessage = queueMessage as AddLoginQueueMessage;
const ciphers = await this.cipherService.getAllDecryptedForUrl(addLoginMessage.uri);
const usernameMatches = ciphers.filter(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === addLoginMessage.username
);
if (usernameMatches.length >= 1) {
await this.updateCipher(usernameMatches[0], addLoginMessage.password);
return;
}
await this.createNewCipher(addLoginMessage, folderId);
BrowserApi.tabSendMessageData(tab, "addedCipher");
}
}
}
private async createNewCipher(queueMessage: AddLoginQueueMessage, folderId: string) {
const loginModel = new LoginView();
const loginUri = new LoginUriView();
loginUri.uri = queueMessage.uri;
loginModel.uris = [loginUri];
loginModel.username = queueMessage.username;
loginModel.password = queueMessage.password;
const model = new CipherView();
model.name = Utils.getHostname(queueMessage.uri) || queueMessage.domain;
model.name = model.name.replace(/^www\./, "");
model.type = CipherType.Login;
model.login = loginModel;
if (!Utils.isNullOrWhitespace(folderId)) {
const folders = await this.folderService.getAllDecrypted();
if (folders.some((x) => x.id === folderId)) {
model.folderId = folderId;
}
}
const cipher = await this.cipherService.encrypt(model);
await this.cipherService.saveWithServer(cipher);
}
private async getDecryptedCipherById(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) {
return await cipher.decrypt();
}
return null;
}
private async updateCipher(cipher: CipherView, newPassword: string) {
if (cipher != null && cipher.type === CipherType.Login) {
cipher.login.password = newPassword;
const newCipher = await this.cipherService.encrypt(cipher);
await this.cipherService.saveWithServer(newCipher);
}
}
private async saveNever(tab: chrome.tabs.Tab) {
for (let i = this.notificationQueue.length - 1; i >= 0; i--) {
const queueMessage = this.notificationQueue[i];
if (
queueMessage.tabId !== tab.id ||
queueMessage.type !== NotificationQueueMessageType.AddLogin
) {
continue;
}
const tabDomain = Utils.getDomain(tab.url);
if (tabDomain != null && tabDomain !== queueMessage.domain) {
continue;
}
this.notificationQueue.splice(i, 1);
BrowserApi.tabSendMessageData(tab, "closeNotificationBar");
const hostname = Utils.getHostname(tab.url);
await this.cipherService.saveNeverDomain(hostname);
}
}
private async getDataForTab(tab: chrome.tabs.Tab, responseCommand: string) {
const responseData: any = {};
if (responseCommand === "notificationBarGetFoldersList") {
responseData.folders = await this.folderService.getAllDecrypted();
}
await BrowserApi.tabSendMessageData(tab, responseCommand, responseData);
}
private async allowPersonalOwnership(): Promise<boolean> {
return !(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership));
}
}

View File

@@ -0,0 +1,235 @@
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { SystemService } from "jslib-common/abstractions/system.service";
import { Utils } from "jslib-common/misc/utils";
import { BrowserApi } from "../browser/browserApi";
import { AutofillService } from "../services/abstractions/autofill.service";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import MainBackground from "./main.background";
import LockedVaultPendingNotificationsItem from "./models/lockedVaultPendingNotificationsItem";
export default class RuntimeBackground {
private autofillTimeout: any;
private pageDetailsToAutoFill: any[] = [];
private onInstalledReason: string = null;
private lockedVaultPendingNotifications: LockedVaultPendingNotificationsItem[] = [];
constructor(
private main: MainBackground,
private autofillService: AutofillService,
private platformUtilsService: BrowserPlatformUtilsService,
private i18nService: I18nService,
private notificationsService: NotificationsService,
private systemService: SystemService,
private environmentService: EnvironmentService,
private messagingService: MessagingService,
private logService: LogService
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
this.onInstalledReason = details.reason;
});
}
async init() {
if (!chrome.runtime) {
return;
}
await this.checkOnInstalled();
const backgroundMessageListener = async (
msg: any,
sender: chrome.runtime.MessageSender,
sendResponse: any
) => {
await this.processMessage(msg, sender, sendResponse);
};
BrowserApi.messageListener("runtime.background", backgroundMessageListener);
if (this.main.isPrivateMode) {
(window as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
}
}
async processMessage(msg: any, sender: any, sendResponse: any) {
switch (msg.command) {
case "loggedIn":
case "unlocked": {
let item: LockedVaultPendingNotificationsItem;
if (this.lockedVaultPendingNotifications?.length > 0) {
await BrowserApi.closeLoginTab();
item = this.lockedVaultPendingNotifications.pop();
if (item.commandToRetry.sender?.tab?.id) {
await BrowserApi.focusSpecifiedTab(item.commandToRetry.sender.tab.id);
}
}
await this.main.setIcon();
await this.main.refreshBadgeAndMenu(false);
this.notificationsService.updateConnection(msg.command === "unlocked");
this.systemService.cancelProcessReload();
if (item) {
await BrowserApi.tabSendMessageData(
item.commandToRetry.sender.tab,
"unlockCompleted",
item
);
}
break;
}
case "addToLockedVaultPendingNotifications":
this.lockedVaultPendingNotifications.push(msg.data);
break;
case "logout":
await this.main.logout(msg.expired, msg.userId);
break;
case "syncCompleted":
if (msg.successfully) {
setTimeout(async () => await this.main.refreshBadgeAndMenu(), 2000);
}
break;
case "openPopup":
await this.main.openPopup();
break;
case "promptForLogin":
await BrowserApi.createNewTab("popup/index.html?uilocation=popout", true, true);
break;
case "showDialogResolve":
this.platformUtilsService.resolveDialogPromise(msg.dialogId, msg.confirmed);
break;
case "bgCollectPageDetails":
await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId);
break;
case "bgUpdateContextMenu":
case "editedCipher":
case "addedCipher":
case "deletedCipher":
await this.main.refreshBadgeAndMenu();
break;
case "bgReseedStorage":
await this.main.reseedStorage();
break;
case "collectPageDetailsResponse":
switch (msg.sender) {
case "autofiller":
case "autofill_cmd": {
const totpCode = await this.autofillService.doAutoFillActiveTab(
[
{
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
},
],
msg.sender === "autofill_cmd"
);
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
}
break;
}
case "contextMenu":
clearTimeout(this.autofillTimeout);
this.pageDetailsToAutoFill.push({
frameId: sender.frameId,
tab: msg.tab,
details: msg.details,
});
this.autofillTimeout = setTimeout(async () => await this.autofillPage(), 300);
break;
default:
break;
}
break;
case "authResult": {
const vaultUrl = this.environmentService.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
return;
}
try {
BrowserApi.createNewTab(
"popup/index.html?uilocation=popout#/sso?code=" +
encodeURIComponent(msg.code) +
"&state=" +
encodeURIComponent(msg.state)
);
} catch {
this.logService.error("Unable to open sso popout tab");
}
break;
}
case "webAuthnResult": {
const vaultUrl = this.environmentService.getWebVaultUrl();
if (msg.referrer == null || Utils.getHostname(vaultUrl) !== msg.referrer) {
return;
}
const params =
`webAuthnResponse=${encodeURIComponent(msg.data)};` +
`remember=${encodeURIComponent(msg.remember)}`;
BrowserApi.createNewTab(
`popup/index.html?uilocation=popout#/2fa;${params}`,
undefined,
false
);
break;
}
case "reloadPopup":
this.messagingService.send("reloadPopup");
break;
case "emailVerificationRequired":
this.messagingService.send("showDialog", {
dialogId: "emailVerificationRequired",
title: this.i18nService.t("emailVerificationRequired"),
text: this.i18nService.t("emailVerificationRequiredDesc"),
confirmText: this.i18nService.t("ok"),
type: "info",
});
break;
case "getClickedElementResponse":
this.platformUtilsService.copyToClipboard(msg.identifier, { window: window });
break;
default:
break;
}
}
private async autofillPage() {
const totpCode = await this.autofillService.doAutoFill({
cipher: this.main.loginToAutoFill,
pageDetails: this.pageDetailsToAutoFill,
fillNewPassword: true,
});
if (totpCode != null) {
this.platformUtilsService.copyToClipboard(totpCode, { window: window });
}
// reset
this.main.loginToAutoFill = null;
this.pageDetailsToAutoFill = [];
}
private async checkOnInstalled() {
setTimeout(async () => {
if (this.onInstalledReason != null) {
if (this.onInstalledReason === "install") {
BrowserApi.createNewTab("https://bitwarden.com/browser-start/");
}
this.onInstalledReason = null;
}
}, 100);
}
}

View File

@@ -0,0 +1,63 @@
import MainBackground from "./main.background";
import NotificationBackground from "./notification.background";
export default class TabsBackground {
constructor(
private main: MainBackground,
private notificationBackground: NotificationBackground
) {}
private focusedWindowId: number;
async init() {
if (!chrome.tabs || !chrome.windows) {
return;
}
chrome.windows.onFocusChanged.addListener(async (windowId: number) => {
if (windowId === null || windowId < 0) {
return;
}
this.focusedWindowId = windowId;
this.main.messagingService.send("windowChanged");
});
chrome.tabs.onActivated.addListener(async (activeInfo: chrome.tabs.TabActiveInfo) => {
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send("tabChanged");
});
chrome.tabs.onReplaced.addListener(async (addedTabId: number, removedTabId: number) => {
if (this.main.onReplacedRan) {
return;
}
this.main.onReplacedRan = true;
await this.notificationBackground.checkNotificationQueue();
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send("tabChanged");
});
chrome.tabs.onUpdated.addListener(
async (tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) => {
if (this.focusedWindowId > 0 && tab.windowId != this.focusedWindowId) {
return;
}
if (!tab.active) {
return;
}
if (this.main.onUpdatedRan) {
return;
}
this.main.onUpdatedRan = true;
await this.notificationBackground.checkNotificationQueue(tab);
await this.main.refreshBadgeAndMenu();
this.main.messagingService.send("tabChanged");
}
);
}
}

View File

@@ -0,0 +1,96 @@
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CipherService } from "jslib-common/abstractions/cipher.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { UriMatchType } from "jslib-common/enums/uriMatchType";
export default class WebRequestBackground {
private pendingAuthRequests: any[] = [];
private webRequest: any;
private isFirefox: boolean;
constructor(
platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private authService: AuthService
) {
this.webRequest = (window as any).chrome.webRequest;
this.isFirefox = platformUtilsService.isFirefox();
}
async init() {
if (!this.webRequest || !this.webRequest.onAuthRequired) {
return;
}
this.webRequest.onAuthRequired.addListener(
async (details: any, callback: any) => {
if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) {
if (callback) {
callback();
}
return;
}
this.pendingAuthRequests.push(details.requestId);
if (this.isFirefox) {
// eslint-disable-next-line
return new Promise(async (resolve, reject) => {
await this.resolveAuthCredentials(details.url, resolve, reject);
});
} else {
await this.resolveAuthCredentials(details.url, callback, callback);
}
},
{ urls: ["http://*/*", "https://*/*"] },
[this.isFirefox ? "blocking" : "asyncBlocking"]
);
this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), {
urls: ["http://*/*"],
});
this.webRequest.onErrorOccurred.addListener(
(details: any) => this.completeAuthRequest(details),
{
urls: ["http://*/*"],
}
);
}
// eslint-disable-next-line
private async resolveAuthCredentials(domain: string, success: Function, error: Function) {
if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
error();
return;
}
try {
const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain,
null,
UriMatchType.Host
);
if (ciphers == null || ciphers.length !== 1) {
error();
return;
}
success({
authCredentials: {
username: ciphers[0].login.username,
password: ciphers[0].login.password,
},
});
} catch {
error();
}
}
private completeAuthRequest(details: any) {
const i = this.pendingAuthRequests.indexOf(details.requestId);
if (i > -1) {
this.pendingAuthRequests.splice(i, 1);
}
}
}

View File

@@ -0,0 +1,236 @@
import { Utils } from "jslib-common/misc/utils";
import { SafariApp } from "./safariApp";
export class BrowserApi {
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
static isSafariApi: boolean =
navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf(" Chrome/") === -1 &&
navigator.userAgent.indexOf(" Chromium/") === -1;
static isChromeApi: boolean = !BrowserApi.isSafariApi && typeof chrome !== "undefined";
static isFirefoxOnAndroid: boolean =
navigator.userAgent.indexOf("Firefox/") !== -1 && navigator.userAgent.indexOf("Android") !== -1;
static async getTabFromCurrentWindowId(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({
active: true,
windowId: chrome.windows.WINDOW_ID_CURRENT,
});
}
static async getTabFromCurrentWindow(): Promise<chrome.tabs.Tab> | null {
return await BrowserApi.tabsQueryFirst({
active: true,
currentWindow: true,
});
}
static async getActiveTabs(): Promise<chrome.tabs.Tab[]> {
return await BrowserApi.tabsQuery({
active: true,
});
}
static async tabsQuery(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> {
return new Promise((resolve) => {
chrome.tabs.query(options, (tabs: any[]) => {
resolve(tabs);
});
});
}
static async tabsQueryFirst(options: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab> | null {
const tabs = await BrowserApi.tabsQuery(options);
if (tabs.length > 0) {
return tabs[0];
}
return null;
}
static tabSendMessageData(
tab: chrome.tabs.Tab,
command: string,
data: any = null
): Promise<any[]> {
const obj: any = {
command: command,
};
if (data != null) {
obj.data = data;
}
return BrowserApi.tabSendMessage(tab, obj);
}
static async tabSendMessage(
tab: chrome.tabs.Tab,
obj: any,
options: chrome.tabs.MessageSendOptions = null
): Promise<any> {
if (!tab || !tab.id) {
return;
}
return new Promise<void>((resolve) => {
chrome.tabs.sendMessage(tab.id, obj, options, () => {
if (chrome.runtime.lastError) {
// Some error happened
}
resolve();
});
});
}
static async getPrivateModeWindows(): Promise<browser.windows.Window[]> {
return (await browser.windows.getAll()).filter((win) => win.incognito);
}
static async onWindowCreated(callback: (win: chrome.windows.Window) => any) {
return chrome.windows.onCreated.addListener(callback);
}
static getBackgroundPage(): any {
return chrome.extension.getBackgroundPage();
}
static getApplicationVersion(): string {
return chrome.runtime.getManifest().version;
}
static async isPopupOpen(): Promise<boolean> {
return Promise.resolve(chrome.extension.getViews({ type: "popup" }).length > 0);
}
static createNewTab(url: string, extensionPage = false, active = true) {
chrome.tabs.create({ url: url, active: active });
}
static messageListener(
name: string,
callback: (message: any, sender: chrome.runtime.MessageSender, response: any) => void
) {
chrome.runtime.onMessage.addListener(
(msg: any, sender: chrome.runtime.MessageSender, response: any) => {
callback(msg, sender, response);
}
);
}
static async closeLoginTab() {
const tabs = await BrowserApi.tabsQuery({
active: true,
title: "Bitwarden",
windowType: "normal",
currentWindow: true,
});
if (tabs.length === 0) {
return;
}
const tabToClose = tabs[tabs.length - 1].id;
chrome.tabs.remove(tabToClose);
}
static async focusSpecifiedTab(tabId: number) {
chrome.tabs.update(tabId, { active: true, highlighted: true });
}
static closePopup(win: Window) {
if (BrowserApi.isWebExtensionsApi && BrowserApi.isFirefoxOnAndroid) {
// Reactivating the active tab dismisses the popup tab. The promise final
// condition is only called if the popup wasn't already dismissed (future proofing).
// ref: https://bugzilla.mozilla.org/show_bug.cgi?id=1433604
browser.tabs.update({ active: true }).finally(win.close);
} else {
win.close();
}
}
static downloadFile(win: Window, blobData: any, blobOptions: any, fileName: string) {
if (BrowserApi.isSafariApi) {
const type = blobOptions != null ? blobOptions.type : null;
let data: string = null;
if (type === "text/plain" && typeof blobData === "string") {
data = blobData;
} else {
data = Utils.fromBufferToB64(blobData);
}
SafariApp.sendMessageToApp(
"downloadFile",
JSON.stringify({
blobData: data,
blobOptions: blobOptions,
fileName: fileName,
}),
true
);
} else {
const blob = new Blob([blobData], blobOptions);
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveBlob(blob, fileName);
} else {
const a = win.document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = fileName;
win.document.body.appendChild(a);
a.click();
win.document.body.removeChild(a);
}
}
}
static gaFilter() {
return process.env.ENV !== "production";
}
static getUILanguage(win: Window) {
return chrome.i18n.getUILanguage();
}
static reloadExtension(win: Window) {
if (win != null) {
return win.location.reload(true);
} else {
return chrome.runtime.reload();
}
}
static reloadOpenWindows() {
const views = chrome.extension.getViews() as Window[];
views
.filter((w) => w.location.href != null)
.forEach((w) => {
w.location.reload();
});
}
static connectNative(application: string): browser.runtime.Port | chrome.runtime.Port {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.connectNative(application);
} else if (BrowserApi.isChromeApi) {
return chrome.runtime.connectNative(application);
}
}
static requestPermission(permission: any) {
if (BrowserApi.isWebExtensionsApi) {
return browser.permissions.request(permission);
}
return new Promise((resolve, reject) => {
chrome.permissions.request(permission, resolve);
});
}
static getPlatformInfo(): Promise<browser.runtime.PlatformInfo | chrome.runtime.PlatformInfo> {
if (BrowserApi.isWebExtensionsApi) {
return browser.runtime.getPlatformInfo();
}
return new Promise((resolve) => {
chrome.runtime.getPlatformInfo(resolve);
});
}
}

View File

@@ -0,0 +1,26 @@
import { BrowserApi } from "./browserApi";
export class SafariApp {
static sendMessageToApp(command: string, data: any = null, resolveNow = false): Promise<any> {
if (!BrowserApi.isSafariApi) {
return Promise.resolve(null);
}
return new Promise((resolve) => {
const now = new Date();
const messageId =
now.getTime().toString() + "_" + Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
(browser as any).runtime.sendNativeMessage(
"com.bitwarden.desktop",
{
id: messageId,
command: command,
data: data,
responseData: null,
},
(response: any) => {
resolve(response);
}
);
});
}
}

View File

@@ -0,0 +1,36 @@
@-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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
document.addEventListener("DOMContentLoaded", (event) => {
let pageHref: string = null;
let filledThisHref = false;
let delayFillTimeout: number;
const activeUserIdKey = "activeUserId";
let activeUserId: string;
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
if (obj == null || obj[activeUserIdKey] == null) {
return;
}
activeUserId = obj[activeUserIdKey];
});
chrome.storage.local.get(activeUserId, (obj: any) => {
if (obj?.[activeUserId]?.settings?.enableAutoFillOnPageLoad === true) {
setInterval(() => doFillIfNeeded(), 500);
}
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.command === "fillForm" && pageHref === msg.url) {
filledThisHref = true;
}
});
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;
if (delayFillTimeout != null) {
window.clearTimeout(delayFillTimeout);
}
delayFillTimeout = window.setTimeout(() => {
if (!filledThisHref) {
doFillIfNeeded(true);
}
}, 1500);
}
pageHref = window.location.href;
const msg: any = {
command: "bgCollectPageDetails",
sender: "autofiller",
};
chrome.runtime.sendMessage(msg);
}
}
});

View File

@@ -0,0 +1,66 @@
const inputTags = ["input", "textarea", "select"];
const labelTags = ["label", "span"];
const attributes = ["id", "name", "label-aria", "placeholder"];
const invalidElement = chrome.i18n.getMessage("copyCustomFieldNameInvalidElement");
const noUniqueIdentifier = chrome.i18n.getMessage("copyCustomFieldNameNotUnique");
let clickedEl: HTMLElement = null;
// Find the best attribute to be used as the Name for an element in a custom field.
function getClickedElementIdentifier() {
if (clickedEl == null) {
return invalidElement;
}
const clickedTag = clickedEl.nodeName.toLowerCase();
let inputEl = null;
// Try to identify the input element (which may not be the clicked element)
if (labelTags.includes(clickedTag)) {
let inputId = null;
if (clickedTag === "label") {
inputId = clickedEl.getAttribute("for");
} else {
inputId = clickedEl.closest("label")?.getAttribute("for");
}
inputEl = document.getElementById(inputId);
} else {
inputEl = clickedEl;
}
if (inputEl == null || !inputTags.includes(inputEl.nodeName.toLowerCase())) {
return invalidElement;
}
for (const attr of attributes) {
const attributeValue = inputEl.getAttribute(attr);
const selector = "[" + attr + '="' + attributeValue + '"]';
if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) {
return attributeValue;
}
}
return noUniqueIdentifier;
}
function isNullOrEmpty(s: string) {
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) => {
clickedEl = event.target as HTMLElement;
});
// Runs when the 'Copy Custom Field Name' context menu item is actually clicked.
chrome.runtime.onMessage.addListener((event) => {
if (event.command === "getClickedElement") {
const identifier = getClickedElementIdentifier();
chrome.runtime.sendMessage({
command: "getClickedElementResponse",
sender: "contextMenuHandler",
identifier: identifier,
});
}
});

View File

@@ -0,0 +1,38 @@
window.addEventListener(
"message",
(event) => {
if (event.source !== window) return;
if (event.data.command && event.data.command === "authResult") {
chrome.runtime.sendMessage({
command: event.data.command,
code: event.data.code,
state: event.data.state,
referrer: event.source.location.hostname,
});
}
if (event.data.command && event.data.command === "webAuthnResult") {
chrome.runtime.sendMessage({
command: event.data.command,
data: event.data.data,
remember: event.data.remember,
referrer: event.source.location.hostname,
});
}
},
false
);
const forwardCommands = [
"promptForLogin",
"addToLockedVaultPendingNotifications",
"unlockCompleted",
"addedCipher",
];
chrome.runtime.onMessage.addListener((event) => {
if (forwardCommands.includes(event.command)) {
chrome.runtime.sendMessage(event);
}
});

View File

@@ -0,0 +1,603 @@
import AddLoginRuntimeMessage from "src/background/models/addLoginRuntimeMessage";
import ChangePasswordRuntimeMessage from "src/background/models/changePasswordRuntimeMessage";
document.addEventListener("DOMContentLoaded", (event) => {
if (window.location.hostname.endsWith("vault.bitwarden.com")) {
return;
}
const pageDetails: any[] = [];
const formData: any[] = [];
let barType: string = null;
let pageHref: string = null;
let observer: MutationObserver = null;
const observeIgnoredElements = new Set([
"a",
"i",
"b",
"strong",
"span",
"code",
"br",
"img",
"small",
"em",
"hr",
]);
let domObservationCollectTimeout: number = null;
let collectIfNeededTimeout: number = null;
let observeDomTimeout: number = null;
const inIframe = isInIframe();
const cancelButtonNames = new Set(["cancel", "close", "back"]);
const logInButtonNames = new Set([
"log in",
"sign in",
"login",
"go",
"submit",
"continue",
"next",
]);
const changePasswordButtonNames = new Set([
"save password",
"update password",
"change password",
"change",
]);
const changePasswordButtonContainsNames = new Set(["pass", "change", "contras", "senha"]);
let disabledAddLoginNotification = false;
let disabledChangedPasswordNotification = false;
const activeUserIdKey = "activeUserId";
let activeUserId: string;
chrome.storage.local.get(activeUserIdKey, (obj: any) => {
if (obj == null || obj[activeUserIdKey] == null) {
return;
}
activeUserId = obj[activeUserIdKey];
});
chrome.storage.local.get(activeUserId, (obj: any) => {
if (obj?.[activeUserId] == null) {
return;
}
const domains = obj[activeUserId].settings.neverDomains;
// eslint-disable-next-line
if (domains != null && domains.hasOwnProperty(window.location.hostname)) {
return;
}
disabledAddLoginNotification = obj[activeUserId].settings.disableAddLoginNotification;
disabledChangedPasswordNotification =
obj[activeUserId].settings.disableChangedPasswordNotification;
if (!disabledAddLoginNotification || !disabledChangedPasswordNotification) {
collectIfNeededWithTimeout();
}
});
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
processMessages(msg, sendResponse);
});
function processMessages(msg: any, sendResponse: (response?: any) => void) {
if (msg.command === "openNotificationBar") {
if (inIframe) {
return;
}
closeExistingAndOpenBar(msg.data.type, msg.data.typeData);
sendResponse();
return true;
} else if (msg.command === "closeNotificationBar") {
if (inIframe) {
return;
}
closeBar(true);
sendResponse();
return true;
} else if (msg.command === "adjustNotificationBar") {
if (inIframe) {
return;
}
adjustBar(msg.data);
sendResponse();
return true;
} else if (msg.command === "notificationBarPageDetails") {
pageDetails.push(msg.data.details);
watchForms(msg.data.forms);
sendResponse();
return true;
}
}
function isInIframe() {
try {
return window.self !== window.top;
} catch {
return true;
}
}
function observeDom() {
const bodies = document.querySelectorAll("body");
if (bodies && bodies.length > 0) {
observer = new MutationObserver((mutations) => {
if (mutations == null || mutations.length === 0 || pageHref !== window.location.href) {
return;
}
let doCollect = false;
for (let i = 0; i < mutations.length; i++) {
const mutation = mutations[i];
if (mutation.addedNodes == null || mutation.addedNodes.length === 0) {
continue;
}
for (let j = 0; j < mutation.addedNodes.length; j++) {
const addedNode: any = mutation.addedNodes[j];
if (addedNode == null) {
continue;
}
const tagName = addedNode.tagName != null ? addedNode.tagName.toLowerCase() : null;
if (
tagName != null &&
tagName === "form" &&
(addedNode.dataset == null || !addedNode.dataset.bitwardenWatching)
) {
doCollect = true;
break;
}
if (
(tagName != null && observeIgnoredElements.has(tagName)) ||
addedNode.querySelectorAll == null
) {
continue;
}
const forms = addedNode.querySelectorAll("form:not([data-bitwarden-watching])");
if (forms != null && forms.length > 0) {
doCollect = true;
break;
}
}
if (doCollect) {
break;
}
}
if (doCollect) {
if (domObservationCollectTimeout != null) {
window.clearTimeout(domObservationCollectTimeout);
}
domObservationCollectTimeout = window.setTimeout(collect, 1000);
}
});
observer.observe(bodies[0], { childList: true, subtree: true });
}
}
function collectIfNeededWithTimeout() {
if (collectIfNeededTimeout != null) {
window.clearTimeout(collectIfNeededTimeout);
}
collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000);
}
function collectIfNeeded() {
if (pageHref !== window.location.href) {
pageHref = window.location.href;
if (observer) {
observer.disconnect();
observer = null;
}
collect();
if (observeDomTimeout != null) {
window.clearTimeout(observeDomTimeout);
}
observeDomTimeout = window.setTimeout(observeDom, 1000);
}
if (collectIfNeededTimeout != null) {
window.clearTimeout(collectIfNeededTimeout);
}
collectIfNeededTimeout = window.setTimeout(collectIfNeeded, 1000);
}
function collect() {
sendPlatformMessage({
command: "bgCollectPageDetails",
sender: "notificationBar",
});
}
function watchForms(forms: any[]) {
if (forms == null || forms.length === 0) {
return;
}
forms.forEach((f: any) => {
const formId: string = f.form != null ? f.form.htmlID : null;
let formEl: HTMLFormElement = null;
if (formId != null && formId !== "") {
formEl = document.getElementById(formId) as HTMLFormElement;
}
if (formEl == null) {
const index = parseInt(f.form.opid.split("__")[2], null);
formEl = document.getElementsByTagName("form")[index];
}
if (formEl != null && formEl.dataset.bitwardenWatching !== "1") {
const formDataObj: any = {
data: f,
formEl: formEl,
usernameEl: null,
passwordEl: null,
passwordEls: null,
};
locateFields(formDataObj);
formData.push(formDataObj);
listen(formEl);
formEl.dataset.bitwardenWatching = "1";
}
});
}
function listen(form: HTMLFormElement) {
form.removeEventListener("submit", formSubmitted, false);
form.addEventListener("submit", formSubmitted, false);
const submitButton = getSubmitButton(form, logInButtonNames);
if (submitButton != null) {
submitButton.removeEventListener("click", formSubmitted, false);
submitButton.addEventListener("click", formSubmitted, false);
}
}
function locateFields(formDataObj: any) {
const inputs = Array.from(document.getElementsByTagName("input"));
formDataObj.usernameEl = locateField(formDataObj.formEl, formDataObj.data.username, inputs);
if (formDataObj.usernameEl != null && formDataObj.data.password != null) {
formDataObj.passwordEl = locatePassword(
formDataObj.formEl,
formDataObj.data.password,
inputs,
true
);
} else if (formDataObj.data.passwords != null) {
formDataObj.passwordEls = [];
formDataObj.data.passwords.forEach((pData: any) => {
const el = locatePassword(formDataObj.formEl, pData, inputs, false);
if (el != null) {
formDataObj.passwordEls.push(el);
}
});
if (formDataObj.passwordEls.length === 0) {
formDataObj.passwordEls = null;
}
}
}
function locatePassword(
form: HTMLFormElement,
passwordData: any,
inputs: HTMLInputElement[],
doLastFallback: boolean
) {
let el = locateField(form, passwordData, inputs);
if (el != null && el.type !== "password") {
el = null;
}
if (doLastFallback && el == null) {
el = form.querySelector('input[type="password"]');
}
return el;
}
function locateField(form: HTMLFormElement, fieldData: any, inputs: HTMLInputElement[]) {
if (fieldData == null) {
return;
}
let el: HTMLInputElement = null;
if (fieldData.htmlID != null && fieldData.htmlID !== "") {
try {
el = form.querySelector("#" + fieldData.htmlID);
} catch {
// Ignore error, we perform fallbacks below.
}
}
if (el == null && fieldData.htmlName != null && fieldData.htmlName !== "") {
el = form.querySelector('input[name="' + fieldData.htmlName + '"]');
}
if (el == null && fieldData.elementNumber != null) {
el = inputs[fieldData.elementNumber];
}
return el;
}
function formSubmitted(e: Event) {
let form: HTMLFormElement = null;
if (e.type === "click") {
form = (e.target as HTMLElement).closest("form");
if (form == null) {
const parentModal = (e.target as HTMLElement).closest("div.modal");
if (parentModal != null) {
const modalForms = parentModal.querySelectorAll("form");
if (modalForms.length === 1) {
form = modalForms[0];
}
}
}
} else {
form = e.target as HTMLFormElement;
}
if (form == null || form.dataset.bitwardenProcessed === "1") {
return;
}
for (let i = 0; i < formData.length; i++) {
if (formData[i].formEl !== form) {
continue;
}
const disabledBoth = disabledChangedPasswordNotification && disabledAddLoginNotification;
if (!disabledBoth && formData[i].usernameEl != null && formData[i].passwordEl != null) {
const login: AddLoginRuntimeMessage = {
username: formData[i].usernameEl.value,
password: formData[i].passwordEl.value,
url: document.URL,
};
if (
login.username != null &&
login.username !== "" &&
login.password != null &&
login.password !== ""
) {
processedForm(form);
sendPlatformMessage({
command: "bgAddLogin",
login: login,
});
break;
}
}
if (!disabledChangedPasswordNotification && formData[i].passwordEls != null) {
const passwords: string[] = formData[i].passwordEls
.filter((el: HTMLInputElement) => el.value != null && el.value !== "")
.map((el: HTMLInputElement) => el.value);
let curPass: string = null;
let newPass: string = null;
let newPassOnly = false;
if (formData[i].passwordEls.length === 3 && passwords.length === 3) {
newPass = passwords[1];
if (passwords[0] !== newPass && newPass === passwords[2]) {
curPass = passwords[0];
} else if (newPass !== passwords[2] && passwords[0] === newPass) {
curPass = passwords[2];
}
} else if (formData[i].passwordEls.length === 2 && passwords.length === 2) {
if (passwords[0] === passwords[1]) {
newPassOnly = true;
newPass = passwords[0];
curPass = null;
} else {
const buttonText = getButtonText(getSubmitButton(form, changePasswordButtonNames));
const matches = Array.from(changePasswordButtonContainsNames).filter(
(n) => buttonText.indexOf(n) > -1
);
if (matches.length > 0) {
curPass = passwords[0];
newPass = passwords[1];
}
}
}
if ((newPass != null && curPass != null) || (newPassOnly && newPass != null)) {
processedForm(form);
const changePasswordRuntimeMessage: ChangePasswordRuntimeMessage = {
newPassword: newPass,
currentPassword: curPass,
url: document.URL,
};
sendPlatformMessage({
command: "bgChangedPassword",
data: changePasswordRuntimeMessage,
});
break;
}
}
}
}
function getSubmitButton(wrappingEl: HTMLElement, buttonNames: Set<string>) {
if (wrappingEl == null) {
return null;
}
const wrappingElIsForm = wrappingEl.tagName.toLowerCase() === "form";
let submitButton = wrappingEl.querySelector(
'input[type="submit"], input[type="image"], ' + 'button[type="submit"]'
) as HTMLElement;
if (submitButton == null && wrappingElIsForm) {
submitButton = wrappingEl.querySelector("button:not([type])");
if (submitButton != null) {
const buttonText = getButtonText(submitButton);
if (buttonText != null && cancelButtonNames.has(buttonText.trim().toLowerCase())) {
submitButton = null;
}
}
}
if (submitButton == null) {
const possibleSubmitButtons = Array.from(
wrappingEl.querySelectorAll(
'a, span, button[type="button"], ' + 'input[type="button"], button:not([type])'
)
) as HTMLElement[];
let typelessButton: HTMLElement = null;
possibleSubmitButtons.forEach((button) => {
if (submitButton != null || button == null || button.tagName == null) {
return;
}
const buttonText = getButtonText(button);
if (buttonText != null) {
if (
typelessButton != null &&
button.tagName.toLowerCase() === "button" &&
button.getAttribute("type") == null &&
!cancelButtonNames.has(buttonText.trim().toLowerCase())
) {
typelessButton = button;
} else if (buttonNames.has(buttonText.trim().toLowerCase())) {
submitButton = button;
}
}
});
if (submitButton == null && typelessButton != null) {
submitButton = typelessButton;
}
}
if (submitButton == null && wrappingElIsForm) {
// Maybe it's in a modal?
const parentModal = wrappingEl.closest("div.modal") as HTMLElement;
if (parentModal != null) {
const modalForms = parentModal.querySelectorAll("form");
if (modalForms.length === 1) {
submitButton = getSubmitButton(parentModal, buttonNames);
}
}
}
return submitButton;
}
function getButtonText(button: HTMLElement) {
let buttonText: string = null;
if (button.tagName.toLowerCase() === "input") {
buttonText = (button as HTMLInputElement).value;
} else {
buttonText = button.innerText;
}
return buttonText;
}
function processedForm(form: HTMLFormElement) {
form.dataset.bitwardenProcessed = "1";
window.setTimeout(() => {
form.dataset.bitwardenProcessed = "0";
}, 500);
}
function closeExistingAndOpenBar(type: string, typeData: any) {
let barPage = "notification/bar.html";
switch (type) {
case "add":
barPage = barPage + "?add=1&isVaultLocked=" + typeData.isVaultLocked;
break;
case "change":
barPage = barPage + "?change=1&isVaultLocked=" + typeData.isVaultLocked;
break;
default:
break;
}
const frame = document.getElementById("bit-notification-bar-iframe") as HTMLIFrameElement;
if (frame != null && frame.src.indexOf(barPage) >= 0) {
return;
}
closeBar(false);
openBar(type, barPage);
}
function openBar(type: string, barPage: string) {
barType = type;
if (document.body == null) {
return;
}
const barPageUrl: string = chrome.extension.getURL(barPage);
const iframe = document.createElement("iframe");
iframe.style.cssText = "height: 42px; width: 100%; border: 0; min-height: initial;";
iframe.id = "bit-notification-bar-iframe";
iframe.src = barPageUrl;
const frameDiv = document.createElement("div");
frameDiv.setAttribute("aria-live", "polite");
frameDiv.id = "bit-notification-bar";
frameDiv.style.cssText =
"height: 42px; width: 100%; top: 0; left: 0; padding: 0; position: fixed; " +
"z-index: 2147483647; visibility: visible;";
frameDiv.appendChild(iframe);
document.body.appendChild(frameDiv);
(iframe.contentWindow.location as any) = barPageUrl;
const spacer = document.createElement("div");
spacer.id = "bit-notification-bar-spacer";
spacer.style.cssText = "height: 42px;";
document.body.insertBefore(spacer, document.body.firstChild);
}
function closeBar(explicitClose: boolean) {
const barEl = document.getElementById("bit-notification-bar");
if (barEl != null) {
barEl.parentElement.removeChild(barEl);
}
const spacerEl = document.getElementById("bit-notification-bar-spacer");
if (spacerEl) {
spacerEl.parentElement.removeChild(spacerEl);
}
if (!explicitClose) {
return;
}
switch (barType) {
case "add":
sendPlatformMessage({
command: "bgAddClose",
});
break;
case "change":
sendPlatformMessage({
command: "bgChangeClose",
});
break;
default:
break;
}
}
function adjustBar(data: any) {
if (data != null && data.height !== 42) {
const newHeight = data.height + "px";
doHeightAdjustment("bit-notification-bar-iframe", newHeight);
doHeightAdjustment("bit-notification-bar", newHeight);
doHeightAdjustment("bit-notification-bar-spacer", newHeight);
}
}
function doHeightAdjustment(elId: string, heightStyle: string) {
const el = document.getElementById(elId);
if (el != null) {
el.style.height = heightStyle;
}
}
function sendPlatformMessage(msg: any) {
chrome.runtime.sendMessage(msg);
}
});

View File

@@ -0,0 +1,52 @@
import * as Mousetrap from "mousetrap";
document.addEventListener("DOMContentLoaded", (event) => {
const isSafari =
typeof safari !== "undefined" &&
navigator.userAgent.indexOf(" Safari/") !== -1 &&
navigator.userAgent.indexOf("Chrome") === -1;
const isVivaldi = !isSafari && navigator.userAgent.indexOf(" Vivaldi/") !== -1;
if (!isSafari && !isVivaldi) {
return;
}
if (isSafari && (window as any).__bitwardenFrameId == null) {
(window as any).__bitwardenFrameId = Math.floor(Math.random() * Math.floor(99999999));
}
Mousetrap.prototype.stopCallback = () => {
return false;
};
let autofillCommand = ["mod+shift+l"];
if (isSafari) {
autofillCommand = ["mod+\\", "mod+8", "mod+shift+p"];
}
Mousetrap.bind(autofillCommand, () => {
sendMessage("autofill_login");
});
if (isSafari) {
Mousetrap.bind("mod+shift+y", () => {
sendMessage("open_popup");
});
Mousetrap.bind("mod+shift+s", () => {
sendMessage("lock_vault");
});
} else {
Mousetrap.bind("mod+shift+9", () => {
sendMessage("generate_password");
});
}
function sendMessage(shortcut: string) {
const msg: any = {
command: "keyboardShortcutTriggered",
shortcut: shortcut,
};
chrome.runtime.sendMessage(msg);
}
});

4
apps/browser/src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare function escape(s: string): string;
declare function unescape(s: string): string;
declare let opr: any;
declare let safari: any;

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Some files were not shown because too many files have changed in this diff Show More