1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-13 14:53:33 +00: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
57 changed files with 2386 additions and 27 deletions

View File

@@ -1,4 +1,6 @@
{ {
"devFlags": {}, "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> </div>
<small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small> <small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small>
</div> </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="form-group">
<div class="checkbox"> <div class="checkbox">
<label for="enableBrowserIntegrationFingerprint"> <label for="enableBrowserIntegrationFingerprint">

View File

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

View File

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

View File

@@ -8,7 +8,9 @@ import {
// required to avoid linting errors when there are no flags // required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */ /* 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 // required to avoid linting errors when there are no flags
/* eslint-disable-next-line @typescript-eslint/ban-types */ /* eslint-disable-next-line @typescript-eslint/ban-types */

View File

@@ -1570,6 +1570,12 @@
"enableBrowserIntegrationDesc": { "enableBrowserIntegrationDesc": {
"message": "Used for biometrics in browser." "message": "Used for biometrics in browser."
}, },
"enableDuckDuckGoBrowserIntegration": {
"message": "Allow DuckDuckGo browser integration"
},
"enableDuckDuckGoBrowserIntegrationDesc": {
"message": "Use your Bitwarden vault when browsing with DuckDuckGo."
},
"browserIntegrationUnsupportedTitle": { "browserIntegrationUnsupportedTitle": {
"message": "Browser integration not supported" "message": "Browser integration not supported"
}, },
@@ -1597,6 +1603,21 @@
"verifyBrowserDesc": { "verifyBrowserDesc": {
"message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension." "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": { "biometricsNotEnabledTitle": {
"message": "Biometrics not enabled" "message": "Biometrics not enabled"
}, },

View File

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

View File

@@ -68,10 +68,18 @@ export class MessagingMain {
this.main.nativeMessagingMain.generateManifests(); this.main.nativeMessagingMain.generateManifests();
this.main.nativeMessagingMain.listen(); this.main.nativeMessagingMain.listen();
break; break;
case "enableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.generateDdgManifests();
this.main.nativeMessagingMain.listen();
break;
case "disableBrowserIntegration": case "disableBrowserIntegration":
this.main.nativeMessagingMain.removeManifests(); this.main.nativeMessagingMain.removeManifests();
this.main.nativeMessagingMain.stop(); this.main.nativeMessagingMain.stop();
break; break;
case "disableDuckDuckGoBrowserIntegration":
this.main.nativeMessagingMain.removeDdgManifests();
this.main.nativeMessagingMain.stop();
break;
default: default:
break; 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() { removeManifests() {
switch (process.platform) { switch (process.platform) {
case "win32": 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() { private getDarwinNMHS() {
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
return { 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 { EncString } from "@bitwarden/common/models/domain/encString";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; 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 MessageValidTimeout = 10 * 1000;
const EncryptionAlgorithm = "sha1"; const EncryptionAlgorithm = "sha1";
type Message = {
command: string;
userId?: string;
timestamp?: number;
publicKey?: string;
};
type OuterMessage = {
message: Message | EncString;
appId: string;
};
@Injectable() @Injectable()
export class NativeMessagingService { export class NativeMessagingService {
private sharedSecrets = new Map<string, SymmetricCryptoKey>(); private sharedSecrets = new Map<string, SymmetricCryptoKey>();
@@ -42,7 +34,8 @@ export class NativeMessagingService {
private logService: LogService, private logService: LogService,
private i18nService: I18nService, private i18nService: I18nService,
private messagingService: MessagingService, private messagingService: MessagingService,
private stateService: StateService private stateService: StateService,
private nativeMessageHandler: NativeMessageHandlerService
) {} ) {}
init() { init() {
@@ -51,15 +44,20 @@ export class NativeMessagingService {
}); });
} }
private async messageHandler(msg: OuterMessage) { private async messageHandler(msg: LegacyMessageWrapper | Message) {
const appId = msg.appId; const outerMessage = msg as Message;
const rawMessage = msg.message; if (outerMessage.version) {
this.nativeMessageHandler.handleMessage(outerMessage);
return;
}
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
// Request to setup secure encryption // Request to setup secure encryption
if ("command" in rawMessage && rawMessage.command === "setupEncryption") { if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey).buffer; 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()); const userIds = Object.keys(this.stateService.accounts.getValue());
if (!userIds.includes(rawMessage.userId)) { if (!userIds.includes(rawMessage.userId)) {
ipcRenderer.send("nativeMessagingReply", { command: "wrongUserId", appId: appId }); ipcRenderer.send("nativeMessagingReply", { command: "wrongUserId", appId: appId });
@@ -103,7 +101,7 @@ export class NativeMessagingService {
return; return;
} }
const message: Message = JSON.parse( const message: LegacyMessage = JSON.parse(
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecrets.get(appId)) 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>; setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>; getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>;
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>; setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>; getEmail: (options?: StorageOptions) => Promise<string>;
setEmail: (value: string, options?: StorageOptions) => Promise<void>; setEmail: (value: string, options?: StorageOptions) => Promise<void>;
getEmailVerified: (options?: StorageOptions) => Promise<boolean>; getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
@@ -158,6 +160,11 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>; ) => Promise<void>;
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>; getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>; setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableDuckDuckGoBrowserIntegration: (
value: boolean,
options?: StorageOptions
) => Promise<void>;
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>; getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>; setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableGravitars: (options?: StorageOptions) => Promise<boolean>; 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; alwaysShowDock?: boolean;
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean;
} }

View File

@@ -58,6 +58,8 @@ const partialKeys = {
masterKey: "_masterkey", masterKey: "_masterkey",
}; };
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
export class StateService< export class StateService<
TGlobalState extends GlobalState = GlobalState, TGlobalState extends GlobalState = GlobalState,
TAccount extends Account = Account 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> { async getEmail(options?: StorageOptions): Promise<string> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) 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> { async getEnableFullWidth(options?: StorageOptions): Promise<boolean> {
return ( return (
( (