From 78986023e779540a9c4ffe7952592c769a6560bd Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Tue, 5 Apr 2022 16:54:44 +0200 Subject: [PATCH] [BEEEP] Add native rust module (#1379) --- .prettierignore | 1 + README.md | 6 +- desktop_native/.gitignore | 5 + desktop_native/Cargo.lock | 946 +++++++++++++++++++ desktop_native/Cargo.toml | 43 + desktop_native/build.rs | 5 + desktop_native/index.d.ts | 15 + desktop_native/package-lock.json | 57 ++ desktop_native/package.json | 19 + desktop_native/src/lib.rs | 39 + desktop_native/src/password/macos.rs | 59 ++ desktop_native/src/password/mod.rs | 5 + desktop_native/src/password/unix.rs | 91 ++ desktop_native/src/password/windows.rs | 180 ++++ jslib | 2 +- package-lock.json | 382 ++------ package.json | 7 + src/app/app.component.ts | 2 +- src/main.ts | 19 +- src/main/biometric/biometric.darwin.main.ts | 36 + src/main/biometric/biometric.main.ts | 6 + src/main/biometric/biometric.windows.main.ts | 146 +++ src/main/desktopCredentialStorageListener.ts | 63 ++ src/main/{ => menu}/menu.about.ts | 0 src/main/{ => menu}/menu.account.ts | 0 src/main/{ => menu}/menu.bitwarden.ts | 0 src/main/{ => menu}/menu.edit.ts | 0 src/main/{ => menu}/menu.file.ts | 0 src/main/{ => menu}/menu.first.ts | 0 src/main/{ => menu}/menu.help.ts | 0 src/main/{ => menu}/menu.main.ts | 2 +- src/main/{ => menu}/menu.updater.ts | 0 src/main/{ => menu}/menu.view.ts | 0 src/main/{ => menu}/menu.window.ts | 0 src/main/{ => menu}/menubar.ts | 0 src/main/messaging.main.ts | 2 +- src/package.json | 4 +- 37 files changed, 1806 insertions(+), 336 deletions(-) create mode 100644 desktop_native/.gitignore create mode 100644 desktop_native/Cargo.lock create mode 100644 desktop_native/Cargo.toml create mode 100644 desktop_native/build.rs create mode 100644 desktop_native/index.d.ts create mode 100644 desktop_native/package-lock.json create mode 100644 desktop_native/package.json create mode 100644 desktop_native/src/lib.rs create mode 100644 desktop_native/src/password/macos.rs create mode 100644 desktop_native/src/password/mod.rs create mode 100644 desktop_native/src/password/unix.rs create mode 100644 desktop_native/src/password/windows.rs create mode 100644 src/main/biometric/biometric.darwin.main.ts create mode 100644 src/main/biometric/biometric.main.ts create mode 100644 src/main/biometric/biometric.windows.main.ts create mode 100644 src/main/desktopCredentialStorageListener.ts rename src/main/{ => menu}/menu.about.ts (100%) rename src/main/{ => menu}/menu.account.ts (100%) rename src/main/{ => menu}/menu.bitwarden.ts (100%) rename src/main/{ => menu}/menu.edit.ts (100%) rename src/main/{ => menu}/menu.file.ts (100%) rename src/main/{ => menu}/menu.first.ts (100%) rename src/main/{ => menu}/menu.help.ts (100%) rename src/main/{ => menu}/menu.main.ts (97%) rename src/main/{ => menu}/menu.updater.ts (100%) rename src/main/{ => menu}/menu.view.ts (100%) rename src/main/{ => menu}/menu.window.ts (100%) rename src/main/{ => menu}/menubar.ts (100%) diff --git a/.prettierignore b/.prettierignore index 36836b30..5910357d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ build dist dist-safari +desktop_native jslib diff --git a/README.md b/README.md index 07140eef..9020d9e4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,11 @@ The Bitwarden desktop app is written using Electron and Angular. The application - [Node.js](https://nodejs.org) v16.13.1 (LTS) or greater - NPM v8 -- Windows users: To compile the native node modules used in the app you will need the _Visual C++ toolset_, available through the standard Visual Studio installer. You will also need to install the _Microsoft Build Tools 2015_ and _Windows 10 SDK 17134_ as additional dependencies in the Visual Studio installer. +- Rust (https://www.rust-lang.org/tools/install) +- Windows: + - To compile the native node modules used in the app you will need the _Visual C++ toolset_, available through the standard Visual Studio installer. You will also need to install the _Microsoft Build Tools 2015_ and _Windows 10 SDK 17134_ as additional dependencies in the Visual Studio installer. +- Linux: + - The following packages `build-essential libsecret-1-dev libglib2.0-dev` **Run the app** diff --git a/desktop_native/.gitignore b/desktop_native/.gitignore new file mode 100644 index 00000000..6ca71fb5 --- /dev/null +++ b/desktop_native/.gitignore @@ -0,0 +1,5 @@ +target +index.node +**/node_modules +**/.DS_Store +npm-debug.log* diff --git a/desktop_native/Cargo.lock b/desktop_native/Cargo.lock new file mode 100644 index 00000000..0382ada8 --- /dev/null +++ b/desktop_native/Cargo.lock @@ -0,0 +1,946 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-expr" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e068cb2806bbc15b439846dc16c5f89f8599f2c3e4d73d4449d38f9b2f0b6c5" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "convert_case" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "cxx" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2295fe8865279f404147e9b2328e5af0ad11a2c016e58c13acfd48a07d8a55" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aaaa055d4908326f1b4524b23ae53758019b806c0c4f382ea240982e9766b26" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a670224c6686471df12560a0b97a08145082e70bd38e2b0b5383b79e46c3da7" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b700096ca0dece28d9535fdb17ab784a8ae155d7f29d39c273643e6292c9620" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "desktop_native" +version = "0.0.0" +dependencies = [ + "anyhow", + "core-foundation", + "gio", + "keytar", + "libsecret", + "napi", + "napi-build", + "napi-derive", + "scopeguard", + "security-framework", + "security-framework-sys", + "tokio", + "widestring", + "windows 0.32.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gio" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96efd8a1c00d890f6b45671916e165b5e43ccec61957d443aff6d7e44f62d348" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0fa5052773f5a56b8ae47dab09d040f5d9ce1311f4f99006e16e9a08269296" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa570813c504bdf7539a9400180c2dd4b789a819556fb86da7226d7d1b037b49" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41bfd8d227dead0829ac142454e97531b93f576d0805d779c42bfd799c65c572" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4366377bd56697de8aaee24e673c575d2694d72e7756324ded2b0428829a7b8" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "keytar" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d361c55fba09829ac620b040f5425bf239b1030c3d6820a84acac8da867dca4d" +dependencies = [ + "keytar-sys", +] + +[[package]] +name = "keytar-sys" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe908c6896705a1cb516cd6a5d956c63f08d95ace81b93253a98cd93e1e6a65a" +dependencies = [ + "cc", + "cxx", + "cxx-build", + "pkg-config", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + +[[package]] +name = "libsecret" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8d814edb0cb306ffded3f1bfe72d6689529f95f7313a718331802723a73e5a" +dependencies = [ + "bitflags", + "gio", + "glib", + "libc", + "libsecret-sys", + "once_cell", +] + +[[package]] +name = "libsecret-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287d2a0fcd95e4d7b0ac6fc9f802691a790d7e522138713b0cacebc4e63cab91" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cae2cd7ba2f3f63938b9c724475dfb7b9861b545a90324476324ed21dbc8c8" +dependencies = [ + "cc", +] + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mio" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "napi" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ec66e60f000c78dd7c6215b6fa260e0591e09805024332bc5b3f55acc12244" +dependencies = [ + "ctor", + "lazy_static", + "napi-sys", + "tokio", + "windows 0.30.0", +] + +[[package]] +name = "napi-build" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd4419172727423cf30351406c54f6cc1b354a2cfb4f1dba3e6cd07f6d5522b" + +[[package]] +name = "napi-derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74ac5287a5e94a8728fc82d16c5127acc5eb5b8ad6404ef5f82d6a4ce8d5bdd2" +dependencies = [ + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427f4f04525635cdf22005d1be62d6d671bcb5550d694a1efb480a315422b4af" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a385494dac3c52cbcacb393bb3b42669e7db8ab240c7ad5115f549eb061f2cc" + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96311ef4a16462c757bb6a39152c40f58f31cd2602a40fceb937e2bc34e6cbab" + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "system-deps" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version-compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" +dependencies = [ + "windows_aarch64_msvc 0.30.0", + "windows_i686_gnu 0.30.0", + "windows_i686_msvc 0.30.0", + "windows_x86_64_gnu 0.30.0", + "windows_x86_64_msvc 0.30.0", +] + +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" diff --git a/desktop_native/Cargo.toml b/desktop_native/Cargo.toml new file mode 100644 index 00000000..13f0bacb --- /dev/null +++ b/desktop_native/Cargo.toml @@ -0,0 +1,43 @@ +[package] +edition = "2021" +exclude = ["index.node"] +license = "GPL-3.0" +name = "desktop_native" +version = "0.0.0" + +[lib] +crate-type = ["cdylib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +napi = {version = "2", features = ["async"]} +napi-derive = "2" +scopeguard = "1.1.0" +tokio = {version = "1.17.0", features = ["full"]} + +[build-dependencies] +napi-build = "1" + +[target.'cfg(windows)'.dependencies] +widestring = "0.5.1" +windows = {version = "0.32.0", features = [ + "alloc", + "Foundation", + "Storage_Streams", + "Win32_Foundation", + "Win32_Security_Credentials", +]} + +[target.'cfg(windows)'.dev-dependencies] +keytar = "0.1.6" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.9.3" +security-framework = "2.6.1" +security-framework-sys = "2.6.1" + +[target.'cfg(target_os = "linux")'.dependencies] +gio = "0.15.6" +libsecret = "0.1.3" diff --git a/desktop_native/build.rs b/desktop_native/build.rs new file mode 100644 index 00000000..9fc23678 --- /dev/null +++ b/desktop_native/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/desktop_native/index.d.ts b/desktop_native/index.d.ts new file mode 100644 index 00000000..6ac55a15 --- /dev/null +++ b/desktop_native/index.d.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* auto-generated by NAPI-RS */ + +export namespace passwords { + /** Fetch the stored password from the keychain. */ + export function getPassword(service: string, account: string): Promise + /** Fetch the stored password from the keychain that was stored with Keytar. */ + export function getPasswordKeytar(service: string, account: string): Promise + /** Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. */ + export function setPassword(service: string, account: string, password: string): Promise + /** Delete the stored password from the keychain. */ + export function deletePassword(service: string, account: string): Promise +} diff --git a/desktop_native/package-lock.json b/desktop_native/package-lock.json new file mode 100644 index 00000000..b72bb80c --- /dev/null +++ b/desktop_native/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "desktop_native", + "version": "0.1.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "desktop_native", + "version": "0.1.0", + "hasInstallScript": true, + "license": "GPL-3.0", + "devDependencies": { + "@napi-rs/cli": "^2.4.4", + "cargo-cp-artifact": "^0.1" + } + }, + "node_modules/@napi-rs/cli": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.4.4.tgz", + "integrity": "sha512-f+tvwCv1ka24dBqI2DgBhR7Oxl3DKHOp4onxLXwyBFt6iCADnr3YZIr1/2Iq5r3uqxFgaf01bfPsRQZPkEp0kQ==", + "dev": true, + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.6.tgz", + "integrity": "sha512-CQw0doK/aaF7j041666XzuilHxqMxaKkn+I5vmBsd8SAwS0cO5CqVEVp0xJwOKstyqWZ6WK4Ww3O6p26x/Goyg==", + "dev": true, + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + } + }, + "dependencies": { + "@napi-rs/cli": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.4.4.tgz", + "integrity": "sha512-f+tvwCv1ka24dBqI2DgBhR7Oxl3DKHOp4onxLXwyBFt6iCADnr3YZIr1/2Iq5r3uqxFgaf01bfPsRQZPkEp0kQ==", + "dev": true + }, + "cargo-cp-artifact": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.6.tgz", + "integrity": "sha512-CQw0doK/aaF7j041666XzuilHxqMxaKkn+I5vmBsd8SAwS0cO5CqVEVp0xJwOKstyqWZ6WK4Ww3O6p26x/Goyg==", + "dev": true + } + } +} diff --git a/desktop_native/package.json b/desktop_native/package.json new file mode 100644 index 00000000..4ddb2758 --- /dev/null +++ b/desktop_native/package.json @@ -0,0 +1,19 @@ +{ + "name": "desktop_native", + "version": "0.1.0", + "description": "", + "main": "index.node", + "scripts": { + "build": "napi build --release --js true", + "build-debug": "npm run build --", + "build-release": "npm run build -- --release", + "install": "npm run build-release", + "test": "cargo test" + }, + "author": "", + "license": "GPL-3.0", + "devDependencies": { + "@napi-rs/cli": "^2.4.4", + "cargo-cp-artifact": "^0.1" + } +} diff --git a/desktop_native/src/lib.rs b/desktop_native/src/lib.rs new file mode 100644 index 00000000..96417a8b --- /dev/null +++ b/desktop_native/src/lib.rs @@ -0,0 +1,39 @@ +#[macro_use] +extern crate napi_derive; + +mod password; + +#[napi] +pub mod passwords { + /// Fetch the stored password from the keychain. + #[napi] + pub async fn get_password(service: String, account: String) -> napi::Result { + super::password::get_password(&service, &account) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Fetch the stored password from the keychain that was stored with Keytar. + #[napi] + pub async fn get_password_keytar(service: String, account: String) -> napi::Result { + super::password::get_password_keytar(&service, &account) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. + #[napi] + pub async fn set_password( + service: String, + account: String, + password: String, + ) -> napi::Result<()> { + super::password::set_password(&service, &account, &password) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + + /// Delete the stored password from the keychain. + #[napi] + pub async fn delete_password(service: String, account: String) -> napi::Result<()> { + super::password::delete_password(&service, &account) + .map_err(|e| napi::Error::from_reason(e.to_string())) + } +} diff --git a/desktop_native/src/password/macos.rs b/desktop_native/src/password/macos.rs new file mode 100644 index 00000000..7f0c3d9f --- /dev/null +++ b/desktop_native/src/password/macos.rs @@ -0,0 +1,59 @@ +use anyhow::Result; +use security_framework::passwords::{ + delete_generic_password, get_generic_password, set_generic_password, +}; + +pub fn get_password(service: &str, account: &str) -> Result { + let result = String::from_utf8(get_generic_password(&service, &account)?)?; + Ok(result) +} + +pub fn get_password_keytar(service: &str, account: &str) -> Result { + get_password(service, account) +} + +pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { + let result = set_generic_password(&service, &account, password.as_bytes())?; + Ok(result) +} + +pub fn delete_password(service: &str, account: &str) -> Result<()> { + let result = delete_generic_password(&service, &account)?; + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); + set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + assert_eq!( + "Random", + get_password("BitwardenTest", "BitwardenTest").unwrap() + ); + delete_password("BitwardenTest", "BitwardenTest").unwrap(); + + // Ensure password is deleted + match get_password("BitwardenTest", "BitwardenTest") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!( + "The specified item could not be found in the keychain.", + e.to_string() + ), + } + } + + #[test] + fn test_error_no_password() { + match get_password("Unknown", "Unknown") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!( + "The specified item could not be found in the keychain.", + e.to_string() + ), + } + } +} diff --git a/desktop_native/src/password/mod.rs b/desktop_native/src/password/mod.rs new file mode 100644 index 00000000..3cc0c28e --- /dev/null +++ b/desktop_native/src/password/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "linux", path = "unix.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "macos.rs")] +mod password; +pub use password::*; diff --git a/desktop_native/src/password/unix.rs b/desktop_native/src/password/unix.rs new file mode 100644 index 00000000..fa808613 --- /dev/null +++ b/desktop_native/src/password/unix.rs @@ -0,0 +1,91 @@ +use anyhow::{anyhow, Result}; +use libsecret::{password_clear_sync, password_lookup_sync, password_store_sync, Schema}; +use std::collections::HashMap; + +pub fn get_password(service: &str, account: &str) -> Result { + let res = password_lookup_sync( + Some(&get_schema()), + build_attributes(service, account), + gio::Cancellable::NONE, + )?; + + match res { + Some(s) => Ok(String::from(s)), + None => Err(anyhow!("No password found")), + } +} + +pub fn get_password_keytar(service: &str, account: &str) -> Result { + get_password(service, account) +} + +pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { + let result = password_store_sync( + Some(&get_schema()), + build_attributes(service, account), + Some(&libsecret::COLLECTION_DEFAULT), + &format!("{}/{}", service, account), + password, + gio::Cancellable::NONE, + )?; + Ok(result) +} + +pub fn delete_password(service: &str, account: &str) -> Result<()> { + let result = password_clear_sync( + Some(&get_schema()), + build_attributes(service, account), + gio::Cancellable::NONE, + )?; + Ok(result) +} + +fn get_schema() -> Schema { + let mut attributes = std::collections::HashMap::new(); + attributes.insert("service", libsecret::SchemaAttributeType::String); + attributes.insert("account", libsecret::SchemaAttributeType::String); + + libsecret::Schema::new( + "org.freedesktop.Secret.Generic", + libsecret::SchemaFlags::NONE, + attributes, + ) +} + +fn build_attributes<'a>(service: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> { + let mut attributes = HashMap::new(); + attributes.insert("service", service); + attributes.insert("account", account); + + attributes +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); + set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + assert_eq!( + "Random", + get_password("BitwardenTest", "BitwardenTest").unwrap() + ); + delete_password("BitwardenTest", "BitwardenTest").unwrap(); + + // Ensure password is deleted + match get_password("BitwardenTest", "BitwardenTest") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!("No password found", e.to_string()), + } + } + + #[test] + fn test_error_no_password() { + match get_password("BitwardenTest", "BitwardenTest") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!("No password found", e.to_string()), + } + } +} diff --git a/desktop_native/src/password/windows.rs b/desktop_native/src/password/windows.rs new file mode 100644 index 00000000..ed2e643c --- /dev/null +++ b/desktop_native/src/password/windows.rs @@ -0,0 +1,180 @@ +use anyhow::{anyhow, Result}; +use widestring::{U16CString, U16String}; +use windows::Win32::{ + Foundation::{GetLastError, ERROR_NOT_FOUND, FILETIME, PWSTR, WIN32_ERROR}, + Security::Credentials::{ + CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CRED_FLAGS, + CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC, + }, +}; + +const CRED_FLAGS_NONE: u32 = 0; + +pub fn get_password<'a>(service: &str, account: &str) -> Result { + let target_name = U16CString::from_str(target_name(service, account))?; + + let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); + let credential_ptr = &mut credential; + + let result = unsafe { + CredReadW( + PWSTR(target_name.as_ptr()), + CRED_TYPE_GENERIC.0, + CRED_FLAGS_NONE, + credential_ptr, + ) + }; + + scopeguard::defer!({ + unsafe { CredFree(credential as *mut _) }; + }); + + if !result.as_bool() { + return Err(anyhow!(convert_error(unsafe { GetLastError() }))); + } + + let password = unsafe { + U16String::from_ptr( + (*credential).CredentialBlob as *const u16, + (*credential).CredentialBlobSize as usize / 2, + ) + .to_string_lossy() + }; + + Ok(String::from(password)) +} + +// Remove this after sufficient releases +pub fn get_password_keytar<'a>(service: &str, account: &str) -> Result { + let target_name = U16CString::from_str(target_name(service, account))?; + + let mut credential: *mut CREDENTIALW = std::ptr::null_mut(); + let credential_ptr = &mut credential; + + let result = unsafe { + CredReadW( + PWSTR(target_name.as_ptr()), + CRED_TYPE_GENERIC.0, + CRED_FLAGS_NONE, + credential_ptr, + ) + }; + + scopeguard::defer!({ + unsafe { CredFree(credential as *mut _) }; + }); + + if !result.as_bool() { + return Err(anyhow!(unsafe { GetLastError() }.0.to_string())); + } + + let password = unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + (*credential).CredentialBlob, + (*credential).CredentialBlobSize as usize, + )) + }; + + Ok(String::from(password)) +} + +pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> { + let target_name = U16CString::from_str(target_name(service, account))?; + let user_name = U16CString::from_str(account)?; + let last_written = FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + }; + + let credential = U16CString::from_str(password)?; + let credential_len = password.len() as u32 * 2; + + let credential = CREDENTIALW { + Flags: CRED_FLAGS(CRED_FLAGS_NONE), + Type: CRED_TYPE_GENERIC, + TargetName: PWSTR(target_name.as_ptr()), + Comment: PWSTR::default(), + LastWritten: last_written, + CredentialBlobSize: credential_len, + CredentialBlob: credential.as_ptr() as *mut u8, + Persist: CRED_PERSIST_ENTERPRISE, + AttributeCount: 0, + Attributes: std::ptr::null_mut(), + TargetAlias: PWSTR::default(), + UserName: PWSTR(user_name.as_ptr()), + }; + + let result = unsafe { CredWriteW(&credential, 0) }; + if !result.as_bool() { + return Err(anyhow!(unsafe { GetLastError() }.0.to_string())); + } + + Ok(()) +} + +pub fn delete_password(service: &str, account: &str) -> Result<()> { + let target_name = U16CString::from_str(target_name(service, account))?; + + unsafe { + CredDeleteW( + PWSTR(target_name.as_ptr()), + CRED_TYPE_GENERIC.0, + CRED_FLAGS_NONE, + ) + .ok()? + }; + + Ok(()) +} + +fn target_name(service: &str, account: &str) -> String { + format!("{}/{}", service, account) +} + +// Convert the internal WIN32 errors to descriptive messages +fn convert_error(code: WIN32_ERROR) -> String { + match code { + ERROR_NOT_FOUND => String::from("Password not found."), + _ => code.0.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); + set_password("BitwardenTest", "BitwardenTest", "Random").unwrap(); + assert_eq!( + "Random", + get_password("BitwardenTest", "BitwardenTest").unwrap() + ); + delete_password("BitwardenTest", "BitwardenTest").unwrap(); + + // Ensure password is deleted + match get_password("BitwardenTest", "BitwardenTest") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!("Password not found.", e.to_string()), + } + } + + #[test] + fn test_get_password_keytar() { + scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({});); + keytar::set_password("BitwardenTest", "BitwardenTest", "HelloFromKeytar").unwrap(); + assert_eq!( + "HelloFromKeytar", + get_password_keytar("BitwardenTest", "BitwardenTest").unwrap() + ); + } + + #[test] + fn test_error_no_password() { + match get_password("BitwardenTest", "BitwardenTest") { + Ok(_) => panic!("Got a result"), + Err(e) => assert_eq!("Password not found.", e.to_string()), + } + } +} diff --git a/jslib b/jslib index 4d58200e..f3a4fde5 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 4d58200ee90fb4fafa5d4f9f4c957654d4da306d +Subproject commit f3a4fde513dc16779e85597bd00027b160671db7 diff --git a/package-lock.json b/package-lock.json index b1fdd894..96814657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,12 @@ "@angular/platform-browser": "^12.2.13", "@angular/platform-browser-dynamic": "^12.2.13", "@angular/router": "^12.2.13", + "@bitwarden/desktop-native": "file:desktop_native", "@bitwarden/jslib-angular": "file:jslib/angular", "@bitwarden/jslib-common": "file:jslib/common", "@bitwarden/jslib-electron": "file:jslib/electron", + "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", + "forcefocus": "^1.1.0", "ngx-toastr": "14.1.4", "node-ipc": "^9.1.4", "nord": "^0.2.1", @@ -72,6 +75,15 @@ "npm": "~8" } }, + "desktop_native": { + "version": "0.1.0", + "hasInstallScript": true, + "license": "GPL-3.0", + "devDependencies": { + "@napi-rs/cli": "^2.4.4", + "cargo-cp-artifact": "^0.1" + } + }, "jslib/angular": { "name": "@bitwarden/jslib-common", "version": "0.0.0", @@ -131,13 +143,10 @@ "license": "GPL-3.0", "dependencies": { "@bitwarden/jslib-common": "file:../common", - "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "electron": "16.0.7", "electron-log": "4.4.1", "electron-store": "8.0.1", - "electron-updater": "4.6.1", - "forcefocus": "^1.1.0", - "keytar": "7.7.0" + "electron-updater": "4.6.1" }, "devDependencies": { "@types/node": "^16.11.12", @@ -679,6 +688,10 @@ "node": ">=6.9.0" } }, + "node_modules/@bitwarden/desktop-native": { + "resolved": "desktop_native", + "link": true + }, "node_modules/@bitwarden/jslib-angular": { "resolved": "jslib/angular", "link": true @@ -1098,6 +1111,22 @@ "msgpack5": "^4.5.0" } }, + "node_modules/@napi-rs/cli": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.4.4.tgz", + "integrity": "sha512-f+tvwCv1ka24dBqI2DgBhR7Oxl3DKHOp4onxLXwyBFt6iCADnr3YZIr1/2Iq5r3uqxFgaf01bfPsRQZPkEp0kQ==", + "dev": true, + "bin": { + "napi": "scripts/index.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@ngtools/webpack": { "version": "12.2.14", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.2.14.tgz", @@ -2784,6 +2813,15 @@ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", "dev": true }, + "node_modules/cargo-cp-artifact": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.6.tgz", + "integrity": "sha512-CQw0doK/aaF7j041666XzuilHxqMxaKkn+I5vmBsd8SAwS0cO5CqVEVp0xJwOKstyqWZ6WK4Ww3O6p26x/Goyg==", + "dev": true, + "bin": { + "cargo-cp-artifact": "bin/cargo-cp-artifact.js" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -6666,173 +6704,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" - } - }, - "node_modules/keytar/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/keytar/node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "node_modules/keytar/node_modules/are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "node_modules/keytar/node_modules/gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dependencies": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "node_modules/keytar/node_modules/is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dependencies": { - "number-is-nan": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/keytar/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node_modules/keytar/node_modules/node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", - "dependencies": { - "semver": "^5.4.1" - } - }, - "node_modules/keytar/node_modules/node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" - }, - "node_modules/keytar/node_modules/npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dependencies": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "node_modules/keytar/node_modules/prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", - "dependencies": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keytar/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/keytar/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/keytar/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/keytar/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/keytar/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/keyv": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", @@ -11675,6 +11546,13 @@ "to-fast-properties": "^2.0.0" } }, + "@bitwarden/desktop-native": { + "version": "file:desktop_native", + "requires": { + "@napi-rs/cli": "^2.4.4", + "cargo-cp-artifact": "^0.1" + } + }, "@bitwarden/jslib-angular": { "version": "file:jslib/angular", "requires": { @@ -11724,14 +11602,11 @@ "version": "file:jslib/electron", "requires": { "@bitwarden/jslib-common": "file:../common", - "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", "@types/node": "^16.11.12", "electron": "16.0.7", "electron-log": "4.4.1", "electron-store": "8.0.1", "electron-updater": "4.6.1", - "forcefocus": "^1.1.0", - "keytar": "7.7.0", "rimraf": "^3.0.2", "typescript": "4.3.5" } @@ -12055,6 +11930,12 @@ "msgpack5": "^4.5.0" } }, + "@napi-rs/cli": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-2.4.4.tgz", + "integrity": "sha512-f+tvwCv1ka24dBqI2DgBhR7Oxl3DKHOp4onxLXwyBFt6iCADnr3YZIr1/2Iq5r3uqxFgaf01bfPsRQZPkEp0kQ==", + "dev": true + }, "@ngtools/webpack": { "version": "12.2.14", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-12.2.14.tgz", @@ -13356,6 +13237,12 @@ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", "dev": true }, + "cargo-cp-artifact": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.6.tgz", + "integrity": "sha512-CQw0doK/aaF7j041666XzuilHxqMxaKkn+I5vmBsd8SAwS0cO5CqVEVp0xJwOKstyqWZ6WK4Ww3O6p26x/Goyg==", + "dev": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -16254,153 +16141,6 @@ "universalify": "^2.0.0" } }, - "keytar": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.7.0.tgz", - "integrity": "sha512-YEY9HWqThQc5q5xbXbRwsZTh2PJ36OSYRjSv3NN2xf5s5dpLTjEZnC2YikR29OaVybf9nQ0dJ/80i40RS97t/A==", - "requires": { - "node-addon-api": "^3.0.0", - "prebuild-install": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "node-abi": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz", - "integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==", - "requires": { - "semver": "^5.4.1" - } - }, - "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "prebuild-install": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz", - "integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==", - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.21.0", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, "keyv": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.4.tgz", diff --git a/package.json b/package.json index 5b3bfaaf..1c432e14 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,10 @@ "asarUnpack": [ "**/*.node" ], + "files": [ + "**/*", + "!**/node_modules/@bitwarden/desktop-native/**/*" + ], "mac": { "electronUpdaterCompatibility": ">=0.0.1", "category": "public.app-category.productivity", @@ -304,9 +308,12 @@ "@angular/platform-browser": "^12.2.13", "@angular/platform-browser-dynamic": "^12.2.13", "@angular/router": "^12.2.13", + "@bitwarden/desktop-native": "file:desktop_native", "@bitwarden/jslib-angular": "file:jslib/angular", "@bitwarden/jslib-common": "file:jslib/common", "@bitwarden/jslib-electron": "file:jslib/electron", + "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", + "forcefocus": "^1.1.0", "ngx-toastr": "14.1.4", "node-ipc": "^9.1.4", "nord": "^0.2.1", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 34cdb56c..8923eb45 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -37,7 +37,7 @@ import { TokenService } from "jslib-common/abstractions/token.service"; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { CipherType } from "jslib-common/enums/cipherType"; -import { MenuUpdateRequest } from "src/main/menu.updater"; +import { MenuUpdateRequest } from "../main/menu/menu.updater"; import { PremiumComponent } from "./accounts/premium.component"; import { SettingsComponent } from "./accounts/settings.component"; diff --git a/src/main.ts b/src/main.ts index 0aecf0be..66cc06a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,11 +2,9 @@ import * as path from "path"; import { app } from "electron"; -import { BiometricMain } from "jslib-common/abstractions/biometric.main"; import { StateFactory } from "jslib-common/factories/stateFactory"; import { GlobalState } from "jslib-common/models/domain/globalState"; import { StateService } from "jslib-common/services/state.service"; -import { KeytarStorageListener } from "jslib-electron/keytarStorageListener"; import { ElectronLogService } from "jslib-electron/services/electronLog.service"; import { ElectronMainMessagingService } from "jslib-electron/services/electronMainMessaging.service"; import { ElectronStorageService } from "jslib-electron/services/electronStorage.service"; @@ -14,7 +12,9 @@ import { TrayMain } from "jslib-electron/tray.main"; import { UpdaterMain } from "jslib-electron/updater.main"; import { WindowMain } from "jslib-electron/window.main"; -import { MenuMain } from "./main/menu.main"; +import { BiometricMain } from "./main/biometric/biometric.main"; +import { DesktopCredentialStorageListener } from "./main/desktopCredentialStorageListener"; +import { MenuMain } from "./main/menu/menu.main"; import { MessagingMain } from "./main/messaging.main"; import { NativeMessagingMain } from "./main/nativeMessaging.main"; import { PowerMonitorMain } from "./main/powerMonitor.main"; @@ -27,7 +27,7 @@ export class Main { storageService: ElectronStorageService; messagingService: ElectronMainMessagingService; stateService: StateService; - keytarStorageListener: KeytarStorageListener; + desktopCredentialStorageListener: DesktopCredentialStorageListener; windowMain: WindowMain; messagingMain: MessagingMain; @@ -116,7 +116,7 @@ export class Main { if (process.platform === "win32") { // eslint-disable-next-line - const BiometricWindowsMain = require("jslib-electron/biometric.windows.main").default; + const BiometricWindowsMain = require("./main/biometric/biometric.windows.main").default; this.biometricMain = new BiometricWindowsMain( this.i18nService, this.windowMain, @@ -125,11 +125,14 @@ export class Main { ); } else if (process.platform === "darwin") { // eslint-disable-next-line - const BiometricDarwinMain = require("jslib-electron/biometric.darwin.main").default; + const BiometricDarwinMain = require("./main/biometric/biometric.darwin.main").default; this.biometricMain = new BiometricDarwinMain(this.i18nService, this.stateService); } - this.keytarStorageListener = new KeytarStorageListener("Bitwarden", this.biometricMain); + this.desktopCredentialStorageListener = new DesktopCredentialStorageListener( + "Bitwarden", + this.biometricMain + ); this.nativeMessagingMain = new NativeMessagingMain( this.logService, @@ -140,7 +143,7 @@ export class Main { } bootstrap() { - this.keytarStorageListener.init(); + this.desktopCredentialStorageListener.init(); this.windowMain.init().then( async () => { const locale = await this.stateService.getLocale(); diff --git a/src/main/biometric/biometric.darwin.main.ts b/src/main/biometric/biometric.darwin.main.ts new file mode 100644 index 00000000..03ee5460 --- /dev/null +++ b/src/main/biometric/biometric.darwin.main.ts @@ -0,0 +1,36 @@ +import { ipcMain, systemPreferences } from "electron"; + +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { StateService } from "jslib-common/abstractions/state.service"; + +import { BiometricMain } from "../biometric/biometric.main"; + +export default class BiometricDarwinMain implements BiometricMain { + isError = false; + + constructor(private i18nservice: I18nService, private stateService: StateService) {} + + async init() { + await this.stateService.setEnableBiometric(await this.supportsBiometric()); + await this.stateService.setBiometricText("unlockWithTouchId"); + await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptTouchId"); + + // eslint-disable-next-line + ipcMain.on("biometric", async (event: any, message: any) => { + event.returnValue = await this.authenticateBiometric(); + }); + } + + supportsBiometric(): Promise { + return Promise.resolve(systemPreferences.canPromptTouchID()); + } + + async authenticateBiometric(): Promise { + try { + await systemPreferences.promptTouchID(this.i18nservice.t("touchIdConsentMessage")); + return true; + } catch { + return false; + } + } +} diff --git a/src/main/biometric/biometric.main.ts b/src/main/biometric/biometric.main.ts new file mode 100644 index 00000000..a8557c40 --- /dev/null +++ b/src/main/biometric/biometric.main.ts @@ -0,0 +1,6 @@ +export abstract class BiometricMain { + isError: boolean; + init: () => Promise; + supportsBiometric: () => Promise; + authenticateBiometric: () => Promise; +} diff --git a/src/main/biometric/biometric.windows.main.ts b/src/main/biometric/biometric.windows.main.ts new file mode 100644 index 00000000..aac4f4dc --- /dev/null +++ b/src/main/biometric/biometric.windows.main.ts @@ -0,0 +1,146 @@ +import { ipcMain } from "electron"; +import forceFocus from "forcefocus"; + +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { WindowMain } from "jslib-electron/window.main"; + +import { BiometricMain } from "src/main/biometric/biometric.main"; + +export default class BiometricWindowsMain implements BiometricMain { + isError = false; + + private windowsSecurityCredentialsUiModule: any; + + constructor( + private i18nservice: I18nService, + private windowMain: WindowMain, + private stateService: StateService, + private logService: LogService + ) {} + + async init() { + this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule(); + let supportsBiometric = false; + try { + supportsBiometric = await this.supportsBiometric(); + } catch { + // store error state so we can let the user know on the settings page + this.isError = true; + } + await this.stateService.setEnableBiometric(supportsBiometric); + await this.stateService.setBiometricText("unlockWithWindowsHello"); + await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptWindowsHello"); + + ipcMain.on("biometric", async (event: any, message: any) => { + event.returnValue = await this.authenticateBiometric(); + }); + } + + async supportsBiometric(): Promise { + const availability = await this.checkAvailabilityAsync(); + + return this.getAllowedAvailabilities().includes(availability); + } + + async authenticateBiometric(): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module == null) { + return false; + } + + const verification = await this.requestVerificationAsync( + this.i18nservice.t("windowsHelloConsentMessage") + ); + + return verification === module.UserConsentVerificationResult.verified; + } + + getWindowsSecurityCredentialsUiModule(): any { + try { + if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) { + this.windowsSecurityCredentialsUiModule = require("@nodert-win10-rs4/windows.security.credentials.ui"); + } + return this.windowsSecurityCredentialsUiModule; + } catch { + this.isError = true; + } + return null; + } + + async checkAvailabilityAsync(): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + // eslint-disable-next-line + return new Promise((resolve, reject) => { + try { + module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + }); + } catch { + this.isError = true; + return resolve(null); + } + }); + } + return Promise.resolve(null); + } + + async requestVerificationAsync(message: string): Promise { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return new Promise((resolve, reject) => { + try { + module.UserConsentVerifier.requestVerificationAsync( + message, + (error: Error, result: any) => { + if (error) { + return resolve(null); + } + return resolve(result); + } + ); + + forceFocus.focusWindow(this.windowMain.win); + } catch (error) { + this.isError = true; + return reject(error); + } + }); + } + return Promise.resolve(null); + } + + getAllowedAvailabilities(): any[] { + try { + const module = this.getWindowsSecurityCredentialsUiModule(); + if (module != null) { + return [ + module.UserConsentVerifierAvailability.available, + module.UserConsentVerifierAvailability.deviceBusy, + ]; + } + } catch { + /*Ignore error*/ + } + return []; + } + + getWindowsMajorVersion(): number { + if (process.platform !== "win32") { + return -1; + } + try { + // eslint-disable-next-line + const version = require("os").release(); + return Number.parseInt(version.split(".")[0], 10); + } catch { + this.logService.error("Unable to resolve windows major version number"); + } + return -1; + } +} diff --git a/src/main/desktopCredentialStorageListener.ts b/src/main/desktopCredentialStorageListener.ts new file mode 100644 index 00000000..70c9c7c9 --- /dev/null +++ b/src/main/desktopCredentialStorageListener.ts @@ -0,0 +1,63 @@ +import { passwords } from "@bitwarden/desktop-native"; +import { ipcMain } from "electron"; + +import { BiometricMain } from "./biometric/biometric.main"; + +const AuthRequiredSuffix = "_biometric"; +const AuthenticatedActions = ["getPassword"]; + +export class DesktopCredentialStorageListener { + constructor(private serviceName: string, private biometricService: BiometricMain) {} + + init() { + ipcMain.on("keytar", async (event: any, message: any) => { + try { + let serviceName = this.serviceName; + message.keySuffix = "_" + (message.keySuffix ?? ""); + if (message.keySuffix !== "_") { + serviceName += message.keySuffix; + } + + const authenticationRequired = + AuthenticatedActions.includes(message.action) && AuthRequiredSuffix === message.keySuffix; + const authenticated = !authenticationRequired || (await this.authenticateBiometric()); + + let val: string | boolean = null; + if (authenticated && message.action && message.key) { + if (message.action === "getPassword") { + val = await this.getPassword(serviceName, message.key); + } else if (message.action === "hasPassword") { + const result = await this.getPassword(serviceName, message.key); + val = result != null; + } else if (message.action === "setPassword" && message.value) { + await passwords.setPassword(serviceName, message.key, message.value); + } else if (message.action === "deletePassword") { + await passwords.deletePassword(serviceName, message.key); + } + } + event.returnValue = val; + } catch { + event.returnValue = null; + } + }); + } + + // Gracefully handle old keytar values, and if detected updated the entry to the proper format + private async getPassword(serviceName: string, key: string) { + let val = await passwords.getPassword(serviceName, key); + try { + JSON.parse(val); + } catch (e) { + val = await passwords.getPasswordKeytar(serviceName, key); + await passwords.setPassword(serviceName, key, val); + } + return val; + } + + private async authenticateBiometric(): Promise { + if (this.biometricService) { + return await this.biometricService.authenticateBiometric(); + } + return false; + } +} diff --git a/src/main/menu.about.ts b/src/main/menu/menu.about.ts similarity index 100% rename from src/main/menu.about.ts rename to src/main/menu/menu.about.ts diff --git a/src/main/menu.account.ts b/src/main/menu/menu.account.ts similarity index 100% rename from src/main/menu.account.ts rename to src/main/menu/menu.account.ts diff --git a/src/main/menu.bitwarden.ts b/src/main/menu/menu.bitwarden.ts similarity index 100% rename from src/main/menu.bitwarden.ts rename to src/main/menu/menu.bitwarden.ts diff --git a/src/main/menu.edit.ts b/src/main/menu/menu.edit.ts similarity index 100% rename from src/main/menu.edit.ts rename to src/main/menu/menu.edit.ts diff --git a/src/main/menu.file.ts b/src/main/menu/menu.file.ts similarity index 100% rename from src/main/menu.file.ts rename to src/main/menu/menu.file.ts diff --git a/src/main/menu.first.ts b/src/main/menu/menu.first.ts similarity index 100% rename from src/main/menu.first.ts rename to src/main/menu/menu.first.ts diff --git a/src/main/menu.help.ts b/src/main/menu/menu.help.ts similarity index 100% rename from src/main/menu.help.ts rename to src/main/menu/menu.help.ts diff --git a/src/main/menu.main.ts b/src/main/menu/menu.main.ts similarity index 97% rename from src/main/menu.main.ts rename to src/main/menu/menu.main.ts index ce548967..2cc19354 100644 --- a/src/main/menu.main.ts +++ b/src/main/menu/menu.main.ts @@ -2,7 +2,7 @@ import { app, Menu } from "electron"; import { BaseMenu } from "jslib-electron/baseMenu"; -import { Main } from "../main"; +import { Main } from "../../main"; import { MenuUpdateRequest } from "./menu.updater"; import { Menubar } from "./menubar"; diff --git a/src/main/menu.updater.ts b/src/main/menu/menu.updater.ts similarity index 100% rename from src/main/menu.updater.ts rename to src/main/menu/menu.updater.ts diff --git a/src/main/menu.view.ts b/src/main/menu/menu.view.ts similarity index 100% rename from src/main/menu.view.ts rename to src/main/menu/menu.view.ts diff --git a/src/main/menu.window.ts b/src/main/menu/menu.window.ts similarity index 100% rename from src/main/menu.window.ts rename to src/main/menu/menu.window.ts diff --git a/src/main/menubar.ts b/src/main/menu/menubar.ts similarity index 100% rename from src/main/menubar.ts rename to src/main/menu/menubar.ts diff --git a/src/main/messaging.main.ts b/src/main/messaging.main.ts index 811cb3b6..290109ee 100644 --- a/src/main/messaging.main.ts +++ b/src/main/messaging.main.ts @@ -7,7 +7,7 @@ import { StateService } from "jslib-common/abstractions/state.service"; import { Main } from "../main"; -import { MenuUpdateRequest } from "./menu.updater"; +import { MenuUpdateRequest } from "./menu/menu.updater"; const SyncInterval = 5 * 60 * 1000; // 5 minutes diff --git a/src/package.json b/src/package.json index d6e9a623..d6989fc4 100644 --- a/src/package.json +++ b/src/package.json @@ -12,8 +12,8 @@ "url": "https://github.com/bitwarden/desktop" }, "dependencies": { + "@bitwarden/desktop-native": "file:../desktop_native", "@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4", - "forcefocus": "^1.1.0", - "keytar": "7.7.0" + "forcefocus": "^1.1.0" } }