1
0
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:
Andreas Coroiu 2024-12-06 16:31:30 +01:00 committed by GitHub
parent f95cc7b82c
commit f16bfa4cd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1099 additions and 112 deletions

2
.github/CODEOWNERS vendored
View File

@ -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

View File

@ -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"

View File

@ -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"

View File

@ -0,0 +1,5 @@
use anyhow::Result;
pub async fn run_command(value: String) -> Result<String> {
desktop_objc::run_command(value).await
}

View 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::*;

View File

@ -0,0 +1,5 @@
use anyhow::Result;
pub async fn run_command(value: String) -> Result<String> {
todo!("Unix does not support autofill");
}

View File

@ -0,0 +1,5 @@
use anyhow::Result;
pub async fn run_command(value: String) -> Result<String> {
todo!("Windows does not support autofill");
}

View File

@ -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;

View File

@ -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>
}

View File

@ -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;

View 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"

View 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
}

View 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)
}

View File

@ -0,0 +1,2 @@
CompileFlags:
Add: [-fobjc-arc]

View File

@ -0,0 +1,8 @@
#ifndef STATUS_H
#define STATUS_H
#import <Foundation/Foundation.h>
void status(void *context, NSDictionary *params);
#endif

View File

@ -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),
}
})
);
});
}

View File

@ -0,0 +1,8 @@
#ifndef SYNC_H
#define SYNC_H
#import <Foundation/Foundation.h>
void runSync(void *context, NSDictionary *params);
#endif

View File

@ -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])}));
}];
}

View File

@ -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

View File

@ -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]));
}

View 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

View 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];
}

View 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]));
}
}
}

View 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

View 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;
}

View File

@ -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

View File

@ -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",

View File

@ -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) =>

View File

@ -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();
};
}
}

View File

@ -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,

View 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),
};

View 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();
}
}

View File

@ -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() {

View 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;

View File

@ -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>;
}
}
}

View 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;
};
}>;

View 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;
}>;

View File

@ -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,
};

View File

@ -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>;

View File

@ -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,
};
});
}

View File

@ -0,0 +1,7 @@
export class Fido2CredentialAutofillView {
cipherId: string;
credentialId: string;
rpId: string;
userHandle: string;
userName: string;
}