mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-13 00:51:45 +01:00
[PM-9035] desktop build logic to provide credentials to os on sync (#10181)
* feat: scaffold desktop_objc * feat: rename fido2 to autofill * feat: scaffold electron autofill * feat: auto call hello world on init * feat: scaffold call to basic objc function * feat: simple log that checks if autofill is enabled * feat: adding some availability guards * feat: scaffold services and allow calls from inspector * feat: create custom type for returning strings across rust/objc boundary * chore: clean up comments * feat: enable ARC * feat: add util function `c_string_to_nsstring` * chore: refactor and rename to `run_command` * feat: add try-catch around command execution * feat: properly implement command calling Add static typing. Add proper error handling. * feat: add autoreleasepool to avoid memory leaks * chore: change objc names to camelCase * fix: error returning * feat: extract some helper functions into utils class * feat: scaffold status command * feat: implement status command * feat: implement password credential mapping * wip: implement sync command This crashes because we are not properly handling the fact that `saveCredentialIdentities` uses callbacks, resulting in a race condition where we try to access a variable (result) that has already gotten dealloc'd. * feat: first version of callback * feat: make run_command async * feat: functioning callback returns * chore: refactor to make objc code easier to read and use * feat: refactor everything to use new callback return method * feat: re-implement status command with callback * fix: warning about CommandContext not being FFI-safe * feat: implement sync command using callbacks * feat: implement manual password credential sync * feat: add auto syncing * docs: add todo * feat: add support for passkeys * chore: move desktop autofill service to init service * feat: auto-add all .m files to builder * fix: native build on unix and windows * fix: unused compiler warnings * fix: napi type exports * feat: add corresponding dist command * feat: comment signing profile until we fix signing * fix: build breaking on non-macOS platforms * chore: cargo lock update * chore: revert accidental version change * feat: put sync behind feature flag * chore: put files in autofill folder * fix: obj-c code not recompiling on changes * feat: add `namespace` to commands * fix: linting complaining about flag * feat: add autofill as owner of their objc code * chore: make autofill owner of run_command in core crate * fix: re-add napi annotation * fix: remove dev bypass
This commit is contained in:
parent
f95cc7b82c
commit
f16bfa4cd2
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -103,6 +103,8 @@ apps/web/src/app/layouts @bitwarden/team-design-system
|
||||
|
||||
## Desktop native module ##
|
||||
apps/desktop/desktop_native @bitwarden/team-platform-dev
|
||||
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
|
||||
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
|
||||
|
||||
## Key management team files ##
|
||||
apps/desktop/src/key-management @bitwarden/team-key-management-dev
|
||||
|
245
apps/desktop/desktop_native/Cargo.lock
generated
245
apps/desktop/desktop_native/Cargo.lock
generated
@ -62,6 +62,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
@ -147,9 +153,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.3.4"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8"
|
||||
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
@ -412,9 +418,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
|
||||
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
@ -427,9 +433,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.34"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9"
|
||||
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@ -474,6 +480,32 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.0"
|
||||
@ -517,6 +549,16 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.0"
|
||||
@ -535,9 +577,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
|
||||
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@ -561,9 +603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@ -606,25 +648,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.129"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007"
|
||||
checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxxbridge-cmd",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"foldhash",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.129"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc"
|
||||
checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
@ -632,19 +675,33 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.129"
|
||||
name = "cxxbridge-cmd"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d422aff542b4fa28c2ce8e5cc202d42dbf24702345c1fba3087b2d3f8a1b90ff"
|
||||
checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.129"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f"
|
||||
checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@ -692,7 +749,8 @@ dependencies = [
|
||||
"bitwarden-russh",
|
||||
"byteorder",
|
||||
"cbc",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"desktop_objc",
|
||||
"dirs",
|
||||
"ed25519",
|
||||
"futures",
|
||||
@ -743,6 +801,18 @@ dependencies = [
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "desktop_objc"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
"core-foundation 0.9.4",
|
||||
"glob",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "desktop_proxy"
|
||||
version = "0.0.0"
|
||||
@ -874,12 +944,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -901,9 +971,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
@ -911,9 +981,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.1"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
|
||||
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
@ -933,6 +1003,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@ -983,9 +1059,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210"
|
||||
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
@ -1083,16 +1159,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.1"
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
@ -1138,9 +1214,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.6.0"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@ -1173,9 +1249,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
|
||||
|
||||
[[package]]
|
||||
name = "keytar"
|
||||
@ -1215,9 +1291,9 @@ checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.5"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
@ -1312,11 +1388,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys 0.52.0",
|
||||
@ -1859,13 +1934,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.7.3"
|
||||
version = "3.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511"
|
||||
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.4.0",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
@ -1921,9 +1996,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.89"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -2016,9 +2091,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -2079,18 +2154,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
version = "0.38.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
|
||||
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@ -2099,6 +2174,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
@ -2144,7 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.0",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
@ -2168,18 +2249,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.214"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
||||
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.214"
|
||||
version = "1.0.215"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
||||
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2272,9 +2353,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.7"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
|
||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
@ -2349,6 +2430,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@ -2357,9 +2444,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
version = "2.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2368,9 +2455,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.13.0"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
|
||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
@ -2410,9 +2497,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.36"
|
||||
version = "0.3.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
@ -2433,9 +2520,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
|
||||
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@ -2511,9 +2598,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
@ -2522,9 +2609,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.27"
|
||||
version = "0.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2533,9 +2620,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.32"
|
||||
version = "0.1.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
@ -2572,9 +2659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
|
@ -23,7 +23,7 @@ sys = [
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.93"
|
||||
arboard = { version = "=3.4.1", default-features = false, features = [
|
||||
"wayland-data-control",
|
||||
"wayland-data-control",
|
||||
] }
|
||||
argon2 = { version = "=0.5.3", features = ["zeroize"] }
|
||||
async-stream = "=0.3.6"
|
||||
@ -44,10 +44,10 @@ scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false, features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
] }
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" }
|
||||
tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] }
|
||||
@ -81,6 +81,7 @@ keytar = "=0.1.6"
|
||||
core-foundation = { version = "=0.10.0", optional = true }
|
||||
security-framework = { version = "=3.0.0", optional = true }
|
||||
security-framework-sys = { version = "=2.12.0", optional = true }
|
||||
desktop_objc = { path = "../objc" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
oo7 = "=0.3.3"
|
||||
|
5
apps/desktop/desktop_native/core/src/autofill/macos.rs
Normal file
5
apps/desktop/desktop_native/core/src/autofill/macos.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_command(value: String) -> Result<String> {
|
||||
desktop_objc::run_command(value).await
|
||||
}
|
5
apps/desktop/desktop_native/core/src/autofill/mod.rs
Normal file
5
apps/desktop/desktop_native/core/src/autofill/mod.rs
Normal file
@ -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 autofill;
|
||||
pub use autofill::*;
|
5
apps/desktop/desktop_native/core/src/autofill/unix.rs
Normal file
5
apps/desktop/desktop_native/core/src/autofill/unix.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_command(value: String) -> Result<String> {
|
||||
todo!("Unix does not support autofill");
|
||||
}
|
5
apps/desktop/desktop_native/core/src/autofill/windows.rs
Normal file
5
apps/desktop/desktop_native/core/src/autofill/windows.rs
Normal file
@ -0,0 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_command(value: String) -> Result<String> {
|
||||
todo!("Windows does not support autofill");
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub mod autofill;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod biometric;
|
||||
#[cfg(feature = "sys")]
|
||||
@ -8,9 +9,8 @@ pub mod ipc;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod password;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod process_isolation;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod powermonitor;
|
||||
#[cfg(feature = "sys")]
|
||||
|
||||
pub mod process_isolation;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod ssh_agent;
|
||||
|
3
apps/desktop/desktop_native/napi/index.d.ts
vendored
3
apps/desktop/desktop_native/napi/index.d.ts
vendored
@ -122,6 +122,9 @@ export declare namespace ipc {
|
||||
send(message: string): number
|
||||
}
|
||||
}
|
||||
export declare namespace autofill {
|
||||
export function runCommand(value: string): Promise<string>
|
||||
}
|
||||
export declare namespace crypto {
|
||||
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer>
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ pub mod passwords {
|
||||
/// Fetch the stored password from the keychain.
|
||||
#[napi]
|
||||
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
|
||||
desktop_core::password::get_password(&service, &account).await
|
||||
desktop_core::password::get_password(&service, &account)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
@ -19,21 +20,25 @@ pub mod passwords {
|
||||
account: String,
|
||||
password: String,
|
||||
) -> napi::Result<()> {
|
||||
desktop_core::password::set_password(&service, &account, &password).await
|
||||
desktop_core::password::set_password(&service, &account, &password)
|
||||
.await
|
||||
.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<()> {
|
||||
desktop_core::password::delete_password(&service, &account).await
|
||||
desktop_core::password::delete_password(&service, &account)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
// Checks if the os secure storage is available
|
||||
#[napi]
|
||||
pub async fn is_available() -> napi::Result<bool> {
|
||||
desktop_core::password::is_available().await.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
desktop_core::password::is_available()
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,13 +249,17 @@ pub mod sshagent {
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<(String, bool), CalleeHandled>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, (String, bool))>(32);
|
||||
let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32);
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<(u32, (String, bool))>(32);
|
||||
let (auth_response_tx, auth_response_rx) =
|
||||
tokio::sync::broadcast::channel::<(u32, bool)>(32);
|
||||
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
|
||||
tokio::spawn(async move {
|
||||
let _ = auth_response_rx;
|
||||
|
||||
while let Some((request_id, (cipher_uuid, is_list_request))) = auth_request_rx.recv().await {
|
||||
while let Some((request_id, (cipher_uuid, is_list_request))) =
|
||||
auth_request_rx.recv().await
|
||||
{
|
||||
let cloned_request_id = request_id.clone();
|
||||
let cloned_cipher_uuid = cipher_uuid.clone();
|
||||
let cloned_response_tx_arc = auth_response_tx_arc.clone();
|
||||
@ -260,23 +269,33 @@ pub mod sshagent {
|
||||
let cipher_uuid = cloned_cipher_uuid;
|
||||
let auth_response_tx_arc = cloned_response_tx_arc;
|
||||
let callback = cloned_callback;
|
||||
let promise_result: Result<Promise<bool>, napi::Error> =
|
||||
callback.call_async(Ok((cipher_uuid, is_list_request))).await;
|
||||
let promise_result: Result<Promise<bool>, napi::Error> = callback
|
||||
.call_async(Ok((cipher_uuid, is_list_request)))
|
||||
.await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
Ok(result) => {
|
||||
let _ = auth_response_tx_arc.lock().await.send((request_id, result))
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request_id, result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e);
|
||||
let _ = auth_response_tx_arc.lock().await.send((request_id, false))
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e);
|
||||
let _ = auth_response_tx_arc.lock().await.send((request_id, false))
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
}
|
||||
@ -343,7 +362,9 @@ pub mod sshagent {
|
||||
#[napi]
|
||||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
let bitwarden_agent_state = &mut agent_state.state;
|
||||
bitwarden_agent_state.clear_keys().map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
bitwarden_agent_state
|
||||
.clear_keys()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@ -524,6 +545,16 @@ pub mod ipc {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod autofill {
|
||||
#[napi]
|
||||
pub async fn run_command(value: String) -> napi::Result<String> {
|
||||
desktop_core::autofill::run_command(value)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub mod crypto {
|
||||
use napi::bindgen_prelude::Buffer;
|
||||
|
21
apps/desktop/desktop_native/objc/Cargo.toml
Normal file
21
apps/desktop/desktop_native/objc/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_objc"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.93"
|
||||
thiserror = "=1.0.69"
|
||||
tokio = "1.39.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "=0.9.4"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.104"
|
||||
glob = "0.3.1"
|
22
apps/desktop/desktop_native/objc/build.rs
Normal file
22
apps/desktop/desktop_native/objc/build.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use glob::glob;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
let mut builder = cc::Build::new();
|
||||
|
||||
// Auto compile all .m files in the src/native directory
|
||||
for entry in glob("src/native/**/*.m").expect("Failed to read glob pattern") {
|
||||
let path = entry.expect("Failed to read glob entry");
|
||||
builder.file(path.clone());
|
||||
println!("cargo::rerun-if-changed={}", path.display());
|
||||
}
|
||||
|
||||
builder
|
||||
.flag("-fobjc-arc") // Enable Auto Reference Counting (ARC)
|
||||
.compile("autofill");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
// Crate is only supported on macOS
|
||||
}
|
124
apps/desktop/desktop_native/objc/src/lib.rs
Normal file
124
apps/desktop/desktop_native/objc/src/lib.rs
Normal file
@ -0,0 +1,124 @@
|
||||
#![cfg(target_os = "macos")]
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, CStr, CString},
|
||||
os::raw::c_void,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ObjCString {
|
||||
value: *const c_char,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CommandContext {
|
||||
tx: Option<tokio::sync::oneshot::Sender<String>>,
|
||||
}
|
||||
|
||||
impl CommandContext {
|
||||
pub fn new() -> (Self, tokio::sync::oneshot::Receiver<String>) {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<String>();
|
||||
|
||||
(CommandContext { tx: Some(tx) }, rx)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, value: String) -> Result<()> {
|
||||
let tx = self.tx.take().context(
|
||||
"Failed to take Sender from CommandContext. Has this context already returned once?",
|
||||
)?;
|
||||
|
||||
tx.send(value).map_err(|_| {
|
||||
anyhow::anyhow!("Failed to send ObjCString from CommandContext to Rust code")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn as_ptr(&mut self) -> *mut c_void {
|
||||
self as *mut Self as *mut c_void
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ObjCString> for String {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: ObjCString) -> Result<Self> {
|
||||
let c_str = unsafe { CStr::from_ptr(value.value) };
|
||||
let str = c_str
|
||||
.to_str()
|
||||
.context("Failed to convert ObjC output string to &str for use in Rust")?;
|
||||
|
||||
Ok(str.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ObjCString {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
objc::freeObjCString(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod objc {
|
||||
use std::os::raw::c_void;
|
||||
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn runCommand(context: *mut c_void, value: *const c_char);
|
||||
pub fn freeObjCString(value: &ObjCString);
|
||||
}
|
||||
|
||||
/// This function is called from the ObjC code to return the output of the command
|
||||
#[no_mangle]
|
||||
pub extern "C" fn commandReturn(context: &mut CommandContext, value: ObjCString) -> bool {
|
||||
let value: String = match value.try_into() {
|
||||
Ok(value) => value,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error: Failed to convert ObjCString to Rust string during commandReturn: {}",
|
||||
e
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match context.send(value) {
|
||||
Ok(_) => 0,
|
||||
Err(e) => {
|
||||
println!(
|
||||
"Error: Failed to return ObjCString from ObjC code to Rust code: {}",
|
||||
e
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_command(input: String) -> Result<String> {
|
||||
// Convert input to type that can be passed to ObjC code
|
||||
let c_input = CString::new(input)
|
||||
.context("Failed to convert Rust input string to a CString for use in call to ObjC code")?;
|
||||
|
||||
let (mut context, rx) = CommandContext::new();
|
||||
|
||||
// Call ObjC code
|
||||
unsafe { objc::runCommand(context.as_ptr(), c_input.as_ptr()) };
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
let objc_output = rx.await?.try_into()?;
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
// let objc_output = output.try_into()?;
|
||||
|
||||
Ok(objc_output)
|
||||
}
|
2
apps/desktop/desktop_native/objc/src/native/.clangd
Normal file
2
apps/desktop/desktop_native/objc/src/native/.clangd
Normal file
@ -0,0 +1,2 @@
|
||||
CompileFlags:
|
||||
Add: [-fobjc-arc]
|
@ -0,0 +1,8 @@
|
||||
#ifndef STATUS_H
|
||||
#define STATUS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void status(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
@ -0,0 +1,57 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStore.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStoreState.h>
|
||||
#import "../../interop.h"
|
||||
#import "status.h"
|
||||
|
||||
void storeState(void (^callback)(ASCredentialIdentityStoreState*)) {
|
||||
if (@available(macos 11, *)) {
|
||||
ASCredentialIdentityStore *store = [ASCredentialIdentityStore sharedStore];
|
||||
[store getCredentialIdentityStoreStateWithCompletion:^(ASCredentialIdentityStoreState * _Nonnull state) {
|
||||
callback(state);
|
||||
}];
|
||||
} else {
|
||||
callback(nil);
|
||||
}
|
||||
}
|
||||
|
||||
BOOL fido2Supported() {
|
||||
if (@available(macos 14, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
BOOL passwordSupported() {
|
||||
if (@available(macos 11, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
void status(void* context, __attribute__((unused)) NSDictionary *params) {
|
||||
storeState(^(ASCredentialIdentityStoreState *state) {
|
||||
BOOL enabled = NO;
|
||||
BOOL supportsIncremental = NO;
|
||||
|
||||
if (state != nil) {
|
||||
enabled = state.isEnabled;
|
||||
supportsIncremental = state.supportsIncrementalUpdates;
|
||||
}
|
||||
|
||||
_return(context,
|
||||
_success(@{
|
||||
@"support": @{
|
||||
@"fido2": @(fido2Supported()),
|
||||
@"password": @(passwordSupported()),
|
||||
@"incrementalUpdates": @(supportsIncremental),
|
||||
},
|
||||
@"state": @{
|
||||
@"enabled": @(enabled),
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
#ifndef SYNC_H
|
||||
#define SYNC_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void runSync(void *context, NSDictionary *params);
|
||||
|
||||
#endif
|
@ -0,0 +1,59 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStore.h>
|
||||
#import <AuthenticationServices/ASCredentialIdentityStoreState.h>
|
||||
#import <AuthenticationServices/ASCredentialServiceIdentifier.h>
|
||||
#import <AuthenticationServices/ASPasswordCredentialIdentity.h>
|
||||
#import <AuthenticationServices/ASPasskeyCredentialIdentity.h>
|
||||
#import "../../utils.h"
|
||||
#import "../../interop.h"
|
||||
#import "sync.h"
|
||||
|
||||
// 'run' is added to the name because it clashes with internal macOS function
|
||||
void runSync(void* context, NSDictionary *params) {
|
||||
NSArray *credentials = params[@"credentials"];
|
||||
|
||||
// Map credentials to ASPasswordCredential objects
|
||||
NSMutableArray *mappedCredentials = [NSMutableArray arrayWithCapacity:credentials.count];
|
||||
for (NSDictionary *credential in credentials) {
|
||||
NSString *type = credential[@"type"];
|
||||
|
||||
if ([type isEqualToString:@"password"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *uri = credential[@"uri"];
|
||||
NSString *username = credential[@"username"];
|
||||
|
||||
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
|
||||
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
|
||||
ASPasswordCredentialIdentity *credential = [[ASPasswordCredentialIdentity alloc]
|
||||
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
|
||||
ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
}
|
||||
|
||||
[ASCredentialIdentityStore.sharedStore replaceCredentialIdentityEntries:mappedCredentials
|
||||
completion:^(__attribute__((unused)) BOOL success, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
return _return(context, _error_er(error));
|
||||
}
|
||||
|
||||
_return(context, _success(@{@"added": @([mappedCredentials count])}));
|
||||
}];
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
#ifndef RUN_AUTOFILL_COMMAND_H
|
||||
#define RUN_AUTOFILL_COMMAND_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
void runAutofillCommand(void* context, NSDictionary *input);
|
||||
|
||||
#endif
|
@ -0,0 +1,20 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "commands/sync.h"
|
||||
#import "commands/status.h"
|
||||
#import "../interop.h"
|
||||
#import "../utils.h"
|
||||
#import "run_autofill_command.h"
|
||||
|
||||
void runAutofillCommand(void* context, NSDictionary *input) {
|
||||
NSString *command = input[@"command"];
|
||||
NSDictionary *params = input[@"params"];
|
||||
|
||||
if ([command isEqual:@"status"]) {
|
||||
return status(context, params);
|
||||
} else if ([command isEqual:@"sync"]) {
|
||||
return runSync(context, params);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown command: %@", command]));
|
||||
}
|
||||
|
47
apps/desktop/desktop_native/objc/src/native/interop.h
Normal file
47
apps/desktop/desktop_native/objc/src/native/interop.h
Normal file
@ -0,0 +1,47 @@
|
||||
#ifndef INTEROP_H
|
||||
#define INTEROP_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Tips for developing Objective-C code:
|
||||
// - Use the `NSLog` function to log messages to the system log
|
||||
// - Example:
|
||||
// NSLog(@"An example log: %@", someVariable);
|
||||
// - Use the `@try` and `@catch` directives to catch exceptions
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
// Auto Reference Counting makes memory management easier for Objective-C objects
|
||||
// Regular C objects still need to be managed manually
|
||||
#error ARC must be enabled!
|
||||
#endif
|
||||
|
||||
/// [Shared with Rust]
|
||||
/// Simple struct to hold a C-string and its length
|
||||
/// This is used to return strings created in Objective-C to Rust
|
||||
/// so that Rust can free the memory when it's done with the string
|
||||
struct ObjCString
|
||||
{
|
||||
char *value;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
/// [Defined in Rust]
|
||||
/// External function callable from Objective-C to return a string to Rust
|
||||
extern bool commandReturn(void *context, struct ObjCString output);
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Frees the memory allocated for an ObjCString
|
||||
void freeObjCString(struct ObjCString *value);
|
||||
|
||||
// --- Helper functions to convert between Objective-C and Rust types ---
|
||||
|
||||
NSString *_success(NSDictionary *value);
|
||||
NSString *_error(NSString *error);
|
||||
NSString *_error_er(NSError *error);
|
||||
NSString *_error_ex(NSException *error);
|
||||
void _return(void *context, NSString *output);
|
||||
|
||||
struct ObjCString nsStringToObjCString(NSString *string);
|
||||
NSString *cStringToNSString(char *string);
|
||||
|
||||
#endif
|
71
apps/desktop/desktop_native/objc/src/native/interop.m
Normal file
71
apps/desktop/desktop_native/objc/src/native/interop.m
Normal file
@ -0,0 +1,71 @@
|
||||
#import "interop.h"
|
||||
#import "utils.h"
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Frees the memory allocated for an ObjCString
|
||||
void freeObjCString(struct ObjCString *value) {
|
||||
free(value->value);
|
||||
}
|
||||
|
||||
// --- Helper functions to convert between Objective-C and Rust types ---
|
||||
|
||||
NSString *_success(NSDictionary *value) {
|
||||
NSDictionary *wrapper = @{@"type": @"success", @"value": value};
|
||||
NSError *jsonError = nil;
|
||||
NSString *toReturn = serializeJson(wrapper, jsonError);
|
||||
|
||||
if (jsonError) {
|
||||
// Manually format message since there seems to be an issue with the JSON serialization
|
||||
return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError];
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
NSString *_error(NSString *error) {
|
||||
NSDictionary *errorDictionary = @{@"type": @"error", @"error": error};
|
||||
NSError *jsonError = nil;
|
||||
NSString *toReturn = serializeJson(errorDictionary, jsonError);
|
||||
|
||||
if (jsonError) {
|
||||
// Manually format message since there seems to be an issue with the JSON serialization
|
||||
return [NSString stringWithFormat:@"{\"type\": \"error\", \"error\": \"Error occurred while serializing error: %@\"}", jsonError];
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
NSString *_error_er(NSError *error) {
|
||||
return _error([error localizedDescription]);
|
||||
}
|
||||
|
||||
NSString *_error_ex(NSException *error) {
|
||||
return _error([NSString stringWithFormat:@"%@ (%@): %@", error.name, error.reason, [error callStackSymbols]]);
|
||||
}
|
||||
|
||||
void _return(void* context, NSString *output) {
|
||||
if (!commandReturn(context, nsStringToObjCString(output))) {
|
||||
NSLog(@"Error: Failed to return command output");
|
||||
// NOTE: This will most likely crash the application
|
||||
@throw [NSException exceptionWithName:@"CommandReturnError" reason:@"Failed to return command output" userInfo:nil];
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an NSString to an ObjCString struct
|
||||
struct ObjCString nsStringToObjCString(NSString* string) {
|
||||
size_t size = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
|
||||
char *value = malloc(size);
|
||||
[string getCString:value maxLength:size encoding:NSUTF8StringEncoding];
|
||||
|
||||
struct ObjCString objCString;
|
||||
objCString.value = value;
|
||||
objCString.size = size;
|
||||
|
||||
return objCString;
|
||||
}
|
||||
|
||||
/// Converts a C-string to an NSString
|
||||
NSString* cStringToNSString(char* string) {
|
||||
return [[NSString alloc] initWithUTF8String:string];
|
||||
}
|
||||
|
39
apps/desktop/desktop_native/objc/src/native/run_command.m
Normal file
39
apps/desktop/desktop_native/objc/src/native/run_command.m
Normal file
@ -0,0 +1,39 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "autofill/run_autofill_command.h"
|
||||
#import "interop.h"
|
||||
#import "utils.h"
|
||||
|
||||
void pickAndRunCommand(void* context, NSDictionary *input) {
|
||||
NSString *namespace = input[@"namespace"];
|
||||
|
||||
if ([namespace isEqual:@"autofill"]) {
|
||||
return runAutofillCommand(context, input);
|
||||
}
|
||||
|
||||
_return(context, _error([NSString stringWithFormat:@"Unknown namespace: %@", namespace]));
|
||||
}
|
||||
|
||||
/// [Callable from Rust]
|
||||
/// Runs a command with the given input JSON
|
||||
/// This function is called from Rust and is the entry point for running Objective-C code.
|
||||
/// It takes a JSON string as input, deserializes it, runs the command, and serializes the output.
|
||||
/// It also catches any exceptions that occur during the command execution.
|
||||
void runCommand(void *context, char* inputJson) {
|
||||
@autoreleasepool {
|
||||
@try {
|
||||
NSString *inputString = cStringToNSString(inputJson);
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *input = parseJson(inputString, error);
|
||||
if (error) {
|
||||
NSLog(@"Error occured while deserializing input params: %@", error);
|
||||
return _return(context, _error([NSString stringWithFormat:@"Error occured while deserializing input params: %@", error]));
|
||||
}
|
||||
|
||||
pickAndRunCommand(context, input);
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"Error occurred while running Objective-C command: %@", e);
|
||||
_return(context, _error([NSString stringWithFormat:@"Error occurred while running Objective-C command: %@", e]));
|
||||
}
|
||||
}
|
||||
}
|
11
apps/desktop/desktop_native/objc/src/native/utils.h
Normal file
11
apps/desktop/desktop_native/objc/src/native/utils.h
Normal file
@ -0,0 +1,11 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NSDictionary *parseJson(NSString *jsonString, NSError *error);
|
||||
NSString *serializeJson(NSDictionary *dictionary, NSError *error);
|
||||
|
||||
NSData *decodeBase64URL(NSString *base64URLString);
|
||||
|
||||
#endif
|
28
apps/desktop/desktop_native/objc/src/native/utils.m
Normal file
28
apps/desktop/desktop_native/objc/src/native/utils.m
Normal file
@ -0,0 +1,28 @@
|
||||
#import "utils.h"
|
||||
|
||||
NSDictionary *parseJson(NSString *jsonString, NSError *error) {
|
||||
NSData *data = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (error) {
|
||||
return nil;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
NSString *serializeJson(NSDictionary *dictionary, NSError *error) {
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error];
|
||||
if (error) {
|
||||
return nil;
|
||||
}
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
NSData *decodeBase64URL(NSString *base64URLString) {
|
||||
NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
|
||||
NSData *nsdataFromBase64String = [[NSData alloc]
|
||||
initWithBase64EncodedString:base64String options:0];
|
||||
|
||||
return nsdataFromBase64String;
|
||||
}
|
@ -19,17 +19,18 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
If using the credential would require showing custom UI for authenticating the user, cancel
|
||||
the request with error code ASExtensionError.userInteractionRequired.
|
||||
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
let databaseIsUnlocked = true
|
||||
if (databaseIsUnlocked) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
} else {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
|
||||
}
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
override func provideCredentialWithoutUserInteraction(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
// let databaseIsUnlocked = true
|
||||
// if (databaseIsUnlocked) {
|
||||
let passwordCredential = ASPasswordCredential(user: credentialIdentity.user, password: "example1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
// } else {
|
||||
// self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code:ASExtensionError.userInteractionRequired.rawValue))
|
||||
// }
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
|
@ -48,6 +48,7 @@
|
||||
"dist:mac": "npm run build && npm run pack:mac",
|
||||
"dist:mac:mas": "npm run build && npm run pack:mac:mas",
|
||||
"dist:mac:masdev": "npm run build && npm run pack:mac:masdev",
|
||||
"dist:mac:masdev:with-extension": "npm run build && npm run pack:mac:masdev:with-extension",
|
||||
"dist:win": "npm run build && npm run pack:win",
|
||||
"dist:win:ci": "npm run build && npm run pack:win:ci",
|
||||
"publish:lin": "npm run build && npm run clean:dist && electron-builder --linux --x64 -p always",
|
||||
|
@ -28,8 +28,9 @@ async function buildMacOs() {
|
||||
"-alltargets",
|
||||
"-configuration",
|
||||
"Release",
|
||||
"-xcconfig",
|
||||
paths.macOsConfig,
|
||||
// Uncomment when signing is fixed
|
||||
// "-xcconfig",
|
||||
// paths.macOsConfig,
|
||||
]);
|
||||
stdOutProc(proc);
|
||||
await new Promise((resolve, reject) =>
|
||||
|
@ -20,6 +20,7 @@ import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/va
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { SshAgentService } from "../../platform/services/ssh-agent.service";
|
||||
import { VersionService } from "../../platform/services/version.service";
|
||||
@ -45,6 +46,7 @@ export class InitService {
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
@ -82,6 +84,8 @@ export class InitService {
|
||||
|
||||
const containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
await this.autofillService.init();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,7 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
@ -91,6 +92,7 @@ import {
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
@ -301,6 +303,10 @@ const safeProviders: SafeProvider[] = [
|
||||
provide: DesktopAutofillSettingsService,
|
||||
deps: [StateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopAutofillService,
|
||||
deps: [LogService, CipherServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: NativeMessagingManifestService,
|
||||
useClass: NativeMessagingManifestService,
|
||||
|
9
apps/desktop/src/autofill/preload.ts
Normal file
9
apps/desktop/src/autofill/preload.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { Command } from "../platform/main/autofill/command";
|
||||
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
|
||||
|
||||
export default {
|
||||
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
|
||||
ipcRenderer.invoke("autofill.runCommand", params),
|
||||
};
|
121
apps/desktop/src/autofill/services/desktop-autofill.service.ts
Normal file
121
apps/desktop/src/autofill/services/desktop-autofill.service.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { EMPTY, Subject, distinctUntilChanged, mergeMap, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
import { NativeAutofillStatusCommand } from "../../platform/main/autofill/status.command";
|
||||
import {
|
||||
NativeAutofillFido2Credential,
|
||||
NativeAutofillPasswordCredential,
|
||||
NativeAutofillSyncCommand,
|
||||
} from "../../platform/main/autofill/sync.command";
|
||||
|
||||
@Injectable()
|
||||
export class DesktopAutofillService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private cipherService: CipherService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((enabled) => {
|
||||
if (!enabled) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return this.cipherService.cipherViews$;
|
||||
}),
|
||||
// TODO: This will unset all the autofill credentials on the OS
|
||||
// when the account locks. We should instead explicilty clear the credentials
|
||||
// when the user logs out. Maybe by subscribing to the encrypted ciphers observable instead.
|
||||
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
/** Give metadata about all available credentials in the users vault */
|
||||
async sync(cipherViews: CipherView[]) {
|
||||
const status = await this.status();
|
||||
if (status.type === "error") {
|
||||
return this.logService.error("Error getting autofill status", status.error);
|
||||
}
|
||||
|
||||
if (!status.value.state.enabled) {
|
||||
// Autofill is disabled
|
||||
return;
|
||||
}
|
||||
|
||||
let fido2Credentials: NativeAutofillFido2Credential[];
|
||||
let passwordCredentials: NativeAutofillPasswordCredential[];
|
||||
|
||||
if (status.value.support.password) {
|
||||
passwordCredentials = cipherViews
|
||||
.filter(
|
||||
(cipher) =>
|
||||
cipher.type === CipherType.Login &&
|
||||
cipher.login.uris?.length > 0 &&
|
||||
cipher.login.uris.some((uri) => uri.match !== UriMatchStrategy.Never) &&
|
||||
cipher.login.uris.some((uri) => !Utils.isNullOrWhitespace(uri.uri)) &&
|
||||
!Utils.isNullOrWhitespace(cipher.login.username),
|
||||
)
|
||||
.map((cipher) => ({
|
||||
type: "password",
|
||||
cipherId: cipher.id,
|
||||
uri: cipher.login.uris.find((uri) => uri.match !== UriMatchStrategy.Never).uri,
|
||||
username: cipher.login.username,
|
||||
}));
|
||||
}
|
||||
|
||||
if (status.value.support.fido2) {
|
||||
fido2Credentials = (await getCredentialsForAutofill(cipherViews)).map((credential) => ({
|
||||
type: "fido2",
|
||||
...credential,
|
||||
}));
|
||||
}
|
||||
|
||||
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
|
||||
namespace: "autofill",
|
||||
command: "sync",
|
||||
params: {
|
||||
credentials: [...fido2Credentials, ...passwordCredentials],
|
||||
},
|
||||
});
|
||||
|
||||
if (syncResult.type === "error") {
|
||||
return this.logService.error("Error syncing autofill credentials", syncResult.error);
|
||||
}
|
||||
|
||||
this.logService.debug(`Synced ${syncResult.value.added} autofill credentials`);
|
||||
}
|
||||
|
||||
/** Get autofill status from OS */
|
||||
private status() {
|
||||
// TODO: Investigate why this type needs to be explicitly set
|
||||
return ipc.autofill.runCommand<NativeAutofillStatusCommand>({
|
||||
namespace: "autofill",
|
||||
command: "status",
|
||||
params: {},
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@ import { PowerMonitorMain } from "./main/power-monitor.main";
|
||||
import { TrayMain } from "./main/tray.main";
|
||||
import { UpdaterMain } from "./main/updater.main";
|
||||
import { WindowMain } from "./main/window.main";
|
||||
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
|
||||
import { ClipboardMain } from "./platform/main/clipboard.main";
|
||||
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
|
||||
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
|
||||
@ -72,6 +73,7 @@ export class Main {
|
||||
biometricsService: DesktopBiometricsService;
|
||||
nativeMessagingMain: NativeMessagingMain;
|
||||
clipboardMain: ClipboardMain;
|
||||
nativeAutofillMain: NativeAutofillMain;
|
||||
desktopAutofillSettingsService: DesktopAutofillSettingsService;
|
||||
versionMain: VersionMain;
|
||||
sshAgentService: MainSshAgentService;
|
||||
@ -256,6 +258,9 @@ export class Main {
|
||||
|
||||
new EphemeralValueStorageService();
|
||||
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
|
||||
|
||||
this.nativeAutofillMain = new NativeAutofillMain(this.logService);
|
||||
void this.nativeAutofillMain.init();
|
||||
}
|
||||
|
||||
bootstrap() {
|
||||
|
23
apps/desktop/src/platform/main/autofill/command.ts
Normal file
23
apps/desktop/src/platform/main/autofill/command.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NativeAutofillStatusCommand } from "./status.command";
|
||||
import { NativeAutofillSyncCommand } from "./sync.command";
|
||||
|
||||
export type CommandDefinition = {
|
||||
namespace: string;
|
||||
name: string;
|
||||
input: Record<string, unknown>;
|
||||
output: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CommandOutput<SuccessOutput> =
|
||||
| {
|
||||
type: "error";
|
||||
error: string;
|
||||
}
|
||||
| { type: "success"; value: SuccessOutput };
|
||||
|
||||
export type IpcCommandInvoker<C extends CommandDefinition> = (
|
||||
params: C["input"],
|
||||
) => Promise<CommandOutput<C["output"]>>;
|
||||
|
||||
/** A list of all available commands */
|
||||
export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand;
|
@ -0,0 +1,53 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { autofill } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { CommandDefinition } from "./command";
|
||||
|
||||
export type RunCommandParams<C extends CommandDefinition> = {
|
||||
namespace: C["namespace"];
|
||||
command: C["name"];
|
||||
params: C["input"];
|
||||
};
|
||||
|
||||
export type RunCommandResult<C extends CommandDefinition> = C["output"];
|
||||
|
||||
export class NativeAutofillMain {
|
||||
constructor(private logService: LogService) {}
|
||||
|
||||
async init() {
|
||||
ipcMain.handle(
|
||||
"autofill.runCommand",
|
||||
<C extends CommandDefinition>(
|
||||
_event: any,
|
||||
params: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> => {
|
||||
return this.runCommand(params);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async runCommand<C extends CommandDefinition>(
|
||||
command: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> {
|
||||
try {
|
||||
const result = await autofill.runCommand(JSON.stringify(command));
|
||||
const parsed = JSON.parse(result) as RunCommandResult<C>;
|
||||
|
||||
if (parsed.type === "error") {
|
||||
this.logService.error(`Error running autofill command '${command.command}':`, parsed.error);
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
this.logService.error(`Error running autofill command '${command.command}':`, e);
|
||||
|
||||
if (e instanceof Error) {
|
||||
return { type: "error", error: e.stack ?? String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
|
||||
return { type: "error", error: String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
}
|
||||
}
|
20
apps/desktop/src/platform/main/autofill/status.command.ts
Normal file
20
apps/desktop/src/platform/main/autofill/status.command.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { CommandDefinition, CommandOutput } from "./command";
|
||||
|
||||
export interface NativeAutofillStatusCommand extends CommandDefinition {
|
||||
name: "status";
|
||||
input: NativeAutofillStatusParams;
|
||||
output: NativeAutofillStatusResult;
|
||||
}
|
||||
|
||||
export type NativeAutofillStatusParams = Record<string, never>;
|
||||
|
||||
export type NativeAutofillStatusResult = CommandOutput<{
|
||||
support: {
|
||||
fido2: boolean;
|
||||
password: boolean;
|
||||
incrementalUpdates: boolean;
|
||||
};
|
||||
state: {
|
||||
enabled: boolean;
|
||||
};
|
||||
}>;
|
37
apps/desktop/src/platform/main/autofill/sync.command.ts
Normal file
37
apps/desktop/src/platform/main/autofill/sync.command.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { CommandDefinition, CommandOutput } from "./command";
|
||||
|
||||
export interface NativeAutofillSyncCommand extends CommandDefinition {
|
||||
name: "sync";
|
||||
input: NativeAutofillSyncParams;
|
||||
output: NativeAutofillSyncResult;
|
||||
}
|
||||
|
||||
export type NativeAutofillSyncParams = {
|
||||
credentials: NativeAutofillCredential[];
|
||||
};
|
||||
|
||||
export type NativeAutofillCredential =
|
||||
| NativeAutofillFido2Credential
|
||||
| NativeAutofillPasswordCredential;
|
||||
|
||||
export type NativeAutofillFido2Credential = {
|
||||
type: "fido2";
|
||||
cipherId: string;
|
||||
rpId: string;
|
||||
userName: string;
|
||||
/** Should be Base64URL-encoded binary data */
|
||||
credentialId: string;
|
||||
/** Should be Base64URL-encoded binary data */
|
||||
userHandle: string;
|
||||
};
|
||||
|
||||
export type NativeAutofillPasswordCredential = {
|
||||
type: "password";
|
||||
cipherId: string;
|
||||
uri: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type NativeAutofillSyncResult = CommandOutput<{
|
||||
added: number;
|
||||
}>;
|
@ -1,6 +1,7 @@
|
||||
import { contextBridge } from "electron";
|
||||
|
||||
import auth from "./auth/preload";
|
||||
import autofill from "./autofill/preload";
|
||||
import keyManagement from "./key-management/preload";
|
||||
import platform from "./platform/preload";
|
||||
|
||||
@ -17,6 +18,7 @@ import platform from "./platform/preload";
|
||||
// Each team owns a subspace of the `ipc` global variable in the renderer.
|
||||
export const ipc = {
|
||||
auth,
|
||||
autofill,
|
||||
platform,
|
||||
keyManagement,
|
||||
};
|
||||
|
@ -38,6 +38,7 @@ export enum FeatureFlag {
|
||||
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
|
||||
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
|
||||
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
|
||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
|
||||
}
|
||||
|
||||
@ -87,6 +88,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
|
||||
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
|
@ -0,0 +1,26 @@
|
||||
// TODO: Add tests for this method
|
||||
|
||||
import { CipherType } from "../../../vault/enums";
|
||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||
import { Fido2CredentialAutofillView } from "../../../vault/models/view/fido2-credential-autofill.view";
|
||||
|
||||
// TODO: Move into Fido2AuthenticatorService
|
||||
export async function getCredentialsForAutofill(
|
||||
ciphers: CipherView[],
|
||||
): Promise<Fido2CredentialAutofillView[]> {
|
||||
return ciphers
|
||||
.filter(
|
||||
(cipher) =>
|
||||
!cipher.isDeleted && cipher.type === CipherType.Login && cipher.login.hasFido2Credentials,
|
||||
)
|
||||
.map((cipher) => {
|
||||
const credential = cipher.login.fido2Credentials[0];
|
||||
return {
|
||||
cipherId: cipher.id,
|
||||
credentialId: credential.credentialId,
|
||||
rpId: credential.rpId,
|
||||
userHandle: credential.userHandle,
|
||||
userName: credential.userName,
|
||||
};
|
||||
});
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export class Fido2CredentialAutofillView {
|
||||
cipherId: string;
|
||||
credentialId: string;
|
||||
rpId: string;
|
||||
userHandle: string;
|
||||
userName: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user