diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 12a8c18233..959c176c5c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 82dbebb12d..148e56fcb0 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -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" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 6e17cde2fa..4ace1c13e0 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -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" diff --git a/apps/desktop/desktop_native/core/src/autofill/macos.rs b/apps/desktop/desktop_native/core/src/autofill/macos.rs new file mode 100644 index 0000000000..08f333abe9 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/macos.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + desktop_objc::run_command(value).await +} diff --git a/apps/desktop/desktop_native/core/src/autofill/mod.rs b/apps/desktop/desktop_native/core/src/autofill/mod.rs new file mode 100644 index 0000000000..5997add240 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/mod.rs @@ -0,0 +1,5 @@ +#[cfg_attr(target_os = "linux", path = "unix.rs")] +#[cfg_attr(target_os = "windows", path = "windows.rs")] +#[cfg_attr(target_os = "macos", path = "macos.rs")] +mod autofill; +pub use autofill::*; diff --git a/apps/desktop/desktop_native/core/src/autofill/unix.rs b/apps/desktop/desktop_native/core/src/autofill/unix.rs new file mode 100644 index 0000000000..d77130176a --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/unix.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Unix does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/autofill/windows.rs b/apps/desktop/desktop_native/core/src/autofill/windows.rs new file mode 100644 index 0000000000..2e442263c1 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/autofill/windows.rs @@ -0,0 +1,5 @@ +use anyhow::Result; + +pub async fn run_command(value: String) -> Result { + todo!("Windows does not support autofill"); +} diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs index f38e6ef97b..b63c773209 100644 --- a/apps/desktop/desktop_native/core/src/lib.rs +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -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; diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 9ceb30c4ff..0eaba19791 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -122,6 +122,9 @@ export declare namespace ipc { send(message: string): number } } +export declare namespace autofill { + export function runCommand(value: string): Promise +} export declare namespace crypto { export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise } diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index f160b19ad5..5037108afd 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -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 { - 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 { - 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 { - 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, napi::Error> = - callback.call_async(Ok((cipher_uuid, is_list_request))).await; + let promise_result: Result, 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 { + 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; diff --git a/apps/desktop/desktop_native/objc/Cargo.toml b/apps/desktop/desktop_native/objc/Cargo.toml new file mode 100644 index 0000000000..4b883ce6fa --- /dev/null +++ b/apps/desktop/desktop_native/objc/Cargo.toml @@ -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" diff --git a/apps/desktop/desktop_native/objc/build.rs b/apps/desktop/desktop_native/objc/build.rs new file mode 100644 index 0000000000..57f3b626bf --- /dev/null +++ b/apps/desktop/desktop_native/objc/build.rs @@ -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 +} diff --git a/apps/desktop/desktop_native/objc/src/lib.rs b/apps/desktop/desktop_native/objc/src/lib.rs new file mode 100644 index 0000000000..64475b7956 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/lib.rs @@ -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>, +} + +impl CommandContext { + pub fn new() -> (Self, tokio::sync::oneshot::Receiver) { + let (tx, rx) = tokio::sync::oneshot::channel::(); + + (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 for String { + type Error = anyhow::Error; + + fn try_from(value: ObjCString) -> Result { + 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 { + // 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) +} diff --git a/apps/desktop/desktop_native/objc/src/native/.clangd b/apps/desktop/desktop_native/objc/src/native/.clangd new file mode 100644 index 0000000000..5369bebcdc --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-fobjc-arc] diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h new file mode 100644 index 0000000000..e3e3c7969b --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.h @@ -0,0 +1,8 @@ +#ifndef STATUS_H +#define STATUS_H + +#import + +void status(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m new file mode 100644 index 0000000000..8811ffc6f0 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/status.m @@ -0,0 +1,57 @@ +#import +#import +#import +#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), + } + }) + ); + }); +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h new file mode 100644 index 0000000000..4eb39ff24d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.h @@ -0,0 +1,8 @@ +#ifndef SYNC_H +#define SYNC_H + +#import + +void runSync(void *context, NSDictionary *params); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m new file mode 100644 index 0000000000..8b73635a7c --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/commands/sync.m @@ -0,0 +1,59 @@ +#import +#import +#import +#import +#import +#import +#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])})); + }]; +} diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h new file mode 100644 index 0000000000..fba5f62686 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.h @@ -0,0 +1,8 @@ +#ifndef RUN_AUTOFILL_COMMAND_H +#define RUN_AUTOFILL_COMMAND_H + +#import + +void runAutofillCommand(void* context, NSDictionary *input); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m new file mode 100644 index 0000000000..46b188139b --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/autofill/run_autofill_command.m @@ -0,0 +1,20 @@ +#import +#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])); +} + diff --git a/apps/desktop/desktop_native/objc/src/native/interop.h b/apps/desktop/desktop_native/objc/src/native/interop.h new file mode 100644 index 0000000000..584fe547a9 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.h @@ -0,0 +1,47 @@ +#ifndef INTEROP_H +#define INTEROP_H + +#import + +// 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 diff --git a/apps/desktop/desktop_native/objc/src/native/interop.m b/apps/desktop/desktop_native/objc/src/native/interop.m new file mode 100644 index 0000000000..dc41eb52d7 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/interop.m @@ -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]; +} + diff --git a/apps/desktop/desktop_native/objc/src/native/run_command.m b/apps/desktop/desktop_native/objc/src/native/run_command.m new file mode 100644 index 0000000000..eb90a3340d --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/run_command.m @@ -0,0 +1,39 @@ +#import +#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])); + } + } +} diff --git a/apps/desktop/desktop_native/objc/src/native/utils.h b/apps/desktop/desktop_native/objc/src/native/utils.h new file mode 100644 index 0000000000..50fc991d15 --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.h @@ -0,0 +1,11 @@ +#ifndef UTILS_H +#define UTILS_H + +#import + +NSDictionary *parseJson(NSString *jsonString, NSError *error); +NSString *serializeJson(NSDictionary *dictionary, NSError *error); + +NSData *decodeBase64URL(NSString *base64URLString); + +#endif diff --git a/apps/desktop/desktop_native/objc/src/native/utils.m b/apps/desktop/desktop_native/objc/src/native/utils.m new file mode 100644 index 0000000000..040c723a8a --- /dev/null +++ b/apps/desktop/desktop_native/objc/src/native/utils.m @@ -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; +} diff --git a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift index d5c5cabeee..4d03bf97e6 100644 --- a/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift +++ b/apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift @@ -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 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 54feb7df9e..eab9a7d711 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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", diff --git a/apps/desktop/scripts/build-macos-extension.js b/apps/desktop/scripts/build-macos-extension.js index 3aa43fb678..4cb643c34d 100644 --- a/apps/desktop/scripts/build-macos-extension.js +++ b/apps/desktop/scripts/build-macos-extension.js @@ -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) => diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index 0068a03d68..de80f95593 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -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(); }; } } diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 040102d039..8ca24c0040 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -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, diff --git a/apps/desktop/src/autofill/preload.ts b/apps/desktop/src/autofill/preload.ts new file mode 100644 index 0000000000..9ce5e1319f --- /dev/null +++ b/apps/desktop/src/autofill/preload.ts @@ -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: (params: RunCommandParams): Promise> => + ipcRenderer.invoke("autofill.runCommand", params), +}; diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts new file mode 100644 index 0000000000..8c5dd59785 --- /dev/null +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -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(); + + 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({ + 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({ + namespace: "autofill", + command: "status", + params: {}, + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index bbb5c23513..5ae72150ff 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -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() { diff --git a/apps/desktop/src/platform/main/autofill/command.ts b/apps/desktop/src/platform/main/autofill/command.ts new file mode 100644 index 0000000000..a8b5548052 --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/command.ts @@ -0,0 +1,23 @@ +import { NativeAutofillStatusCommand } from "./status.command"; +import { NativeAutofillSyncCommand } from "./sync.command"; + +export type CommandDefinition = { + namespace: string; + name: string; + input: Record; + output: Record; +}; + +export type CommandOutput = + | { + type: "error"; + error: string; + } + | { type: "success"; value: SuccessOutput }; + +export type IpcCommandInvoker = ( + params: C["input"], +) => Promise>; + +/** A list of all available commands */ +export type Command = NativeAutofillSyncCommand | NativeAutofillStatusCommand; diff --git a/apps/desktop/src/platform/main/autofill/native-autofill.main.ts b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts new file mode 100644 index 0000000000..b4b7895e8a --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/native-autofill.main.ts @@ -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 = { + namespace: C["namespace"]; + command: C["name"]; + params: C["input"]; +}; + +export type RunCommandResult = C["output"]; + +export class NativeAutofillMain { + constructor(private logService: LogService) {} + + async init() { + ipcMain.handle( + "autofill.runCommand", + ( + _event: any, + params: RunCommandParams, + ): Promise> => { + return this.runCommand(params); + }, + ); + } + + private async runCommand( + command: RunCommandParams, + ): Promise> { + try { + const result = await autofill.runCommand(JSON.stringify(command)); + const parsed = JSON.parse(result) as RunCommandResult; + + 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; + } + + return { type: "error", error: String(e) } as RunCommandResult; + } + } +} diff --git a/apps/desktop/src/platform/main/autofill/status.command.ts b/apps/desktop/src/platform/main/autofill/status.command.ts new file mode 100644 index 0000000000..b6c0943fa6 --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/status.command.ts @@ -0,0 +1,20 @@ +import { CommandDefinition, CommandOutput } from "./command"; + +export interface NativeAutofillStatusCommand extends CommandDefinition { + name: "status"; + input: NativeAutofillStatusParams; + output: NativeAutofillStatusResult; +} + +export type NativeAutofillStatusParams = Record; + +export type NativeAutofillStatusResult = CommandOutput<{ + support: { + fido2: boolean; + password: boolean; + incrementalUpdates: boolean; + }; + state: { + enabled: boolean; + }; +}>; diff --git a/apps/desktop/src/platform/main/autofill/sync.command.ts b/apps/desktop/src/platform/main/autofill/sync.command.ts new file mode 100644 index 0000000000..dc3b12383e --- /dev/null +++ b/apps/desktop/src/platform/main/autofill/sync.command.ts @@ -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; +}>; diff --git a/apps/desktop/src/preload.ts b/apps/desktop/src/preload.ts index 57b20490a4..0fb2db3751 100644 --- a/apps/desktop/src/preload.ts +++ b/apps/desktop/src/preload.ts @@ -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, }; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 66d6b155e9..f79ebf8aa5 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -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; diff --git a/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts new file mode 100644 index 0000000000..7ef705b95f --- /dev/null +++ b/libs/common/src/platform/services/fido2/fido2-autofill-utils.ts @@ -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 { + 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, + }; + }); +} diff --git a/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts new file mode 100644 index 0000000000..b120f27b77 --- /dev/null +++ b/libs/common/src/vault/models/view/fido2-credential-autofill.view.ts @@ -0,0 +1,7 @@ +export class Fido2CredentialAutofillView { + cipherId: string; + credentialId: string; + rpId: string; + userHandle: string; + userName: string; +}