mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-02 23:11:40 +01:00
[SG-520] Native messaging handler (#3566)
* [SG-523] Base test runner app for native messages (#3269) * Base test runner app for native messages * Remove default test script * Add case for canceled status * Modify to allow usage of libs crypto services and functions * Small adjustments * Handshake request (#3277) * Handshake request * Fix capitalization * Update info text * lock node-ipc to 9.2.1 * [SG-569] Native Messaging settings bug (#3285) * Fix bug where updating setting wasn't starting the native messaging listener * Update test runner error message * [SG-532] Implement Status command in Native Messaging Service (#3310) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Remove in progress file (merge error) * Move models to their own folder and add index.ts * Remove file that got un-deleted * Remove file that will be added in separate command * Fix imports that got borked * [SG-533] Implement bw-credential-retrieval (#3334) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Add error handling for passing a bad public key to handshake * [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Add bw-credential-create * Better response handling in test runner * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * bw-cipher-create move type into its own file * Use LogUtils for all logging * Implement bw-credential-update * Give naming conventions for types * Rename file correctly * Update handleEncyptedMessage with EncString changes * [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380) * Add MessagingService to send messages on login create and update * Add `not-active-user` error to create and update and other refactors * [SG-536] Implement bw-generate-password (#3370) * implement bw-generate-password * Fix merge conflict resolution errors * Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Logging improvements * Add NativeMessagingVersion enum * Add version check in NativeMessagingHandler Co-authored-by: Addison Beck <addisonbeck1@gmail.com> * Refactor account status checks and check for locked state in generate command (#3461) * Add feawture flag to show/hide ddg setting (#3506) * [SG-649] Add confirmation dialog and tweak shared key retrieval (#3451) * Add confirmation dialog when completing handshake * Copy updates for dialog * HandshakeResponse type fixes * Add longer timeout for handshake command * [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551) * NativeMessageHandlerService refactor and additional types * Return empty array if no uri to retrieve command * Move commands from test runner into a separate folder * Fix bug where confirmation dialog messes with styling * Enable DDG feature * Fix generated password not saving to history * Take credentialId as parameter to update * Add applicationName to handshake payload * Add warning text to confirmation modal Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
This commit is contained in:
parent
32eac70c82
commit
f4e61d1cec
@ -1,4 +1,6 @@
|
||||
{
|
||||
"devFlags": {},
|
||||
"flags": {}
|
||||
"flags": {
|
||||
"showDDGSetting": true
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
{
|
||||
"flags": {}
|
||||
"flags": {
|
||||
"showDDGSetting": true
|
||||
}
|
||||
}
|
||||
|
5
apps/desktop/native-messaging-test-runner/.eslintrc.json
Normal file
5
apps/desktop/native-messaging-test-runner/.eslintrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
681
apps/desktop/native-messaging-test-runner/package-lock.json
generated
Normal file
681
apps/desktop/native-messaging-test-runner/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
35
apps/desktop/native-messaging-test-runner/package.json
Normal file
35
apps/desktop/native-messaging-test-runner/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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();
|
||||
})();
|
@ -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();
|
||||
})();
|
@ -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();
|
||||
})();
|
@ -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();
|
||||
})();
|
@ -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();
|
||||
})();
|
@ -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();
|
||||
})();
|
26
apps/desktop/native-messaging-test-runner/src/deferred.ts
Normal file
26
apps/desktop/native-messaging-test-runner/src/deferred.ts
Normal 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;
|
||||
}
|
||||
}
|
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal file
166
apps/desktop/native-messaging-test-runner/src/ipcService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
29
apps/desktop/native-messaging-test-runner/src/logUtils.ts
Normal file
29
apps/desktop/native-messaging-test-runner/src/logUtils.ts
Normal 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`);
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
);
|
||||
}
|
||||
}
|
25
apps/desktop/native-messaging-test-runner/src/race.ts
Normal file
25
apps/desktop/native-messaging-test-runner/src/race.ts
Normal 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;
|
||||
}),
|
||||
]);
|
||||
};
|
27
apps/desktop/native-messaging-test-runner/src/variables.ts
Normal file
27
apps/desktop/native-messaging-test-runner/src/variables.ts
Normal 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=";
|
17
apps/desktop/native-messaging-test-runner/tsconfig.json
Normal file
17
apps/desktop/native-messaging-test-runner/tsconfig.json
Normal 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"]
|
||||
}
|
@ -309,6 +309,23 @@
|
||||
</div>
|
||||
<small class="help-block">{{ "enableBrowserIntegrationDesc" | i18n }}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
|
||||
<div class="checkbox">
|
||||
<label for="enableDuckDuckGoBrowserIntegration">
|
||||
<input
|
||||
id="enableDuckDuckGoBrowserIntegration"
|
||||
type="checkbox"
|
||||
name="enableDuckDuckGoBrowserIntegration"
|
||||
[(ngModel)]="enableDuckDuckGoBrowserIntegration"
|
||||
(change)="saveDdgBrowserIntegration()"
|
||||
/>
|
||||
{{ "enableDuckDuckGoBrowserIntegration" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block">{{
|
||||
"enableDuckDuckGoBrowserIntegrationDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label for="enableBrowserIntegrationFingerprint">
|
||||
|
@ -16,6 +16,7 @@ import { ThemeType } from "@bitwarden/common/enums/themeType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { isWindowsStore } from "@bitwarden/electron/utils";
|
||||
|
||||
import { flagEnabled } from "../../flags";
|
||||
import { SetPinComponent } from "../components/set-pin.component";
|
||||
|
||||
@Component({
|
||||
@ -28,6 +29,7 @@ export class SettingsComponent implements OnInit {
|
||||
pin: boolean = null;
|
||||
enableFavicons = false;
|
||||
enableBrowserIntegration = false;
|
||||
enableDuckDuckGoBrowserIntegration = false;
|
||||
enableBrowserIntegrationFingerprint = false;
|
||||
enableMinToTray = false;
|
||||
enableCloseToTray = false;
|
||||
@ -51,6 +53,7 @@ export class SettingsComponent implements OnInit {
|
||||
showAlwaysShowDock = false;
|
||||
openAtLogin: boolean;
|
||||
requireEnableTray = false;
|
||||
showDuckDuckGoIntegrationOption = false;
|
||||
|
||||
enableTrayText: string;
|
||||
enableTrayDescText: string;
|
||||
@ -102,6 +105,9 @@ export class SettingsComponent implements OnInit {
|
||||
this.startToTrayText = this.i18nService.t(startToTrayKey);
|
||||
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
|
||||
|
||||
// DuckDuckGo browser is only for macos initially
|
||||
this.showDuckDuckGoIntegrationOption = flagEnabled("showDDGSetting") && isMac;
|
||||
|
||||
this.vaultTimeouts = [
|
||||
// { name: i18nService.t('immediately'), value: 0 },
|
||||
{ name: i18nService.t("oneMinute"), value: 1 },
|
||||
@ -188,6 +194,8 @@ export class SettingsComponent implements OnInit {
|
||||
// Account preferences
|
||||
this.enableFavicons = !(await this.stateService.getDisableFavicon());
|
||||
this.enableBrowserIntegration = await this.stateService.getEnableBrowserIntegration();
|
||||
this.enableDuckDuckGoBrowserIntegration =
|
||||
await this.stateService.getEnableDuckDuckGoBrowserIntegration();
|
||||
this.enableBrowserIntegrationFingerprint =
|
||||
await this.stateService.getEnableBrowserIntegrationFingerprint();
|
||||
this.clearClipboard = await this.stateService.getClearClipboard();
|
||||
@ -432,6 +440,22 @@ export class SettingsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async saveDdgBrowserIntegration() {
|
||||
await this.stateService.setEnableDuckDuckGoBrowserIntegration(
|
||||
this.enableDuckDuckGoBrowserIntegration
|
||||
);
|
||||
|
||||
if (!this.enableBrowserIntegration) {
|
||||
await this.stateService.setDuckDuckGoSharedKey(null);
|
||||
}
|
||||
|
||||
this.messagingService.send(
|
||||
this.enableDuckDuckGoBrowserIntegration
|
||||
? "enableDuckDuckGoBrowserIntegration"
|
||||
: "disableDuckDuckGoBrowserIntegration"
|
||||
);
|
||||
}
|
||||
|
||||
async saveBrowserIntegrationFingerprint() {
|
||||
await this.stateService.setEnableBrowserIntegrationFingerprint(
|
||||
this.enableBrowserIntegrationFingerprint
|
||||
|
@ -12,7 +12,9 @@ import {
|
||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||
import { AbstractThemingService } from "@bitwarden/angular/services/theming/theming.service.abstraction";
|
||||
import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/abstractions/auth.service";
|
||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/abstractions/cipher.service";
|
||||
import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
|
||||
@ -22,8 +24,10 @@ import {
|
||||
LogService as LogServiceAbstraction,
|
||||
} from "@bitwarden/common/abstractions/log.service";
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
|
||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstractions/state.service";
|
||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
|
||||
@ -41,7 +45,9 @@ import { ElectronRendererSecureStorageService } from "@bitwarden/electron/servic
|
||||
import { ElectronRendererStorageService } from "@bitwarden/electron/services/electronRendererStorage.service";
|
||||
|
||||
import { Account } from "../../models/account";
|
||||
import { EncryptedMessageHandlerService } from "../../services/encryptedMessageHandlerService";
|
||||
import { I18nService } from "../../services/i18n.service";
|
||||
import { NativeMessageHandlerService } from "../../services/nativeMessageHandler.service";
|
||||
import { NativeMessagingService } from "../../services/nativeMessaging.service";
|
||||
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
|
||||
import { StateService } from "../../services/state.service";
|
||||
@ -147,6 +153,28 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
|
||||
provide: AbstractThemingService,
|
||||
useClass: DesktopThemingService,
|
||||
},
|
||||
{
|
||||
provide: EncryptedMessageHandlerService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
PasswordGenerationServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: NativeMessageHandlerService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
EncryptedMessageHandlerService,
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ServicesModule {}
|
||||
|
@ -8,7 +8,9 @@ import {
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-types */
|
||||
export type Flags = {} & SharedFlags;
|
||||
export type Flags = {
|
||||
showDDGSetting?: boolean;
|
||||
} & SharedFlags;
|
||||
|
||||
// required to avoid linting errors when there are no flags
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-types */
|
||||
|
@ -1570,6 +1570,12 @@
|
||||
"enableBrowserIntegrationDesc": {
|
||||
"message": "Used for biometrics in browser."
|
||||
},
|
||||
"enableDuckDuckGoBrowserIntegration": {
|
||||
"message": "Allow DuckDuckGo browser integration"
|
||||
},
|
||||
"enableDuckDuckGoBrowserIntegrationDesc": {
|
||||
"message": "Use your Bitwarden vault when browsing with DuckDuckGo."
|
||||
},
|
||||
"browserIntegrationUnsupportedTitle": {
|
||||
"message": "Browser integration not supported"
|
||||
},
|
||||
@ -1597,6 +1603,21 @@
|
||||
"verifyBrowserDesc": {
|
||||
"message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension."
|
||||
},
|
||||
"verifyNativeMessagingConnectionTitle": {
|
||||
"message": "$APPID$ wants to connect to Bitwarden",
|
||||
"placeholders": {
|
||||
"appid": {
|
||||
"content": "$1",
|
||||
"example": "My App"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verifyNativeMessagingConnectionDesc": {
|
||||
"message": "Would you like to approve this request?"
|
||||
},
|
||||
"verifyNativeMessagingConnectionWarning": {
|
||||
"message": "If you did not initiate this request, do not approve it."
|
||||
},
|
||||
"biometricsNotEnabledTitle": {
|
||||
"message": "Biometrics not enabled"
|
||||
},
|
||||
@ -1985,15 +2006,15 @@
|
||||
"organizationIsDisabled": {
|
||||
"message": "Organization is disabled."
|
||||
},
|
||||
"disabledOrganizationFilterError" : {
|
||||
"message" : "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
|
||||
"disabledOrganizationFilterError": {
|
||||
"message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance."
|
||||
},
|
||||
"neverLockWarning": {
|
||||
"message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected."
|
||||
},
|
||||
"cardBrandMir": {
|
||||
"message": "Mir"
|
||||
},
|
||||
},
|
||||
"vault": {
|
||||
"message": "Vault"
|
||||
}
|
||||
|
@ -171,7 +171,10 @@ export class Main {
|
||||
await this.biometricMain.init();
|
||||
}
|
||||
|
||||
if (await this.stateService.getEnableBrowserIntegration()) {
|
||||
if (
|
||||
(await this.stateService.getEnableBrowserIntegration()) ||
|
||||
(await this.stateService.getEnableDuckDuckGoBrowserIntegration())
|
||||
) {
|
||||
this.nativeMessagingMain.listen();
|
||||
}
|
||||
|
||||
|
@ -68,10 +68,18 @@ export class MessagingMain {
|
||||
this.main.nativeMessagingMain.generateManifests();
|
||||
this.main.nativeMessagingMain.listen();
|
||||
break;
|
||||
case "enableDuckDuckGoBrowserIntegration":
|
||||
this.main.nativeMessagingMain.generateDdgManifests();
|
||||
this.main.nativeMessagingMain.listen();
|
||||
break;
|
||||
case "disableBrowserIntegration":
|
||||
this.main.nativeMessagingMain.removeManifests();
|
||||
this.main.nativeMessagingMain.stop();
|
||||
break;
|
||||
case "disableDuckDuckGoBrowserIntegration":
|
||||
this.main.nativeMessagingMain.removeDdgManifests();
|
||||
this.main.nativeMessagingMain.stop();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
@ -163,6 +163,27 @@ export class NativeMessagingMain {
|
||||
}
|
||||
}
|
||||
|
||||
generateDdgManifests() {
|
||||
const manifest = {
|
||||
name: "com.8bit.bitwarden",
|
||||
description: "Bitwarden desktop <-> DuckDuckGo bridge",
|
||||
path: this.binaryPath(),
|
||||
type: "stdio",
|
||||
};
|
||||
switch (process.platform) {
|
||||
case "darwin": {
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
|
||||
this.writeManifest(path, manifest).catch((e) =>
|
||||
this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`)
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
removeManifests() {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
@ -217,6 +238,21 @@ export class NativeMessagingMain {
|
||||
}
|
||||
}
|
||||
|
||||
removeDdgManifests() {
|
||||
switch (process.platform) {
|
||||
case "darwin": {
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
|
||||
if (existsSync(path)) {
|
||||
fs.unlink(path);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private getDarwinNMHS() {
|
||||
/* eslint-disable no-useless-escape */
|
||||
return {
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { EncryptedCommand } from "./encryptedCommand";
|
||||
|
||||
export type DecryptedCommandData = {
|
||||
command: EncryptedCommand;
|
||||
payload?: any;
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
export type EncryptedCommand =
|
||||
| "bw-status"
|
||||
| "bw-credential-retrieval"
|
||||
| "bw-credential-create"
|
||||
| "bw-credential-update"
|
||||
| "bw-generate-password";
|
@ -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;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export type CredentialCreatePayload = {
|
||||
userId: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
name: string;
|
||||
uri: string;
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export type CredentialRetrievePayload = {
|
||||
userId: string;
|
||||
uri: string;
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
export type CredentialUpdatePayload = {
|
||||
userId: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
name: string;
|
||||
uri: string;
|
||||
credentialId: string;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type PasswordGeneratePayload = {
|
||||
userId: string;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
|
||||
import { MessageCommon } from "./messageCommon";
|
||||
|
||||
export type EncryptedMessageResponse = MessageCommon & {
|
||||
encryptedPayload: EncString;
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
export type AccountStatusResponse = {
|
||||
id: string;
|
||||
email: string;
|
||||
status: "locked" | "unlocked";
|
||||
active: boolean;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type CannotDecryptErrorResponse = {
|
||||
error: "cannot-decrypt";
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
export type CipherResponse = {
|
||||
userId: string;
|
||||
credentialId: string;
|
||||
userName: string;
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
export type FailureStatusResponse = {
|
||||
status: "failure";
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type GenerateResponse = {
|
||||
password: string;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type SuccessStatusResponse = {
|
||||
status: "success";
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export type UserStatusErrorResponse = {
|
||||
error: "locked" | "not-active-user";
|
||||
};
|
25
apps/desktop/src/models/nativeMessaging/index.ts
Normal file
25
apps/desktop/src/models/nativeMessaging/index.ts
Normal 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";
|
8
apps/desktop/src/models/nativeMessaging/legacyMessage.ts
Normal file
8
apps/desktop/src/models/nativeMessaging/legacyMessage.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export type LegacyMessage = {
|
||||
command: string;
|
||||
|
||||
userId?: string;
|
||||
timestamp?: number;
|
||||
|
||||
publicKey?: string;
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
|
||||
import { LegacyMessage } from "./legacyMessage";
|
||||
|
||||
export type LegacyMessageWrapper = {
|
||||
message: LegacyMessage | EncString;
|
||||
appId: string;
|
||||
};
|
4
apps/desktop/src/models/nativeMessaging/message.ts
Normal file
4
apps/desktop/src/models/nativeMessaging/message.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { EncryptedMessage } from "./encryptedMessage";
|
||||
import { UnencryptedMessage } from "./unencryptedMessage";
|
||||
|
||||
export type Message = UnencryptedMessage | EncryptedMessage;
|
4
apps/desktop/src/models/nativeMessaging/messageCommon.ts
Normal file
4
apps/desktop/src/models/nativeMessaging/messageCommon.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface MessageCommon {
|
||||
version: number;
|
||||
messageId: string;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export type UnencryptedCommand = "bw-handshake";
|
@ -0,0 +1,10 @@
|
||||
import { MessageCommon } from "./messageCommon";
|
||||
import { UnencryptedCommand } from "./unencryptedCommand";
|
||||
|
||||
export type UnencryptedMessage = MessageCommon & {
|
||||
command: UnencryptedCommand;
|
||||
payload: {
|
||||
publicKey: string;
|
||||
applicationName: string;
|
||||
};
|
||||
};
|
@ -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";
|
||||
};
|
||||
}
|
||||
);
|
228
apps/desktop/src/services/encryptedMessageHandlerService.ts
Normal file
228
apps/desktop/src/services/encryptedMessageHandlerService.ts
Normal 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 };
|
||||
}
|
||||
}
|
221
apps/desktop/src/services/nativeMessageHandler.service.ts
Normal file
221
apps/desktop/src/services/nativeMessageHandler.service.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -14,23 +14,15 @@ import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/models/domain/encString";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { LegacyMessage } from "src/models/nativeMessaging/legacyMessage";
|
||||
import { LegacyMessageWrapper } from "src/models/nativeMessaging/legacyMessageWrapper";
|
||||
import { Message } from "src/models/nativeMessaging/message";
|
||||
|
||||
import { NativeMessageHandlerService } from "./nativeMessageHandler.service";
|
||||
|
||||
const MessageValidTimeout = 10 * 1000;
|
||||
const EncryptionAlgorithm = "sha1";
|
||||
|
||||
type Message = {
|
||||
command: string;
|
||||
|
||||
userId?: string;
|
||||
timestamp?: number;
|
||||
|
||||
publicKey?: string;
|
||||
};
|
||||
|
||||
type OuterMessage = {
|
||||
message: Message | EncString;
|
||||
appId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class NativeMessagingService {
|
||||
private sharedSecrets = new Map<string, SymmetricCryptoKey>();
|
||||
@ -42,7 +34,8 @@ export class NativeMessagingService {
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private stateService: StateService
|
||||
private stateService: StateService,
|
||||
private nativeMessageHandler: NativeMessageHandlerService
|
||||
) {}
|
||||
|
||||
init() {
|
||||
@ -51,15 +44,20 @@ export class NativeMessagingService {
|
||||
});
|
||||
}
|
||||
|
||||
private async messageHandler(msg: OuterMessage) {
|
||||
const appId = msg.appId;
|
||||
const rawMessage = msg.message;
|
||||
private async messageHandler(msg: LegacyMessageWrapper | Message) {
|
||||
const outerMessage = msg as Message;
|
||||
if (outerMessage.version) {
|
||||
this.nativeMessageHandler.handleMessage(outerMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const { appId, message: rawMessage } = msg as LegacyMessageWrapper;
|
||||
|
||||
// Request to setup secure encryption
|
||||
if ("command" in rawMessage && rawMessage.command === "setupEncryption") {
|
||||
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey).buffer;
|
||||
|
||||
// Valudate the UserId to ensure we are logged into the same account.
|
||||
// Validate the UserId to ensure we are logged into the same account.
|
||||
const userIds = Object.keys(this.stateService.accounts.getValue());
|
||||
if (!userIds.includes(rawMessage.userId)) {
|
||||
ipcRenderer.send("nativeMessagingReply", { command: "wrongUserId", appId: appId });
|
||||
@ -103,7 +101,7 @@ export class NativeMessagingService {
|
||||
return;
|
||||
}
|
||||
|
||||
const message: Message = JSON.parse(
|
||||
const message: LegacyMessage = JSON.parse(
|
||||
await this.cryptoService.decryptToUtf8(rawMessage as EncString, this.sharedSecrets.get(appId))
|
||||
);
|
||||
|
||||
|
@ -139,6 +139,8 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDontShowCardsCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDontShowIdentitiesCurrentTab: (options?: StorageOptions) => Promise<boolean>;
|
||||
setDontShowIdentitiesCurrentTab: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
@ -158,6 +160,11 @@ export abstract class StateService<T extends Account = Account> {
|
||||
) => Promise<void>;
|
||||
getEnableCloseToTray: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableCloseToTray: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableDuckDuckGoBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableDuckDuckGoBrowserIntegration: (
|
||||
value: boolean,
|
||||
options?: StorageOptions
|
||||
) => Promise<void>;
|
||||
getEnableFullWidth: (options?: StorageOptions) => Promise<boolean>;
|
||||
setEnableFullWidth: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getEnableGravitars: (options?: StorageOptions) => Promise<boolean>;
|
||||
|
4
libs/common/src/enums/nativeMessagingVersion.ts
Normal file
4
libs/common/src/enums/nativeMessagingVersion.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum NativeMessagingVersion {
|
||||
One = 1, // Original implementation
|
||||
Latest = One,
|
||||
}
|
@ -37,4 +37,5 @@ export class GlobalState {
|
||||
alwaysShowDock?: boolean;
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
}
|
||||
|
@ -58,6 +58,8 @@ const partialKeys = {
|
||||
masterKey: "_masterkey",
|
||||
};
|
||||
|
||||
const DDG_SHARED_KEY = "DuckDuckGoSharedKey";
|
||||
|
||||
export class StateService<
|
||||
TGlobalState extends GlobalState = GlobalState,
|
||||
TAccount extends Account = Account
|
||||
@ -1008,6 +1010,24 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
return await this.secureStorageService.get<string>(DDG_SHARED_KEY, options);
|
||||
}
|
||||
|
||||
async setDuckDuckGoSharedKey(value: string, options?: StorageOptions): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
value == null
|
||||
? await this.secureStorageService.remove(DDG_SHARED_KEY, options)
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
@ -1166,6 +1186,27 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getEnableDuckDuckGoBrowserIntegration(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
?.enableDuckDuckGoBrowserIntegration ?? false
|
||||
);
|
||||
}
|
||||
|
||||
async setEnableDuckDuckGoBrowserIntegration(
|
||||
value: boolean,
|
||||
options?: StorageOptions
|
||||
): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
globals.enableDuckDuckGoBrowserIntegration = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions())
|
||||
);
|
||||
}
|
||||
|
||||
async getEnableFullWidth(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(
|
||||
|
Loading…
Reference in New Issue
Block a user