1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-02 23:11:40 +01:00

[SG-520] Native messaging handler (#3566)

* [SG-523] Base test runner app for native messages (#3269)

* Base test runner app for native messages

* Remove default test script

* Add case for canceled status

* Modify to allow usage of libs crypto services and functions

* Small adjustments

* Handshake request (#3277)

* Handshake request

* Fix capitalization

* Update info text

* lock node-ipc to 9.2.1

* [SG-569] Native Messaging settings bug (#3285)

* Fix bug where updating setting wasn't starting the native messaging listener

* Update test runner error message

* [SG-532] Implement Status command in Native Messaging Service (#3310)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Add active field to status response

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* Remove in progress file (merge error)

* Move models to their own folder and add index.ts

* Remove file that got un-deleted

* Remove file that will be added in separate command

* Fix imports that got borked

* [SG-533] Implement bw-credential-retrieval (#3334)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Implement bw-credential-retrieval

* Add active field to status response

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* Add error handling for passing a bad public key to handshake

* [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342)

* Status command start

* Refactor ipc test service and add status command

* fixed linter errors

* Move types into a model file

* Cleanup and comments

* Fix auth status condition

* Remove .vscode settings file. Fix this in a separate work item

* Implement bw-credential-retrieval

* Add active field to status response

* Add bw-credential-create

* Better response handling in test runner

* Extract native messaging types into their own files

* Remove experimental decorators

* Turn off no console lint rule for the test runner

* Casing fix

* Models import casing fixes

* bw-cipher-create move type into its own file

* Use LogUtils for all logging

* Implement bw-credential-update

* Give naming conventions for types

* Rename file correctly

* Update handleEncyptedMessage with EncString changes

* [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380)

* Add MessagingService to send messages on login create and update

* Add `not-active-user` error to create and update and other refactors

* [SG-536] Implement bw-generate-password (#3370)

* implement bw-generate-password

* Fix merge conflict resolution errors

* Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Logging improvements

* Add NativeMessagingVersion enum

* Add version check in NativeMessagingHandler

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>

* Refactor account status checks and check for locked state in generate command (#3461)

* Add feawture flag to show/hide ddg setting (#3506)

* [SG-649] Add confirmation dialog and tweak shared key retrieval  (#3451)

* Add confirmation dialog when completing handshake

* Copy updates for dialog

* HandshakeResponse type fixes

* Add longer timeout for handshake command

* [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551)

* NativeMessageHandlerService refactor and additional types

* Return empty array if no uri to retrieve command

* Move commands from test runner into a separate folder

* Fix bug where confirmation dialog messes with styling

* Enable DDG feature

* Fix generated password not saving to history

* Take credentialId as parameter to update

* Add applicationName to handshake payload

* Add warning text to confirmation modal

Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
Robyn MacCallum 2022-09-23 15:47:17 -04:00 committed by GitHub
parent 32eac70c82
commit f4e61d1cec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 2386 additions and 27 deletions

View File

@ -1,4 +1,6 @@
{
"devFlags": {},
"flags": {}
"flags": {
"showDDGSetting": true
}
}

View File

@ -1,3 +1,5 @@
{
"flags": {}
"flags": {
"showDDGSetting": true
}
}

View File

@ -0,0 +1,5 @@
{
"rules": {
"no-console": "off"
}
}

View File

@ -0,0 +1,681 @@
{
"name": "native-messaging-test-runner",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "native-messaging-test-runner",
"version": "1.0.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/node": "file:../../../libs/node",
"module-alias": "^2.2.2",
"node-ipc": "9.2.1",
"ts-node": "^10.9.1",
"uuid": "^8.3.2",
"yargs": "^17.5.1"
},
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
"@types/node": "^18.6.5",
"@types/node-ipc": "9.2.0",
"typescript": "^4.7.4"
}
},
"../../../libs/common": {
"name": "@bitwarden/common",
"version": "0.0.0",
"license": "GPL-3.0"
},
"../../../libs/node": {
"name": "@bitwarden/node",
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/common": "file:../common"
}
},
"node_modules/@bitwarden/common": {
"resolved": "../../../libs/common",
"link": true
},
"node_modules/@bitwarden/node": {
"resolved": "../../../libs/node",
"link": true
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.3",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.6.5",
"license": "MIT"
},
"node_modules/@types/node-ipc": {
"version": "9.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/acorn": {
"version": "8.8.0",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/arg": {
"version": "4.1.3",
"license": "MIT"
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/create-require": {
"version": "1.1.1",
"license": "MIT"
},
"node_modules/diff": {
"version": "4.0.2",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"engines": {
"node": ">=6"
}
},
"node_modules/event-pubsub": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/js-queue": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
"dependencies": {
"easy-stack": "^1.0.1"
},
"engines": {
"node": ">=1.0.0"
}
},
"node_modules/make-error": {
"version": "1.3.6",
"license": "ISC"
},
"node_modules/module-alias": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz",
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q=="
},
"node_modules/node-ipc": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
"dependencies": {
"event-pubsub": "4.3.0",
"js-message": "1.0.7",
"js-queue": "2.0.2"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ts-node": {
"version": "10.9.1",
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/typescript": {
"version": "4.7.4",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/yn": {
"version": "3.1.1",
"license": "MIT",
"engines": {
"node": ">=6"
}
}
},
"dependencies": {
"@bitwarden/common": {
"version": "file:../../../libs/common"
},
"@bitwarden/node": {
"version": "file:../../../libs/node",
"requires": {
"@bitwarden/common": "file:../common"
}
},
"@cspotcode/source-map-support": {
"version": "0.8.1",
"requires": {
"@jridgewell/trace-mapping": "0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0"
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14"
},
"@jridgewell/trace-mapping": {
"version": "0.3.9",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@tsconfig/node10": {
"version": "1.0.9"
},
"@tsconfig/node12": {
"version": "1.0.11"
},
"@tsconfig/node14": {
"version": "1.0.3"
},
"@tsconfig/node16": {
"version": "1.0.3"
},
"@types/node": {
"version": "18.6.5"
},
"@types/node-ipc": {
"version": "9.2.0",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"acorn": {
"version": "8.8.0"
},
"acorn-walk": {
"version": "8.2.0"
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"arg": {
"version": "4.1.3"
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"requires": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"create-require": {
"version": "1.1.1"
},
"diff": {
"version": "4.0.2"
},
"easy-stack": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
},
"event-pubsub": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ=="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"js-message": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA=="
},
"js-queue": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
"requires": {
"easy-stack": "^1.0.1"
}
},
"make-error": {
"version": "1.3.6"
},
"module-alias": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz",
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q=="
},
"node-ipc": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
"requires": {
"event-pubsub": "4.3.0",
"js-message": "1.0.7",
"js-queue": "2.0.2"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
},
"string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
}
},
"strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"requires": {
"ansi-regex": "^5.0.1"
}
},
"ts-node": {
"version": "10.9.1",
"requires": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
}
},
"typescript": {
"version": "4.7.4"
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"v8-compile-cache-lib": {
"version": "3.0.1"
},
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
},
"yargs": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"requires": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.0.0"
}
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="
},
"yn": {
"version": "3.1.1"
}
}
}

View File

@ -0,0 +1,35 @@
{
"name": "native-messaging-test-runner",
"version": "1.0.0",
"description": "Test runner for Desktop native messaging",
"main": "dist/bw-handshake.ts",
"scripts": {
"handshake": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.js",
"status": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-status.js",
"retrieve": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.js",
"create": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.js",
"update": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.js",
"generate": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.js"
},
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/common": "file:../../../libs/common",
"@bitwarden/node": "file:../../../libs/node",
"module-alias": "^2.2.2",
"node-ipc": "9.2.1",
"ts-node": "^10.9.1",
"uuid": "^8.3.2",
"yargs": "^17.5.1"
},
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
"@types/node": "^18.6.5",
"@types/node-ipc": "9.2.0",
"typescript": "^4.7.4"
},
"_moduleAliases": {
"@bitwarden/common": "dist/libs/common/src",
"@bitwarden/node/services/nodeCryptoFunction.service": "dist/libs/node/src/services/nodeCryptoFunction.service"
}
}

View File

@ -0,0 +1,66 @@
import "module-alias/register";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { CredentialCreatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("name", {
alias: "n",
demand: true,
describe: "Name that the created login will be given",
type: "string",
}).argv;
const { name } = argv;
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
const handshakeResponse = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
if (!handshakeResponse.status) {
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
nativeMessageService.disconnect();
return;
}
// Get active account userId
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
if (activeUser === undefined) {
LogUtils.logError("No active or unlocked user");
}
LogUtils.logInfo("Active userId: " + activeUser.id);
LogUtils.logSuccess("Handshake success response");
const response = await nativeMessageService.credentialCreation(handshakeResponse.sharedKey, {
name: name,
userName: "SuperAwesomeUser",
password: "dolhpin",
uri: "google.com",
userId: activeUser.id,
} as CredentialCreatePayload);
if (response.payload.status === "failure") {
LogUtils.logError("Failure response returned ");
} else if (response.payload.status === "success") {
LogUtils.logSuccess("Success response returned ");
} else if (response.payload.error === "locked") {
LogUtils.logError("Error: vault is locked");
} else {
LogUtils.logWarning("Other response: ", response);
}
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,46 @@
import "module-alias/register";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("uri", {
alias: "u",
demand: true,
describe: "The uri to retrieve logins for",
type: "string",
}).argv;
const { uri } = argv;
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
const handshakeResponse = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
if (!handshakeResponse.status) {
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
nativeMessageService.disconnect();
return;
}
LogUtils.logSuccess("Handshake success response");
const response = await nativeMessageService.credentialRetrieval(handshakeResponse.sharedKey, uri);
if (response.payload.error != null) {
LogUtils.logError("Error response returned: ", response.payload.error);
} else {
LogUtils.logSuccess("Credentials returned ", response);
}
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,89 @@
import "module-alias/register";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { CredentialUpdatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
// Command line arguments
const argv: any = yargs(hideBin(process.argv))
.option("name", {
alias: "n",
demand: true,
describe: "Name that the updated login will be given",
type: "string",
})
.option("username", {
alias: "u",
demand: true,
describe: "Username that the login will be given",
type: "string",
})
.option("password", {
alias: "p",
demand: true,
describe: "Password that the login will be given",
type: "string",
})
.option("uri", {
demand: true,
describe: "Uri that the login will be given",
type: "string",
})
.option("credentialId", {
demand: true,
describe: "GUID of the credential to update",
type: "string",
}).argv;
const { name, username, password, uri, credentialId } = argv;
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
const handshakeResponse = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
if (!handshakeResponse.status) {
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
nativeMessageService.disconnect();
return;
}
LogUtils.logSuccess("Handshake success response");
// Get active account userId
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0];
if (activeUser === undefined) {
LogUtils.logError("No active or unlocked user");
}
LogUtils.logInfo("Active userId: " + activeUser.id);
const response = await nativeMessageService.credentialUpdate(handshakeResponse.sharedKey, {
name: name,
password: password,
userName: username,
uri: uri,
userId: activeUser.id,
credentialId: credentialId,
} as CredentialUpdatePayload);
if (response.payload.status === "failure") {
LogUtils.logError("Failure response returned ");
} else if (response.payload.status === "success") {
LogUtils.logSuccess("Success response returned ");
} else {
LogUtils.logWarning("Other response: ", response);
}
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,46 @@
import "module-alias/register";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
const argv: any = yargs(hideBin(process.argv)).option("userId", {
alias: "u",
demand: true,
describe: "UserId to generate password for",
type: "string",
}).argv;
const { userId } = argv;
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
// Handshake
LogUtils.logInfo("Sending Handshake");
const handshakeResponse = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
if (!handshakeResponse.status) {
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
nativeMessageService.disconnect();
return;
}
LogUtils.logSuccess("Handshake success response");
const response = await nativeMessageService.generatePassword(handshakeResponse.sharedKey, userId);
if (response.payload.error != null) {
LogUtils.logError("Error response returned: ", response.payload.error);
} else {
LogUtils.logSuccess("Response: ", response);
}
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,25 @@
import "module-alias/register";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
const response = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
LogUtils.logSuccess("Received response to handshake request");
if (response.status) {
LogUtils.logSuccess("Handshake success response");
} else if (response.error === "canceled") {
LogUtils.logWarning("Handshake canceled by user");
} else {
LogUtils.logError("Handshake failure response");
}
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,29 @@
import "module-alias/register";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { LogUtils } from "../logUtils";
import NativeMessageService from "../nativeMessageService";
import * as config from "../variables";
(async () => {
const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One);
LogUtils.logInfo("Sending Handshake");
const handshakeResponse = await nativeMessageService.sendHandshake(
config.testRsaPublicKey,
config.applicationName
);
LogUtils.logSuccess("Received response to handshake request");
if (!handshakeResponse.status) {
LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error);
nativeMessageService.disconnect();
return;
}
LogUtils.logSuccess("Handshake success response");
const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey);
LogUtils.logSuccess("Status output is: ", status);
nativeMessageService.disconnect();
})();

View File

@ -0,0 +1,26 @@
// Wrapper for a promise that we can await the promise in one case
// while allowing an unrelated event to fulfill it elsewhere.
export default class Deferred<T> {
private promise: Promise<T>;
private resolver: (T?) => void;
private rejecter: (Error?) => void;
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.resolver = resolve;
this.rejecter = reject;
});
}
resolve(value?: T) {
this.resolver(value);
}
reject(error?: Error) {
this.rejecter(error);
}
getPromise(): Promise<T> {
return this.promise;
}
}

View File

@ -0,0 +1,166 @@
import { homedir } from "os";
import * as NodeIPC from "node-ipc";
import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon";
import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse";
import Deferred from "./deferred";
import { race } from "./race";
NodeIPC.config.id = "native-messaging-test-runner";
NodeIPC.config.maxRetries = 0;
NodeIPC.config.silent = true;
const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`;
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
export type MessageHandler = (MessageCommon) => void;
export enum IPCConnectionState {
Disconnected = "disconnected",
Connecting = "connecting",
Connected = "connected",
}
export type IPCOptions = {
overrideTimeout?: number;
};
export default class IPCService {
// The current connection state of the socket.
private connectionState: IPCConnectionState = IPCConnectionState.Disconnected;
// Messages that have been sent, but have not yet received responses
private pendingMessages = new Map<string, Deferred<UnencryptedMessageResponse>>();
// A set of deferred promises that are awaiting socket connection
private awaitingConnection = new Set<Deferred<void>>();
constructor(private socketName: string, private messageHandler: MessageHandler) {}
async connect(): Promise<void> {
console.log("[IPCService] connecting...");
if (this.connectionState === IPCConnectionState.Connected) {
// Socket is already connected. Don't throw, just allow the callsite to proceed
return;
}
const deferredConnections = new Deferred<void>();
this.awaitingConnection.add(deferredConnections);
// If the current connection state is disconnected, we should start trying to connect.
// The only other possible connection state at this point is "connecting" and if this
// is the case, we just want to add a deferred promise to the awaitingConnection collection
// and not try to initiate the connection again.
if (this.connectionState === IPCConnectionState.Disconnected) {
this._connect();
}
return deferredConnections.getPromise();
}
private _connect() {
this.connectionState = IPCConnectionState.Connecting;
NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => {
// Process incoming message
this.getSocket().on("message", (message: any) => {
this.processMessage(message);
});
this.getSocket().on("error", (error: Error) => {
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
// invoked multiple times each time a connection error happens
console.log("[IPCService] errored");
console.log(
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m"
);
this.awaitingConnection.forEach((deferred) => {
console.log(`rejecting: ${deferred}`);
deferred.reject(error);
});
this.awaitingConnection.clear();
});
this.getSocket().on("connect", () => {
console.log("[IPCService] connected");
this.connectionState = IPCConnectionState.Connected;
this.awaitingConnection.forEach((deferred) => {
deferred.resolve(null);
});
this.awaitingConnection.clear();
});
this.getSocket().on("disconnect", () => {
console.log("[IPCService] disconnected");
this.connectionState = IPCConnectionState.Disconnected;
});
});
}
disconnect() {
console.log("[IPCService] disconnecting...");
if (this.connectionState !== IPCConnectionState.Disconnected) {
NodeIPC.disconnect(this.socketName);
}
}
async sendMessage(
message: MessageCommon,
options: IPCOptions = {}
): Promise<UnencryptedMessageResponse> {
console.log("[IPCService] sendMessage");
if (this.pendingMessages.has(message.messageId)) {
throw new Error(`A message with the id: ${message.messageId} has already been sent.`);
}
// Creates a new deferred promise that allows us to convert a message received over the IPC socket
// into a response for a message that we previously sent. This mechanism relies on the fact that we
// create a unique message id and attach it with each message. Response messages are expected to
// include the message id of the message they are responding to.
const deferred = new Deferred<UnencryptedMessageResponse>();
this.pendingMessages.set(message.messageId, deferred);
this.getSocket().emit("message", message);
try {
// Since we can not guarentee that a response message will ever be sent, we put a timeout
// on messages
return race({
promise: deferred.getPromise(),
timeout: options?.overrideTimeout ?? DEFAULT_MESSAGE_TIMEOUT,
error: new Error(`Message: ${message.messageId} timed out`),
});
} catch (error) {
// If there is a timeout, remove the message from the pending messages set
// before triggering error handling elsewhere.
this.pendingMessages.delete(message.messageId);
throw error;
}
}
private getSocket() {
return NodeIPC.of[this.socketName];
}
private processMessage(message: any) {
// If the message is a response to a previous message, resolve the deferred promise that
// is awaiting that response. Otherwise, assume this was a new message that wasn't sent as a
// response and invoke the message handler.
if (message.messageId && this.pendingMessages.has(message.messageId)) {
const deferred = this.pendingMessages.get(message.messageId);
// In the future, this could be improved to add ability to reject, but most messages coming in are
// encrypted at this point so we're unable to determine if they contain error info.
deferred.resolve(message);
this.pendingMessages.delete(message.messageId);
} else {
this.messageHandler(message);
}
}
}

View File

@ -0,0 +1,29 @@
/* eslint-disable no-console */
// Class for logging messages with colors for ease of reading important info
// Reference: https://stackoverflow.com/a/41407246
export class LogUtils {
static logSuccess(message: string, payload?: any): void {
this.logFormat(message, "32", payload);
}
static logWarning(message: string, payload?: any): void {
this.logFormat(message, "33", payload);
}
static logError(message: string, payload?: any): void {
this.logFormat(message, "31", payload);
}
static logInfo(message: string, payload?: any): void {
this.logFormat(message, "36", payload);
}
private static logFormat(message: string, color: string, payload?: any) {
if (payload) {
console.log(`\x1b[${color}m ${message} \x1b[0m`, payload);
} else {
console.log(`\x1b[${color}m ${message} \x1b[0m`);
}
}
}

View File

@ -0,0 +1,235 @@
import "module-alias/register";
import { v4 as uuidv4 } from "uuid";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { EncryptService } from "@bitwarden/common/services/encrypt.service";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service";
import { DecryptedCommandData } from "../../src/models/nativeMessaging/decryptedCommandData";
import { EncryptedMessage } from "../../src/models/nativeMessaging/encryptedMessage";
import { CredentialCreatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
import { CredentialUpdatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
import { EncryptedMessageResponse } from "../../src/models/nativeMessaging/encryptedMessageResponse";
import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon";
import { UnencryptedMessage } from "../../src/models/nativeMessaging/unencryptedMessage";
import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse";
import IPCService, { IPCOptions } from "./ipcService";
import * as config from "./variables";
type HandshakeResponse = {
status: boolean;
sharedKey: string;
error?: "canceled" | "cannot-decrypt";
};
const CONFIRMATION_MESSAGE_TIMEOUT = 100 * 1000; // 100 seconds
export default class NativeMessageService {
private ipcService: IPCService;
private nodeCryptoFunctionService: NodeCryptoFunctionService;
private encryptService: EncryptService;
constructor(private apiVersion: number) {
console.log("Starting native messaging service");
this.ipcService = new IPCService(`bitwarden`, (rawMessage) => {
console.log(`Received unexpected: `, rawMessage);
});
this.nodeCryptoFunctionService = new NodeCryptoFunctionService();
this.encryptService = new EncryptService(
this.nodeCryptoFunctionService,
new ConsoleLogService(false),
false
);
}
// Commands
async sendHandshake(publicKey: string, applicationName: string): Promise<HandshakeResponse> {
const rawResponse = await this.sendUnencryptedMessage(
{
command: "bw-handshake",
payload: {
publicKey,
applicationName: applicationName,
},
},
{
overrideTimeout: CONFIRMATION_MESSAGE_TIMEOUT,
}
);
return rawResponse.payload as HandshakeResponse;
}
async checkStatus(key: string): Promise<DecryptedCommandData> {
const encryptedCommand = await this.encryptCommandData(
{
command: "bw-status",
},
key
);
const response = await this.sendEncryptedMessage({
encryptedCommand,
});
return this.decryptResponsePayload(response.encryptedPayload, key);
}
async credentialRetrieval(key: string, uri: string): Promise<DecryptedCommandData> {
const encryptedCommand = await this.encryptCommandData(
{
command: "bw-credential-retrieval",
payload: {
uri: uri,
},
},
key
);
const response = await this.sendEncryptedMessage({
encryptedCommand,
});
return this.decryptResponsePayload(response.encryptedPayload, key);
}
async credentialCreation(
key: string,
credentialData: CredentialCreatePayload
): Promise<DecryptedCommandData> {
const encryptedCommand = await this.encryptCommandData(
{
command: "bw-credential-create",
payload: credentialData,
},
key
);
const response = await this.sendEncryptedMessage({
encryptedCommand,
});
return this.decryptResponsePayload(response.encryptedPayload, key);
}
async credentialUpdate(
key: string,
credentialData: CredentialUpdatePayload
): Promise<DecryptedCommandData> {
const encryptedCommand = await this.encryptCommandData(
{
command: "bw-credential-update",
payload: credentialData,
},
key
);
const response = await this.sendEncryptedMessage({
encryptedCommand,
});
return this.decryptResponsePayload(response.encryptedPayload, key);
}
async generatePassword(key: string, userId: string): Promise<DecryptedCommandData> {
const encryptedCommand = await this.encryptCommandData(
{
command: "bw-generate-password",
payload: {
userId: userId,
},
},
key
);
const response = await this.sendEncryptedMessage({
encryptedCommand,
});
return this.decryptResponsePayload(response.encryptedPayload, key);
}
// Private message sending
private async sendEncryptedMessage(
message: Omit<EncryptedMessage, keyof MessageCommon>,
options: IPCOptions = {}
): Promise<EncryptedMessageResponse> {
const result = await this.sendMessage(message, options);
return result as EncryptedMessageResponse;
}
private async sendUnencryptedMessage(
message: Omit<UnencryptedMessage, keyof MessageCommon>,
options: IPCOptions = {}
): Promise<UnencryptedMessageResponse> {
const result = await this.sendMessage(message, options);
return result as UnencryptedMessageResponse;
}
private async sendMessage(
message:
| Omit<UnencryptedMessage, keyof MessageCommon>
| Omit<EncryptedMessage, keyof MessageCommon>,
options: IPCOptions
): Promise<EncryptedMessageResponse | UnencryptedMessageResponse> {
// Attempt to connect before sending any messages. If the connection has already
// been made, this is a NOOP within the IPCService.
await this.ipcService.connect();
const commonFields: MessageCommon = {
// Create a messageId that can be used as a lookup when we get a response
messageId: uuidv4(),
version: this.apiVersion,
};
const fullMessage: UnencryptedMessage | EncryptedMessage = {
...message,
...commonFields,
};
console.log(`[NativeMessageService] sendMessage with id: ${fullMessage.messageId}`);
const response = await this.ipcService.sendMessage(fullMessage, options);
console.log(`[NativeMessageService] received response for message: ${fullMessage.messageId}`);
return response;
}
disconnect() {
this.ipcService.disconnect();
}
// Data Encryption
private async encryptCommandData(
commandData: DecryptedCommandData,
key: string
): Promise<EncString> {
const commandDataString = JSON.stringify(commandData);
const sharedKey = await this.getSharedKeyForKey(key);
return this.encryptService.encrypt(commandDataString, sharedKey);
}
private async decryptResponsePayload(
payload: EncString,
key: string
): Promise<DecryptedCommandData> {
const sharedKey = await this.getSharedKeyForKey(key);
const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey);
return JSON.parse(decrypted);
}
private async getSharedKeyForKey(key: string): Promise<SymmetricCryptoKey> {
const dataBuffer = Utils.fromB64ToArray(key).buffer;
const privKey = Utils.fromB64ToArray(config.testRsaPrivateKey).buffer;
return new SymmetricCryptoKey(
await this.nodeCryptoFunctionService.rsaDecrypt(dataBuffer, privKey, "sha1")
);
}
}

View File

@ -0,0 +1,25 @@
export const race = <T>({
promise,
timeout,
error,
}: {
promise: Promise<T>;
timeout: number;
error?: Error;
}) => {
let timer = null;
// Similar to Promise.all, but instead of waiting for all, it resolves once one promise finishes.
// Using this so we can reject if the timeout threshold is hit
return Promise.race([
new Promise<T>((_, reject) => {
timer = setTimeout(reject, timeout, error);
return timer;
}),
promise.then((value) => {
clearTimeout(timer);
return value;
}),
]);
};

View File

@ -0,0 +1,27 @@
export const applicationName = "Native Messaging Test Runner";
export const encryptionAlogrithm = "sha1";
export const testRsaPublicKey =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
"4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" +
"RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" +
"084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" +
"xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB";
export const testRsaPrivateKey =
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" +
"YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" +
"nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" +
"YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" +
"PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" +
"Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" +
"WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" +
"5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" +
"1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" +
"BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" +
"TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" +
"q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" +
"q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" +
"5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" +
"eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" +
"Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" +
"+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" +
"BokBGnjFnTnKcs7nv/O8=";

View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"baseUrl": "./",
"outDir": "dist",
"target": "es6",
"module": "CommonJS",
"moduleResolution": "node",
"sourceMap": false,
"declaration": false,
"paths": {
"@src/*": ["src/*"],
"@bitwarden/node/*": ["../../../libs/node/src/*"],
"@bitwarden/common/*": ["../../../libs/common/src/*"]
}
},
"exclude": ["node_modules"]
}

View File

@ -309,6 +309,23 @@
</div>
<small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small>
</div>
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
<div class="checkbox">
<label for="enableDuckDuckGoBrowserIntegration">
<input
id="enableDuckDuckGoBrowserIntegration"
type="checkbox"
name="enableDuckDuckGoBrowserIntegration"
[(ngModel)]="enableDuckDuckGoBrowserIntegration"
(change)="saveDdgBrowserIntegration()"
/>
{{ "enableDuckDuckGoBrowserIntegration" | i18n }}
</label>
</div>
<small class="help-block">{{
"enableDuckDuckGoBrowserIntegrationDesc" | i18n
}}</small>
</div>
<div class="form-group">
<div class="checkbox">
<label for="enableBrowserIntegrationFingerprint">

View File

@ -16,6 +16,7 @@ import { ThemeType } from "@bitwarden/common/enums/themeType";
import { Utils } from "@bitwarden/common/misc/utils";
import { isWindowsStore } from "@bitwarden/electron/utils";
import { flagEnabled } from "../../flags";
import { SetPinComponent } from "../components/set-pin.component";
@Component({
@ -28,6 +29,7 @@ export class SettingsComponent implements OnInit {
pin: boolean = null;
enableFavicons = false;
enableBrowserIntegration = false;
enableDuckDuckGoBrowserIntegration = false;
enableBrowserIntegrationFingerprint = false;
enableMinToTray = false;
enableCloseToTray = false;
@ -51,6 +53,7 @@ export class SettingsComponent implements OnInit {
showAlwaysShowDock = false;
openAtLogin: boolean;
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
enableTrayText: string;
enableTrayDescText: string;
@ -102,6 +105,9 @@ export class SettingsComponent implements OnInit {
this.startToTrayText = this.i18nService.t(startToTrayKey);
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
// DuckDuckGo browser is only for macos initially
this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac;
this.vaultTimeouts = [
// { name: i18nService.t('immediately'), value: 0 },
{ name: i18nService.t("oneMinute"), value: 1 },
@ -188,6 +194,8 @@ export class SettingsComponent implements OnInit {
// Account preferences
this.enableFavicons = !(await this.stateService.getDisableFavicon());
this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration();
this.enableDuckDuckGoBrowserIntegration =
await this.stateService.getEnableDuckDuckGoBrowserIntegration();
this.enableBrowserIntegrationFingerprint =
await this.stateService.getEnableBrowserIntegrationFingerprint();
this.clearClipboard = await this.stateService.getClearClipboard();
@ -432,6 +440,22 @@ export class SettingsComponent implements OnInit {
}
}
async saveDdgBrowserIntegration() {
await this.stateService.setEnableDuckDuckGoBrowserIntegration(
this.enableDuckDuckGoBrowserIntegration
);
if (!this.enableBrowserIntegration) {
await this.stateService.setDuckDuckGoSharedKey(null);
}
this.messagingService.send(
this.enableDuckDuckGoBrowserIntegration
? "enableDuckDuckGoBrowserIntegration"
: "disableDuckDuckGoBrowserIntegration"
);
}
async saveBrowserIntegrationFingerprint() {
await this.stateService.setEnableBrowserIntegrationFingerprint(
this.enableBrowserIntegrationFingerprint

View File

@ -12,7 +12,9 @@ import {
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service";
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
@ -22,8 +24,10 @@ import {
LogService as LogServiceAbstraction,
} from "@bitwarden/common/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
@ -41,7 +45,9 @@ import { ElectronRendererSecureStorageService } from "@bitwarden/electron/servic
import { ElectronRendererStorageService } from "@bitwarden/electron/services/electronRendererStorage.service";
import { Account } from "../../models/account";
import { EncryptedMessageHandlerService } from "../../services/encryptedMessageHandlerService";
import { I18nService } from "../../services/i18n.service";
import { NativeMessageHandlerService } from "../../services/nativeMessageHandler.service";
import { NativeMessagingService } from "../../services/nativeMessaging.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
@ -147,6 +153,28 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
provide: AbstractThemingService,
useClass: DesktopThemingService,
},
{
provide: EncryptedMessageHandlerService,
deps: [
StateServiceAbstraction,
AuthServiceAbstraction,
CipherServiceAbstraction,
PolicyServiceAbstraction,
MessagingServiceAbstraction,
PasswordGenerationServiceAbstraction,
],
},
{
provide: NativeMessageHandlerService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction,
MessagingServiceAbstraction,
I18nServiceAbstraction,
EncryptedMessageHandlerService,
],
},
],
})
export class ServicesModule {}

View File

@ -8,7 +8,9 @@ import {
// required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */
export type Flags = {} & SharedFlags;
export type Flags = {
showDDGSetting?: boolean;
} & SharedFlags;
// required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */

View File

@ -1570,6 +1570,12 @@
"enableBrowserIntegrationDesc": {
"message": "Used for biometrics in browser."
},
"enableDuckDuckGoBrowserIntegration": {
"message": "Allow DuckDuckGo browser integration"
},
"enableDuckDuckGoBrowserIntegrationDesc": {
"message": "Use your Bitwarden vault when browsing with DuckDuckGo."
},
"browserIntegrationUnsupportedTitle": {
"message": "Browser integration not supported"
},
@ -1597,6 +1603,21 @@
"verifyBrowserDesc": {
"message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension."
},
"verifyNativeMessagingConnectionTitle": {
"message": "$APPID$ wants to connect to Bitwarden",
"placeholders": {
"appid": {
"content": "$1",
"example": "My App"
}
}
},
"verifyNativeMessagingConnectionDesc": {
"message": "Would you like to approve this request?"
},
"verifyNativeMessagingConnectionWarning": {
"message": "If you did not initiate this request, do not approve it."
},
"biometricsNotEnabledTitle": {
"message": "Biometrics not enabled"
},
@ -1985,15 +2006,15 @@
"organizationIsDisabled": {
"message": "Organization is disabled."
},
"disabledOrganizationFilterError" : {
"message" : "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
"disabledOrganizationFilterError": {
"message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
},
"neverLockWarning": {
"message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected."
},
"cardBrandMir": {
"message": "Mir"
},
},
"vault": {
"message": "Vault"
}

View File

@ -171,7 +171,10 @@ export class Main {
await this.biometricMain.init();
}
if (await this.stateService.getEnableBrowserIntegration()) {
if (
(await this.stateService.getEnableBrowserIntegration()) ||
(await this.stateService.getEnableDuckDuckGoBrowserIntegration())
) {
this.nativeMessagingMain.listen();
}

View File

@ -68,10 +68,18 @@ export class MessagingMain {
this.main.nativeMessagingMain.generateManifests();
this.main.nativeMessagingMain.listen();
break;
case "enableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.generateDdgManifests();
this.main.nativeMessagingMain.listen();
break;
case "disableBrowserIntegration":
this.main.nativeMessagingMain.removeManifests();
this.main.nativeMessagingMain.stop();
break;
case "disableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.removeDdgManifests();
this.main.nativeMessagingMain.stop();
break;
default:
break;
}

View File

@ -163,6 +163,27 @@ export class NativeMessagingMain {
}
}
generateDdgManifests() {
const manifest = {
name: "com.8bit.bitwarden",
description: "Bitwarden desktop <-> DuckDuckGo bridge",
path: this.binaryPath(),
type: "stdio",
};
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
this.writeManifest(path, manifest).catch((e) =>
this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`)
);
break;
}
default:
break;
}
}
removeManifests() {
switch (process.platform) {
case "win32":
@ -217,6 +238,21 @@ export class NativeMessagingMain {
}
}
removeDdgManifests() {
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
if (existsSync(path)) {
fs.unlink(path);
}
break;
}
default:
break;
}
}
private getDarwinNMHS() {
/* eslint-disable no-useless-escape */
return {

View File

@ -0,0 +1,6 @@
import { EncryptedCommand } from "./encryptedCommand";
export type DecryptedCommandData = {
command: EncryptedCommand;
payload?: any;
};

View File

@ -0,0 +1,6 @@
export type EncryptedCommand =
| "bw-status"
| "bw-credential-retrieval"
| "bw-credential-create"
| "bw-credential-update"
| "bw-generate-password";

View File

@ -0,0 +1,8 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { MessageCommon } from "./messageCommon";
export type EncryptedMessage = MessageCommon & {
// Will decrypt to a DecryptedCommandData object
encryptedCommand: EncString;
};

View File

@ -0,0 +1,7 @@
export type CredentialCreatePayload = {
userId: string;
userName: string;
password: string;
name: string;
uri: string;
};

View File

@ -0,0 +1,4 @@
export type CredentialRetrievePayload = {
userId: string;
uri: string;
};

View File

@ -0,0 +1,8 @@
export type CredentialUpdatePayload = {
userId: string;
userName: string;
password: string;
name: string;
uri: string;
credentialId: string;
};

View File

@ -0,0 +1,3 @@
export type PasswordGeneratePayload = {
userId: string;
};

View File

@ -0,0 +1,7 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { MessageCommon } from "./messageCommon";
export type EncryptedMessageResponse = MessageCommon & {
encryptedPayload: EncString;
};

View File

@ -0,0 +1,6 @@
export type AccountStatusResponse = {
id: string;
email: string;
status: "locked" | "unlocked";
active: boolean;
};

View File

@ -0,0 +1,3 @@
export type CannotDecryptErrorResponse = {
error: "cannot-decrypt";
};

View File

@ -0,0 +1,7 @@
export type CipherResponse = {
userId: string;
credentialId: string;
userName: string;
password: string;
name: string;
};

View File

@ -0,0 +1,16 @@
import { AccountStatusResponse } from "./accountStatusResponse";
import { CannotDecryptErrorResponse } from "./cannotDecryptErrorResponse";
import { CipherResponse } from "./cipherResponse";
import { FailureStatusResponse } from "./failureStatusResponse";
import { GenerateResponse } from "./generateResponse";
import { SuccessStatusResponse } from "./successStatusResponse";
import { UserStatusErrorResponse } from "./userStatusErrorResponse";
export type EncyptedMessageResponse =
| AccountStatusResponse[]
| CannotDecryptErrorResponse
| CipherResponse[]
| FailureStatusResponse
| GenerateResponse
| SuccessStatusResponse
| UserStatusErrorResponse;

View File

@ -0,0 +1,3 @@
export type FailureStatusResponse = {
status: "failure";
};

View File

@ -0,0 +1,3 @@
export type GenerateResponse = {
password: string;
};

View File

@ -0,0 +1,3 @@
export type SuccessStatusResponse = {
status: "success";
};

View File

@ -0,0 +1,3 @@
export type UserStatusErrorResponse = {
error: "locked" | "not-active-user";
};

View File

@ -0,0 +1,25 @@
export * from "./encryptedMessagePayloads/credentialCreatePayload";
export * from "./encryptedMessagePayloads/credentialRetrievePayload";
export * from "./encryptedMessagePayloads/credentialUpdatePayload";
export * from "./encryptedMessagePayloads/passwordGeneratePayload";
export * from "./encryptedMessageResponses/accountStatusResponse";
export * from "./encryptedMessageResponses/cannotDecryptErrorResponse";
export * from "./encryptedMessageResponses/cipherResponse";
export * from "./encryptedMessageResponses/encryptedMessageResponse";
export * from "./encryptedMessageResponses/failureStatusResponse";
export * from "./encryptedMessageResponses/generateResponse";
export * from "./encryptedMessageResponses/successStatusResponse";
export * from "./encryptedMessageResponses/userStatusErrorResponse";
export * from "./decryptedCommandData";
export * from "./encryptedCommand";
export * from "./encryptedMessage";
export * from "./encryptedMessageResponse";
export * from "./legacyMessage";
export * from "./legacyMessageWrapper";
export * from "./message";
export * from "./messageCommon";
export * from "./unencryptedCommand";
export * from "./unencryptedMessage";
export * from "./unencryptedMessageResponse";

View File

@ -0,0 +1,8 @@
export type LegacyMessage = {
command: string;
userId?: string;
timestamp?: number;
publicKey?: string;
};

View File

@ -0,0 +1,8 @@
import { EncString } from "@bitwarden/common/models/domain/encString";
import { LegacyMessage } from "./legacyMessage";
export type LegacyMessageWrapper = {
message: LegacyMessage | EncString;
appId: string;
};

View File

@ -0,0 +1,4 @@
import { EncryptedMessage } from "./encryptedMessage";
import { UnencryptedMessage } from "./unencryptedMessage";
export type Message = UnencryptedMessage | EncryptedMessage;

View File

@ -0,0 +1,4 @@
export interface MessageCommon {
version: number;
messageId: string;
}

View File

@ -0,0 +1 @@
export type UnencryptedCommand = "bw-handshake";

View File

@ -0,0 +1,10 @@
import { MessageCommon } from "./messageCommon";
import { UnencryptedCommand } from "./unencryptedCommand";
export type UnencryptedMessage = MessageCommon & {
command: UnencryptedCommand;
payload: {
publicKey: string;
applicationName: string;
};
};

View File

@ -0,0 +1,16 @@
import { MessageCommon } from "./messageCommon";
export type UnencryptedMessageResponse = MessageCommon &
(
| {
payload: {
status: "success";
sharedKey: string;
};
}
| {
payload: {
error: "canceled" | "locked" | "cannot-decrypt" | "version-discrepancy";
};
}
);

View File

@ -0,0 +1,228 @@
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { PolicyType } from "@bitwarden/common/enums/policyType";
import { CipherView } from "@bitwarden/common/models/view/cipherView";
import { LoginUriView } from "@bitwarden/common/models/view/loginUriView";
import { LoginView } from "@bitwarden/common/models/view/loginView";
import { DecryptedCommandData } from "src/models/nativeMessaging/decryptedCommandData";
import { CredentialCreatePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload";
import { CredentialRetrievePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialRetrievePayload";
import { CredentialUpdatePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload";
import { PasswordGeneratePayload } from "src/models/nativeMessaging/encryptedMessagePayloads/passwordGeneratePayload";
import { AccountStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/accountStatusResponse";
import { CipherResponse } from "src/models/nativeMessaging/encryptedMessageResponses/cipherResponse";
import { EncyptedMessageResponse } from "src/models/nativeMessaging/encryptedMessageResponses/encryptedMessageResponse";
import { FailureStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/failureStatusResponse";
import { GenerateResponse } from "src/models/nativeMessaging/encryptedMessageResponses/generateResponse";
import { SuccessStatusResponse } from "src/models/nativeMessaging/encryptedMessageResponses/successStatusResponse";
import { UserStatusErrorResponse } from "src/models/nativeMessaging/encryptedMessageResponses/userStatusErrorResponse";
import { StateService } from "./state.service";
export class EncryptedMessageHandlerService {
constructor(
private stateService: StateService,
private authService: AuthService,
private cipherService: CipherService,
private policyService: PolicyService,
private messagingService: MessagingService,
private passwordGenerationService: PasswordGenerationService
) {}
async responseDataForCommand(
commandData: DecryptedCommandData
): Promise<EncyptedMessageResponse> {
const { command, payload } = commandData;
switch (command) {
case "bw-status": {
return await this.statusCommandHandler();
}
case "bw-credential-retrieval": {
return await this.credentialretreivalCommandHandler(payload as CredentialRetrievePayload);
}
case "bw-credential-create": {
return await this.credentialCreateCommandHandler(payload as CredentialCreatePayload);
}
case "bw-credential-update": {
return await this.credentialUpdateCommandHandler(payload as CredentialUpdatePayload);
}
case "bw-generate-password": {
return await this.generateCommandHandler(payload as PasswordGeneratePayload);
}
default:
return {
error: "cannot-decrypt",
};
}
}
private async checkUserStatus(userId: string): Promise<string> {
const activeUserId = await this.stateService.getUserId();
if (userId !== activeUserId) {
return "not-active-user";
}
const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) {
return "locked";
}
return "valid";
}
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
const accounts = this.stateService.accounts.getValue();
const activeUserId = await this.stateService.getUserId();
if (!accounts || !Object.keys(accounts)) {
return [];
}
return Promise.all(
Object.keys(accounts).map(async (userId) => {
const authStatus = await this.authService.getAuthStatus(userId);
const email = await this.stateService.getEmail({ userId });
return {
id: userId,
email,
status: authStatus === AuthenticationStatus.Unlocked ? "unlocked" : "locked",
active: userId === activeUserId,
};
})
);
}
private async credentialretreivalCommandHandler(
payload: CredentialRetrievePayload
): Promise<CipherResponse[] | UserStatusErrorResponse> {
if (payload.uri == null) {
return [];
}
const ciphersResponse: CipherResponse[] = [];
const activeUserId = await this.stateService.getUserId();
const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) {
return { error: "locked" };
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(payload.uri);
ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
ciphers.forEach((c) => {
ciphersResponse.push({
userId: activeUserId,
credentialId: c.id,
userName: c.login.username,
password: c.login.password,
name: c.name,
} as CipherResponse);
});
return ciphersResponse;
}
private async credentialCreateCommandHandler(
payload: CredentialCreatePayload
): Promise<SuccessStatusResponse | FailureStatusResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const credentialCreatePayload = payload as CredentialCreatePayload;
if (
credentialCreatePayload.name == null ||
(await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership))
) {
return { status: "failure" };
}
const cipherView = new CipherView();
cipherView.type = CipherType.Login;
cipherView.name = payload.name;
cipherView.login = new LoginView();
cipherView.login.password = credentialCreatePayload.password;
cipherView.login.username = credentialCreatePayload.userName;
cipherView.login.uris = [new LoginUriView()];
cipherView.login.uris[0].uri = credentialCreatePayload.uri;
try {
const encrypted = await this.cipherService.encrypt(cipherView);
await this.cipherService.saveWithServer(encrypted);
// Notify other clients of new login
await this.messagingService.send("addedCipher");
// Refresh Desktop ciphers list
await this.messagingService.send("refreshCiphers");
return { status: "success" };
} catch (error) {
return { status: "failure" };
}
}
private async credentialUpdateCommandHandler(
payload: CredentialUpdatePayload
): Promise<SuccessStatusResponse | FailureStatusResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const credentialUpdatePayload = payload as CredentialUpdatePayload;
if (credentialUpdatePayload.name == null) {
return { status: "failure" };
}
try {
const cipher = await this.cipherService.get(credentialUpdatePayload.credentialId);
if (cipher === null) {
return { status: "failure" };
}
const cipherView = await cipher.decrypt();
cipherView.name = credentialUpdatePayload.name;
cipherView.login.password = credentialUpdatePayload.password;
cipherView.login.username = credentialUpdatePayload.userName;
cipherView.login.uris[0].uri = credentialUpdatePayload.uri;
const encrypted = await this.cipherService.encrypt(cipherView);
await this.cipherService.saveWithServer(encrypted);
// Notify other clients of update
await this.messagingService.send("editedCipher");
// Refresh Desktop ciphers list
await this.messagingService.send("refreshCiphers");
return { status: "success" };
} catch (error) {
return { status: "failure" };
}
}
private async generateCommandHandler(
payload: PasswordGeneratePayload
): Promise<GenerateResponse | UserStatusErrorResponse> {
const userStatus = await this.checkUserStatus(payload.userId);
if (userStatus !== "valid") {
return { error: userStatus } as UserStatusErrorResponse;
}
const options = (await this.passwordGenerationService.getOptions())[0];
const generatedValue = await this.passwordGenerationService.generatePassword(options);
await this.passwordGenerationService.addHistory(generatedValue);
return { password: generatedValue };
}
}

View File

@ -0,0 +1,221 @@
import { Injectable } from "@angular/core";
import { ipcRenderer } from "electron";
import Swal from "sweetalert2";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { StateService } from "@bitwarden/common/services/state.service";
import { DecryptedCommandData } from "src/models/nativeMessaging/decryptedCommandData";
import { EncryptedMessage } from "src/models/nativeMessaging/encryptedMessage";
import { EncryptedMessageResponse } from "src/models/nativeMessaging/encryptedMessageResponse";
import { Message } from "src/models/nativeMessaging/message";
import { UnencryptedMessage } from "src/models/nativeMessaging/unencryptedMessage";
import { UnencryptedMessageResponse } from "src/models/nativeMessaging/unencryptedMessageResponse";
import { EncryptedMessageHandlerService } from "./encryptedMessageHandlerService";
const EncryptionAlgorithm = "sha1";
@Injectable()
export class NativeMessageHandlerService {
private ddgSharedSecret: SymmetricCryptoKey;
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private cryptoFunctionService: CryptoFunctionService,
private messagingService: MessagingService,
private i18nService: I18nService,
private encryptedMessageHandlerService: EncryptedMessageHandlerService
) {}
async handleMessage(message: Message) {
const decryptedCommand = message as UnencryptedMessage;
if (message.version != NativeMessagingVersion.Latest) {
this.sendResponse({
messageId: message.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "version-discrepancy",
},
});
} else {
if (decryptedCommand.command === "bw-handshake") {
await this.handleDecryptedMessage(decryptedCommand);
} else {
await this.handleEncryptedMessage(message as EncryptedMessage);
}
}
}
private async handleDecryptedMessage(message: UnencryptedMessage) {
const { messageId, payload } = message;
const { publicKey, applicationName } = payload;
if (!publicKey) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
try {
const remotePublicKey = Utils.fromB64ToArray(publicKey).buffer;
const ddgEnabled = await this.stateService.getEnableDuckDuckGoBrowserIntegration();
if (!ddgEnabled) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "canceled",
},
});
return;
}
// Ask for confirmation from user
this.messagingService.send("setFocus");
const submitted = await Swal.fire({
heightAuto: false,
titleText: this.i18nService.t("verifyNativeMessagingConnectionTitle", applicationName),
html: `${this.i18nService.t("verifyNativeMessagingConnectionDesc")}<br>${this.i18nService.t(
"verifyNativeMessagingConnectionWarning"
)}`,
showCancelButton: true,
cancelButtonText: this.i18nService.t("no"),
showConfirmButton: true,
confirmButtonText: this.i18nService.t("yes"),
allowOutsideClick: false,
focusCancel: true,
});
if (submitted.value !== true) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "canceled",
},
});
return;
}
const secret = await this.cryptoFunctionService.randomBytes(64);
this.ddgSharedSecret = new SymmetricCryptoKey(secret);
const sharedKeyB64 = new SymmetricCryptoKey(secret).toJSON().keyB64;
await this.stateService.setDuckDuckGoSharedKey(sharedKeyB64);
const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt(
secret,
remotePublicKey,
EncryptionAlgorithm
);
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
status: "success",
sharedKey: Utils.fromBufferToB64(encryptedSecret),
},
});
} catch (error) {
this.sendResponse({
messageId: messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
}
}
private async handleEncryptedMessage(message: EncryptedMessage) {
message.encryptedCommand = EncString.fromJSON(message.encryptedCommand.toString());
const decryptedCommandData = await this.decryptPayload(message);
const { command } = decryptedCommandData;
try {
const responseData = await this.encryptedMessageHandlerService.responseDataForCommand(
decryptedCommandData
);
await this.sendEncryptedResponse(message, { command, payload: responseData });
} catch (error) {
this.sendEncryptedResponse(message, { command, payload: {} });
}
}
private async encryptPayload(
payload: DecryptedCommandData,
key: SymmetricCryptoKey
): Promise<EncString> {
return await this.cryptoService.encrypt(JSON.stringify(payload), key);
}
private async decryptPayload(message: EncryptedMessage): Promise<DecryptedCommandData> {
if (!this.ddgSharedSecret) {
const storedKey = await this.stateService.getDuckDuckGoSharedKey();
if (storedKey == null) {
this.sendResponse({
messageId: message.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
this.ddgSharedSecret = SymmetricCryptoKey.fromJSON({ keyB64: storedKey });
}
return JSON.parse(
await this.cryptoService.decryptToUtf8(
message.encryptedCommand as EncString,
this.ddgSharedSecret
)
);
}
private async sendEncryptedResponse(
originalMessage: EncryptedMessage,
response: DecryptedCommandData
) {
if (!this.ddgSharedSecret) {
this.sendResponse({
messageId: originalMessage.messageId,
version: NativeMessagingVersion.Latest,
payload: {
error: "cannot-decrypt",
},
});
return;
}
const encryptedPayload = await this.encryptPayload(response, this.ddgSharedSecret);
this.sendResponse({
messageId: originalMessage.messageId,
version: NativeMessagingVersion.Latest,
encryptedPayload,
});
}
private sendResponse(response: EncryptedMessageResponse | UnencryptedMessageResponse) {
ipcRenderer.send("nativeMessagingReply", response);
}
}

View File

@ -14,23 +14,15 @@ import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
import { LegacyMessage } from "src/models/nativeMessaging/legacyMessage";
import { LegacyMessageWrapper } from "src/models/nativeMessaging/legacyMessageWrapper";
import { Message } from "src/models/nativeMessaging/message";
import { NativeMessageHandlerService } from "./nativeMessageHandler.service";
const MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = "sha1";
type Message = {
command: string;
userId?: string;
timestamp?: number;
publicKey?: string;
};
type OuterMessage = {
message: Message | EncString;
appId: string;
};
@Injectable()
export class NativeMessagingService {
private sharedSecrets = new Map<string, SymmetricCryptoKey>();
@ -42,7 +34,8 @@ export class NativeMessagingService {
private logService: LogService,
private i18nService: I18nService,
private messagingService: MessagingService,
private stateService: StateService
private stateService: StateService,
private nativeMessageHandler: NativeMessageHandlerService
) {}
init() {
@ -51,15 +44,20 @@ export class NativeMessagingService {
});
}
private async messageHandler(msg: OuterMessage) {
const appId = msg.appId;
const rawMessage = msg.message;
private async messageHandler(msg: LegacyMessageWrapper | Message) {
const outerMessage = msg as Message;
if (outerMessage.version) {
this.nativeMessageHandler.handleMessage(outerMessage);
return;
}
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
// Request to setup secure encryption
if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey).buffer;
// Valudate the UserId to ensure we are logged into the same account.
// Validate the UserId to ensure we are logged into the same account.
const userIds = Object.keys(this.stateService.accounts.getValue());
if (!userIds.includes(rawMessage.userId)) {
ipcRenderer.send("nativeMessagingReply", { command: "wrongUserId", appId: appId });
@ -103,7 +101,7 @@ export class NativeMessagingService {
return;
}
const message: Message = JSON.parse(
const message: LegacyMessage = JSON.parse(
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecrets.get(appId))
);

View File

@ -139,6 +139,8 @@ export abstract class StateService<T extends Account = Account> {
setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>;
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
@ -158,6 +160,11 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableDuckDuckGoBrowserIntegration: (
value: boolean,
options?: StorageOptions
) => Promise<void>;
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableGravitars: (options?: StorageOptions) => Promise<boolean>;

View File

@ -0,0 +1,4 @@
export enum NativeMessagingVersion {
One = 1, // Original implementation
Latest = One,
}

View File

@ -37,4 +37,5 @@ export class GlobalState {
alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
}

View File

@ -58,6 +58,8 @@ const partialKeys = {
masterKey: "_masterkey",
};
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
export class StateService<
TGlobalState extends GlobalState = GlobalState,
TAccount extends Account = Account
@ -1008,6 +1010,24 @@ export class StateService<
);
}
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return null;
}
return await this.secureStorageService.get<string>(DDG_SHARED_KEY, options);
}
async setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) {
return;
}
value == null
? await this.secureStorageService.remove(DDG_SHARED_KEY, options)
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
}
async getEmail(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
@ -1166,6 +1186,27 @@ export class StateService<
);
}
async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise<boolean> {
return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.enableDuckDuckGoBrowserIntegration ?? false
);
}
async setEnableDuckDuckGoBrowserIntegration(
value: boolean,
options?: StorageOptions
): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
globals.enableDuckDuckGoBrowserIntegration = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions())
);
}
async getEnableFullWidth(options?: StorageOptions): Promise<boolean> {
return (
(