1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-15 01:11:47 +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 ## ## Desktop native module ##
apps/desktop/desktop_native @bitwarden/team-platform-dev 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 ## ## Key management team files ##
apps/desktop/src/key-management @bitwarden/team-key-management-dev apps/desktop/src/key-management @bitwarden/team-key-management-dev

View File

@ -62,6 +62,12 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.93" version = "1.0.93"
@ -147,9 +153,9 @@ dependencies = [
[[package]] [[package]]
name = "async-io" name = "async-io"
version = "2.3.4" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"cfg-if", "cfg-if",
@ -412,9 +418,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.8.0" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]] [[package]]
name = "cbc" name = "cbc"
@ -427,9 +433,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.1.34" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -474,6 +480,32 @@ dependencies = [
"zeroize", "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]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.0" version = "5.4.0"
@ -517,6 +549,16 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.0" version = "0.10.0"
@ -535,9 +577,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.14" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@ -561,9 +603,9 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.2.8" version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
dependencies = [ dependencies = [
"quote", "quote",
"syn", "syn",
@ -606,25 +648,26 @@ dependencies = [
[[package]] [[package]]
name = "cxx" name = "cxx"
version = "1.0.129" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbdc8cca144dce1c4981b5c9ab748761619979e515c3d53b5df385c677d1d007" checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef"
dependencies = [ dependencies = [
"cc", "cc",
"cxxbridge-cmd",
"cxxbridge-flags", "cxxbridge-flags",
"cxxbridge-macro", "cxxbridge-macro",
"foldhash",
"link-cplusplus", "link-cplusplus",
] ]
[[package]] [[package]]
name = "cxx-build" name = "cxx-build"
version = "1.0.129" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5764c3142ab44fcf857101d12c0ddf09c34499900557c764f5ad0597159d1fc" checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c"
dependencies = [ dependencies = [
"cc", "cc",
"codespan-reporting", "codespan-reporting",
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"scratch", "scratch",
@ -632,19 +675,33 @@ dependencies = [
] ]
[[package]] [[package]]
name = "cxxbridge-flags" name = "cxxbridge-cmd"
version = "1.0.129" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "cxxbridge-macro" name = "cxxbridge-macro"
version = "1.0.129" version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1719100f31492cd6adeeab9a0f46cdbc846e615fdb66d7b398aa46ec7fdd06f" checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion",
"syn", "syn",
] ]
@ -692,7 +749,8 @@ dependencies = [
"bitwarden-russh", "bitwarden-russh",
"byteorder", "byteorder",
"cbc", "cbc",
"core-foundation", "core-foundation 0.10.0",
"desktop_objc",
"dirs", "dirs",
"ed25519", "ed25519",
"futures", "futures",
@ -743,6 +801,18 @@ dependencies = [
"windows-registry", "windows-registry",
] ]
[[package]]
name = "desktop_objc"
version = "0.0.0"
dependencies = [
"anyhow",
"cc",
"core-foundation 0.9.4",
"glob",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "desktop_proxy" name = "desktop_proxy"
version = "0.0.0" version = "0.0.0"
@ -874,12 +944,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.9" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -901,9 +971,9 @@ dependencies = [
[[package]] [[package]]
name = "event-listener-strategy" name = "event-listener-strategy"
version = "0.5.2" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
dependencies = [ dependencies = [
"event-listener", "event-listener",
"pin-project-lite", "pin-project-lite",
@ -911,9 +981,9 @@ dependencies = [
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]] [[package]]
name = "fiat-crypto" name = "fiat-crypto"
@ -933,6 +1003,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.31" version = "0.3.31"
@ -983,9 +1059,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"futures-core", "futures-core",
@ -1083,16 +1159,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "hashbrown" name = "glob"
version = "0.15.1" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]] [[package]]
name = "hermit-abi" name = "hashbrown"
version = "0.3.9" version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -1138,9 +1214,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1173,9 +1249,9 @@ dependencies = [
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]] [[package]]
name = "keytar" name = "keytar"
@ -1215,9 +1291,9 @@ checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.5" version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.52.6", "windows-targets 0.52.6",
@ -1312,11 +1388,10 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.0.2" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [ dependencies = [
"hermit-abi 0.3.9",
"libc", "libc",
"wasi", "wasi",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -1859,13 +1934,13 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "polling" name = "polling"
version = "3.7.3" version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"hermit-abi 0.4.0", "hermit-abi",
"pin-project-lite", "pin-project-lite",
"rustix", "rustix",
"tracing", "tracing",
@ -1921,9 +1996,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.89" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -2016,9 +2091,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.8" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2079,18 +2154,18 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [ dependencies = [
"semver", "semver",
] ]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.37" version = "0.38.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -2099,6 +2174,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "salsa20" name = "salsa20"
version = "0.10.2" version = "0.10.2"
@ -2144,7 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71" checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation 0.10.0",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
@ -2168,18 +2249,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.214" version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.214" version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2272,9 +2353,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.7" version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -2349,6 +2430,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -2357,9 +2444,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.87" version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2368,9 +2455,9 @@ dependencies = [
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.13.0" version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"fastrand", "fastrand",
@ -2410,9 +2497,9 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.36" version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa", "itoa",
@ -2433,9 +2520,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]] [[package]]
name = "time-macros" name = "time-macros"
version = "0.2.18" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [ dependencies = [
"num-conv", "num-conv",
"time-core", "time-core",
@ -2511,9 +2598,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.40" version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
"tracing-attributes", "tracing-attributes",
@ -2522,9 +2609,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.27" version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2533,9 +2620,9 @@ dependencies = [
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.32" version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [ dependencies = [
"once_cell", "once_cell",
] ]
@ -2572,9 +2659,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"

View File

@ -23,7 +23,7 @@ sys = [
aes = "=0.8.4" aes = "=0.8.4"
anyhow = "=1.0.93" anyhow = "=1.0.93"
arboard = { version = "=3.4.1", default-features = false, features = [ arboard = { version = "=3.4.1", default-features = false, features = [
"wayland-data-control", "wayland-data-control",
] } ] }
argon2 = { version = "=0.5.3", features = ["zeroize"] } argon2 = { version = "=0.5.3", features = ["zeroize"] }
async-stream = "=0.3.6" async-stream = "=0.3.6"
@ -44,10 +44,10 @@ scopeguard = "=1.2.0"
sha2 = "=0.10.8" sha2 = "=0.10.8"
ssh-encoding = "=0.2.0" ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false, features = [ ssh-key = { version = "=0.6.7", default-features = false, features = [
"encryption", "encryption",
"ed25519", "ed25519",
"rsa", "rsa",
"getrandom", "getrandom",
] } ] }
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" } bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "b4e7f2fedbe3df8c35545feb000176d3e7b2bc32" }
tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] } 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 } core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=3.0.0", optional = true } security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.12.0", optional = true } security-framework-sys = { version = "=2.12.0", optional = true }
desktop_objc = { path = "../objc" }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
oo7 = "=0.3.3" 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")] #[cfg(feature = "sys")]
pub mod biometric; pub mod biometric;
#[cfg(feature = "sys")] #[cfg(feature = "sys")]
@ -8,9 +9,8 @@ pub mod ipc;
#[cfg(feature = "sys")] #[cfg(feature = "sys")]
pub mod password; pub mod password;
#[cfg(feature = "sys")] #[cfg(feature = "sys")]
pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod powermonitor; pub mod powermonitor;
#[cfg(feature = "sys")] #[cfg(feature = "sys")]
pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod ssh_agent; pub mod ssh_agent;

View File

@ -122,6 +122,9 @@ export declare namespace ipc {
send(message: string): number send(message: string): number
} }
} }
export declare namespace autofill {
export function runCommand(value: string): Promise<string>
}
export declare namespace crypto { export declare namespace crypto {
export function argon2(secret: Buffer, salt: Buffer, iterations: number, memory: number, parallelism: number): Promise<Buffer> 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. /// Fetch the stored password from the keychain.
#[napi] #[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> { 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())) .map_err(|e| napi::Error::from_reason(e.to_string()))
} }
@ -19,21 +20,25 @@ pub mod passwords {
account: String, account: String,
password: String, password: String,
) -> napi::Result<()> { ) -> 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())) .map_err(|e| napi::Error::from_reason(e.to_string()))
} }
/// Delete the stored password from the keychain. /// Delete the stored password from the keychain.
#[napi] #[napi]
pub async fn delete_password(service: String, account: String) -> napi::Result<()> { 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())) .map_err(|e| napi::Error::from_reason(e.to_string()))
} }
// Checks if the os secure storage is available // Checks if the os secure storage is available
#[napi] #[napi]
pub async fn is_available() -> napi::Result<bool> { 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( pub async fn serve(
callback: ThreadsafeFunction<(String, bool), CalleeHandled>, callback: ThreadsafeFunction<(String, bool), CalleeHandled>,
) -> napi::Result<SshAgentState> { ) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) = tokio::sync::mpsc::channel::<(u32, (String, bool))>(32); let (auth_request_tx, mut auth_request_rx) =
let (auth_response_tx, auth_response_rx) = tokio::sync::broadcast::channel::<(u32, bool)>(32); 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)); let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
tokio::spawn(async move { tokio::spawn(async move {
let _ = auth_response_rx; 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_request_id = request_id.clone();
let cloned_cipher_uuid = cipher_uuid.clone(); let cloned_cipher_uuid = cipher_uuid.clone();
let cloned_response_tx_arc = auth_response_tx_arc.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 cipher_uuid = cloned_cipher_uuid;
let auth_response_tx_arc = cloned_response_tx_arc; let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback; let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = let promise_result: Result<Promise<bool>, napi::Error> = callback
callback.call_async(Ok((cipher_uuid, is_list_request))).await; .call_async(Ok((cipher_uuid, is_list_request)))
.await;
match promise_result { match promise_result {
Ok(promise_result) => match promise_result.await { Ok(promise_result) => match promise_result.await {
Ok(result) => { 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"); .expect("should be able to send auth response to agent");
} }
Err(e) => { Err(e) => {
println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", 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"); .expect("should be able to send auth response to agent");
} }
}, },
Err(e) => { Err(e) => {
println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", 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"); .expect("should be able to send auth response to agent");
} }
} }
@ -343,7 +362,9 @@ pub mod sshagent {
#[napi] #[napi]
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state; 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] #[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] #[napi]
pub mod crypto { pub mod crypto {
use napi::bindgen_prelude::Buffer; 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 If using the credential would require showing custom UI for authenticating the user, cancel
the request with error code ASExtensionError.userInteractionRequired. 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 Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's 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": "npm run build && npm run pack:mac",
"dist:mac:mas": "npm run build && npm run pack:mac:mas", "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": "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": "npm run build && npm run pack:win",
"dist:win:ci": "npm run build && npm run pack:win:ci", "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", "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", "-alltargets",
"-configuration", "-configuration",
"Release", "Release",
"-xcconfig", // Uncomment when signing is fixed
paths.macOsConfig, // "-xcconfig",
// paths.macOsConfig,
]); ]);
stdOutProc(proc); stdOutProc(proc);
await new Promise((resolve, reject) => 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 { UserId } from "@bitwarden/common/types/guid";
import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management"; 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 { I18nRendererService } from "../../platform/services/i18n.renderer.service";
import { SshAgentService } from "../../platform/services/ssh-agent.service"; import { SshAgentService } from "../../platform/services/ssh-agent.service";
import { VersionService } from "../../platform/services/version.service"; import { VersionService } from "../../platform/services/version.service";
@ -45,6 +46,7 @@ export class InitService {
private accountService: AccountService, private accountService: AccountService,
private versionService: VersionService, private versionService: VersionService,
private sshAgentService: SshAgentService, private sshAgentService: SshAgentService,
private autofillService: DesktopAutofillService,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
) {} ) {}
@ -82,6 +84,8 @@ export class InitService {
const containerService = new ContainerService(this.keyService, this.encryptService); const containerService = new ContainerService(this.keyService, this.encryptService);
containerService.attachToGlobal(this.win); 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 { ClientType } from "@bitwarden/common/enums";
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-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 { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.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 { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
import { flagEnabled } from "../../platform/flags"; import { flagEnabled } from "../../platform/flags";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@ -301,6 +303,10 @@ const safeProviders: SafeProvider[] = [
provide: DesktopAutofillSettingsService, provide: DesktopAutofillSettingsService,
deps: [StateProvider], deps: [StateProvider],
}), }),
safeProvider({
provide: DesktopAutofillService,
deps: [LogService, CipherServiceAbstraction, ConfigService],
}),
safeProvider({ safeProvider({
provide: NativeMessagingManifestService, provide: NativeMessagingManifestService,
useClass: 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 { TrayMain } from "./main/tray.main";
import { UpdaterMain } from "./main/updater.main"; import { UpdaterMain } from "./main/updater.main";
import { WindowMain } from "./main/window.main"; import { WindowMain } from "./main/window.main";
import { NativeAutofillMain } from "./platform/main/autofill/native-autofill.main";
import { ClipboardMain } from "./platform/main/clipboard.main"; import { ClipboardMain } from "./platform/main/clipboard.main";
import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener";
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
@ -72,6 +73,7 @@ export class Main {
biometricsService: DesktopBiometricsService; biometricsService: DesktopBiometricsService;
nativeMessagingMain: NativeMessagingMain; nativeMessagingMain: NativeMessagingMain;
clipboardMain: ClipboardMain; clipboardMain: ClipboardMain;
nativeAutofillMain: NativeAutofillMain;
desktopAutofillSettingsService: DesktopAutofillSettingsService; desktopAutofillSettingsService: DesktopAutofillSettingsService;
versionMain: VersionMain; versionMain: VersionMain;
sshAgentService: MainSshAgentService; sshAgentService: MainSshAgentService;
@ -256,6 +258,9 @@ export class Main {
new EphemeralValueStorageService(); new EphemeralValueStorageService();
new SSOLocalhostCallbackService(this.environmentService, this.messagingService); new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
this.nativeAutofillMain = new NativeAutofillMain(this.logService);
void this.nativeAutofillMain.init();
} }
bootstrap() { 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 { contextBridge } from "electron";
import auth from "./auth/preload"; import auth from "./auth/preload";
import autofill from "./autofill/preload";
import keyManagement from "./key-management/preload"; import keyManagement from "./key-management/preload";
import platform from "./platform/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. // Each team owns a subspace of the `ipc` global variable in the renderer.
export const ipc = { export const ipc = {
auth, auth,
autofill,
platform, platform,
keyManagement, keyManagement,
}; };

View File

@ -38,6 +38,7 @@ export enum FeatureFlag {
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
MacOsNativeCredentialSync = "macos-native-credential-sync",
PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission",
} }
@ -87,6 +88,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
[FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>; } 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;
}