mirror of
https://github.com/bitwarden/browser
synced 2026-02-21 11:54:02 +00:00
Merge branch 'main' into fix-15485
This commit is contained in:
406
apps/desktop/desktop_native/Cargo.lock
generated
406
apps/desktop/desktop_native/Cargo.lock
generated
@@ -2,21 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.24.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
@@ -114,9 +99,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.94"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
@@ -138,14 +123,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||
checksum = "da0986d5b4f0802160191ad75f8d33ada000558757db3defb70299ca95d9fcbd"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
@@ -347,23 +332,8 @@ dependencies = [
|
||||
"mockall",
|
||||
"serial_test",
|
||||
"tracing",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-targets 0.52.6",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -457,7 +427,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -501,6 +471,12 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -509,9 +485,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
@@ -556,9 +532,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.46"
|
||||
version = "1.2.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
|
||||
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -614,7 +590,7 @@ dependencies = [
|
||||
"hex",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
@@ -623,7 +599,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"verifysign",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -709,9 +685,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
@@ -770,16 +746,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.5.0"
|
||||
@@ -867,7 +833,7 @@ dependencies = [
|
||||
"memsec",
|
||||
"oo7",
|
||||
"pin-project",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"scopeguard",
|
||||
"secmem-proc",
|
||||
"security-framework",
|
||||
@@ -877,13 +843,13 @@ dependencies = [
|
||||
"sha2",
|
||||
"ssh-key",
|
||||
"sysinfo",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"typenum",
|
||||
"widestring",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
"windows-future",
|
||||
"zbus",
|
||||
"zbus_polkit",
|
||||
@@ -1409,17 +1375,11 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
@@ -1499,14 +1459,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "homedir"
|
||||
version = "0.3.4"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2"
|
||||
checksum = "68df315d2857b2d8d2898be54a85e1d001bbbe0dbb5f8ef847b48dd3a23c4527"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"nix 0.29.0",
|
||||
"nix",
|
||||
"widestring",
|
||||
"windows 0.57.0",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1663,6 +1623,16 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@@ -1674,9 +1644,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
version = "0.2.178"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -1685,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.53.3",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1841,15 +1811,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
@@ -1889,32 +1850,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "2.16.17"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3"
|
||||
checksum = "f1b74e3dce5230795bb4d2821b941706dee733c7308752507254b0497f39cad7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor 0.2.9",
|
||||
"napi-derive",
|
||||
"ctor",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"once_cell",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.2.0"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4"
|
||||
checksum = "dcae8ad5609d14afb3a3b91dee88c757016261b151e9dcecabf1b2a31a6cab14"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "2.16.13"
|
||||
version = "3.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c"
|
||||
checksum = "7552d5a579b834614bbd496db5109f1b9f1c758f08224b0dee1e408333adf0d0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1923,40 +1885,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "1.0.75"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf"
|
||||
checksum = "5f6a81ac7486b70f2532a289603340862c06eea5a1e650c1ffeda2ce1238516a"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "2.4.0"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3"
|
||||
checksum = "3e4e7135a8f97aa0f1509cce21a8a1f9dcec1b50d8dee006b48a5adb69a9d64d"
|
||||
dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -1970,6 +1918,12 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -2173,15 +2127,6 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -2190,9 +2135,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "oo7"
|
||||
version = "0.4.3"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb23d3ec3527d65a83be1c1795cb883c52cfa57147d42acc797127df56fc489"
|
||||
checksum = "e3299dd401feaf1d45afd8fd1c0586f10fcfb22f244bb9afa942cec73503b89d"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"ashpd",
|
||||
@@ -2208,7 +2153,7 @@ dependencies = [
|
||||
"num",
|
||||
"num-bigint-dig",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rand 0.9.2",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
@@ -2548,7 +2493,7 @@ dependencies = [
|
||||
name = "process_isolation"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ctor 0.5.0",
|
||||
"ctor",
|
||||
"desktop_core",
|
||||
"libc",
|
||||
"tracing",
|
||||
@@ -2591,9 +2536,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_core 0.9.3",
|
||||
@@ -2660,19 +2605,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2748,10 +2681,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.24"
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
@@ -2798,6 +2731,12 @@ dependencies = [
|
||||
"rustix 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
@@ -2870,15 +2809,15 @@ dependencies = [
|
||||
"libc",
|
||||
"rustix 1.0.7",
|
||||
"rustix-linux-procfs",
|
||||
"thiserror 2.0.12",
|
||||
"windows 0.61.1",
|
||||
"thiserror 2.0.17",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.5.0"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a"
|
||||
checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
@@ -3068,12 +3007,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.9"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
|
||||
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3188,16 +3127,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.35.0"
|
||||
version = "0.37.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b897c8ea620e181c7955369a31be5f48d9a9121cb59fd33ecef9ff2a34323422"
|
||||
checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.61.1",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3239,11 +3178,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3259,9 +3198,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3289,11 +3228,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.45.0"
|
||||
version = "1.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
|
||||
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
@@ -3303,14 +3241,14 @@ dependencies = [
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3319,9 +3257,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.13"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
|
||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
@@ -3680,6 +3618,17 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -3745,6 +3694,51 @@ dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.10"
|
||||
@@ -3852,16 +3846,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.1"
|
||||
@@ -3869,7 +3853,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-link 0.1.3",
|
||||
"windows-numerics",
|
||||
@@ -3881,19 +3865,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3902,8 +3874,8 @@ version = "0.61.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
@@ -3915,21 +3887,10 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
@@ -3941,17 +3902,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
@@ -3981,7 +3931,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.0",
|
||||
"windows-core",
|
||||
"windows-link 0.1.3",
|
||||
]
|
||||
|
||||
@@ -3996,15 +3946,6 @@ dependencies = [
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -4262,8 +4203,8 @@ name = "windows_plugin_authenticator"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4434,9 +4375,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.11.0"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7"
|
||||
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
@@ -4452,14 +4393,15 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix 0.30.1",
|
||||
"nix",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.60.2",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
@@ -4468,9 +4410,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.11.0"
|
||||
version = "5.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca"
|
||||
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
|
||||
@@ -21,13 +21,13 @@ publish = false
|
||||
[workspace.dependencies]
|
||||
aes = "=0.8.4"
|
||||
aes-gcm = "=0.10.3"
|
||||
anyhow = "=1.0.94"
|
||||
anyhow = "=1.0.100"
|
||||
arboard = { version = "=3.6.1", default-features = false }
|
||||
ashpd = "=0.11.0"
|
||||
ashpd = "=0.12.0"
|
||||
base64 = "=0.22.1"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
|
||||
byteorder = "=1.5.0"
|
||||
bytes = "=1.10.1"
|
||||
bytes = "=1.11.0"
|
||||
cbc = "=0.1.2"
|
||||
chacha20poly1305 = "=0.10.1"
|
||||
core-foundation = "=0.10.1"
|
||||
@@ -37,33 +37,33 @@ ed25519 = "=2.2.3"
|
||||
embed_plist = "=1.2.2"
|
||||
futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.4"
|
||||
homedir = "=0.3.6"
|
||||
interprocess = "=2.2.1"
|
||||
libc = "=0.2.177"
|
||||
libc = "=0.2.178"
|
||||
linux-keyutils = "=0.2.4"
|
||||
memsec = "=0.7.0"
|
||||
napi = "=2.16.17"
|
||||
napi-build = "=2.2.0"
|
||||
napi-derive = "=2.16.13"
|
||||
oo7 = "=0.4.3"
|
||||
napi = "=3.3.0"
|
||||
napi-build = "=2.2.3"
|
||||
napi-derive = "=3.2.5"
|
||||
oo7 = "=0.5.0"
|
||||
pin-project = "=1.1.10"
|
||||
pkcs8 = "=0.10.2"
|
||||
rand = "=0.9.1"
|
||||
rand = "=0.9.2"
|
||||
rsa = "=0.9.6"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
secmem-proc = "=0.3.7"
|
||||
security-framework = "=3.5.0"
|
||||
security-framework = "=3.5.1"
|
||||
security-framework-sys = "=2.15.0"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false }
|
||||
sysinfo = "=0.35.0"
|
||||
thiserror = "=2.0.12"
|
||||
tokio = "=1.45.0"
|
||||
tokio-util = "=0.7.13"
|
||||
sysinfo = "=0.37.2"
|
||||
thiserror = "=2.0.17"
|
||||
tokio = "=1.48.0"
|
||||
tokio-util = "=0.7.17"
|
||||
tracing = "=0.1.41"
|
||||
tracing-subscriber = { version = "=0.3.20", features = [
|
||||
"fmt",
|
||||
@@ -77,7 +77,7 @@ windows = "=0.61.1"
|
||||
windows-core = "=0.61.0"
|
||||
windows-future = "=0.2.0"
|
||||
windows-registry = "=0.6.1"
|
||||
zbus = "=5.11.0"
|
||||
zbus = "=5.12.0"
|
||||
zbus_polkit = "=5.0.0"
|
||||
zeroizing-alloc = "=0.1.0"
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const rustTargetsMap = {
|
||||
"aarch64-pc-windows-msvc": { nodeArch: 'arm64', platform: 'win32' },
|
||||
"x86_64-apple-darwin": { nodeArch: 'x64', platform: 'darwin' },
|
||||
"aarch64-apple-darwin": { nodeArch: 'arm64', platform: 'darwin' },
|
||||
'x86_64-unknown-linux-musl': { nodeArch: 'x64', platform: 'linux' },
|
||||
'aarch64-unknown-linux-musl': { nodeArch: 'arm64', platform: 'linux' },
|
||||
'x86_64-unknown-linux-gnu': { nodeArch: 'x64', platform: 'linux' },
|
||||
'aarch64-unknown-linux-gnu': { nodeArch: 'arm64', platform: 'linux' },
|
||||
}
|
||||
|
||||
// Ensure the dist directory exists
|
||||
@@ -113,8 +113,8 @@ if (process.platform === "linux") {
|
||||
|
||||
platformTargets.forEach(([target, _]) => {
|
||||
installTarget(target);
|
||||
buildNapiModule(target);
|
||||
buildProxyBin(target);
|
||||
buildImporterBinaries(target);
|
||||
buildNapiModule(target, mode === "release");
|
||||
buildProxyBin(target, mode === "release");
|
||||
buildImporterBinaries(target, mode === "release");
|
||||
buildProcessIsolation();
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::{
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: &[".config/google-chrome"],
|
||||
data_dir: &[".config/google-chrome", "snap/chromium/common/chromium"],
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
|
||||
@@ -7,9 +7,9 @@ pub struct NativeImporterMetadata {
|
||||
/// Identifies the importer
|
||||
pub id: String,
|
||||
/// Describes the strategies used to obtain imported data
|
||||
pub loaders: Vec<&'static str>,
|
||||
pub loaders: Vec<String>,
|
||||
/// Identifies the instructions for the importer
|
||||
pub instructions: &'static str,
|
||||
pub instructions: String,
|
||||
}
|
||||
|
||||
/// Returns a map of supported importers based on the current platform.
|
||||
@@ -36,9 +36,9 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect();
|
||||
|
||||
for (id, browser_name) in IMPORTERS {
|
||||
let mut loaders: Vec<&'static str> = vec!["file"];
|
||||
let mut loaders: Vec<String> = vec!["file".to_string()];
|
||||
if supported.contains(browser_name) {
|
||||
loaders.push("chromium");
|
||||
loaders.push("chromium".to_string());
|
||||
}
|
||||
|
||||
if installed_browsers.contains(&browser_name.to_string()) {
|
||||
@@ -47,7 +47,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
NativeImporterMetadata {
|
||||
id: id.to_string(),
|
||||
loaders,
|
||||
instructions: "chromium",
|
||||
instructions: "chromium".to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -79,12 +79,9 @@ mod tests {
|
||||
map.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn get_loaders(
|
||||
map: &HashMap<String, NativeImporterMetadata>,
|
||||
id: &str,
|
||||
) -> HashSet<&'static str> {
|
||||
fn get_loaders(map: &HashMap<String, NativeImporterMetadata>, id: &str) -> HashSet<String> {
|
||||
map.get(id)
|
||||
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>())
|
||||
.map(|m| m.loaders.iter().cloned().collect::<HashSet<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -107,7 +104,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +144,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +180,7 @@ mod tests {
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.contains(&"file"));
|
||||
assert!(meta.loaders.contains(&"file".to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,8 +285,8 @@ async fn windows_hello_authenticate_with_crypto(
|
||||
return Err(anyhow!("Failed to sign data"));
|
||||
}
|
||||
|
||||
let signature_buffer = signature.Result()?;
|
||||
let signature_value = unsafe { as_mut_bytes(&signature_buffer)? };
|
||||
let mut signature_buffer = signature.Result()?;
|
||||
let signature_value = unsafe { as_mut_bytes(&mut signature_buffer)? };
|
||||
|
||||
// The signature is deterministic based on the challenge and keychain key. Thus, it can be
|
||||
// hashed to a key. It is unclear what entropy this key provides.
|
||||
@@ -368,7 +368,7 @@ fn decrypt_data(
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
unsafe fn as_mut_bytes(buffer: &IBuffer) -> Result<&mut [u8]> {
|
||||
unsafe fn as_mut_bytes(buffer: &mut IBuffer) -> Result<&mut [u8]> {
|
||||
let interop = buffer.cast::<IBufferByteAccess>()?;
|
||||
|
||||
unsafe {
|
||||
|
||||
@@ -226,7 +226,7 @@ impl BitwardenDesktopAgent {
|
||||
keystore.0.write().expect("RwLock is not poisoned").clear();
|
||||
|
||||
self.needs_unlock
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
for (key, name, cipher_id) in new_keys.iter() {
|
||||
match parse_key_safe(key) {
|
||||
@@ -307,3 +307,87 @@ fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Err
|
||||
Err(e) => Err(anyhow::Error::msg(format!("Failed to parse key: {e}"))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_agent() -> (
|
||||
BitwardenDesktopAgent,
|
||||
tokio::sync::mpsc::Receiver<SshAgentUIRequest>,
|
||||
tokio::sync::broadcast::Sender<(u32, bool)>,
|
||||
) {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(10);
|
||||
let (response_tx, response_rx) = tokio::sync::broadcast::channel(10);
|
||||
let agent = BitwardenDesktopAgent::new(tx, Arc::new(Mutex::new(response_rx)));
|
||||
(agent, rx, response_tx)
|
||||
}
|
||||
|
||||
const TEST_ED25519_KEY: &str = "-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trwAAAIhqmvSbapr0
|
||||
mwAAAAtzc2gtZWQyNTUxOQAAACCWETEIh/JX+ZaK0Xlg5xZ9QIfjiKD2Qs57PjhRY45trw
|
||||
AAAEAHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVfBWT4eZYRMQiH8lf5lorReWDnFn1A
|
||||
h+OIoPZCzns+OFFjjm2vAAAAAAECAwQF
|
||||
-----END OPENSSH PRIVATE KEY-----";
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_initial_state() {
|
||||
let (agent, _rx, _response_tx) = create_test_agent();
|
||||
|
||||
// Initially, needs_unlock should be true
|
||||
assert!(agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_after_set_keys() {
|
||||
let (mut agent, _rx, _response_tx) = create_test_agent();
|
||||
agent
|
||||
.is_running
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Set keys should set needs_unlock to false
|
||||
let keys = vec![(
|
||||
TEST_ED25519_KEY.to_string(),
|
||||
"test_key".to_string(),
|
||||
"cipher_id".to_string(),
|
||||
)];
|
||||
|
||||
agent.set_keys(keys).unwrap();
|
||||
|
||||
assert!(!agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_needs_unlock_after_clear_keys() {
|
||||
let (mut agent, _rx, _response_tx) = create_test_agent();
|
||||
agent
|
||||
.is_running
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
// Set keys first
|
||||
let keys = vec![(
|
||||
TEST_ED25519_KEY.to_string(),
|
||||
"test_key".to_string(),
|
||||
"cipher_id".to_string(),
|
||||
)];
|
||||
agent.set_keys(keys).unwrap();
|
||||
|
||||
// Verify needs_unlock is false
|
||||
assert!(!agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
|
||||
// Clear keys should set needs_unlock back to true
|
||||
agent.clear_keys().unwrap();
|
||||
|
||||
// Verify needs_unlock is true
|
||||
assert!(agent
|
||||
.needs_unlock
|
||||
.load(std::sync::atomic::Ordering::Relaxed));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ use sysinfo::{Pid, System};
|
||||
use super::models::PeerInfo;
|
||||
|
||||
pub fn get_peer_info(peer_pid: u32) -> Result<PeerInfo, String> {
|
||||
let s = System::new_all();
|
||||
if let Some(process) = s.process(Pid::from_u32(peer_pid)) {
|
||||
let mut system = System::new();
|
||||
system.refresh_processes(
|
||||
sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(peer_pid)]),
|
||||
true,
|
||||
);
|
||||
if let Some(process) = system.process(Pid::from_u32(peer_pid)) {
|
||||
let peer_process_name = match process.name().to_str() {
|
||||
Some(name) => name.to_string(),
|
||||
None => {
|
||||
|
||||
@@ -24,7 +24,7 @@ serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-oslog = "0.3.0"
|
||||
tracing-oslog = "=0.3.0"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
35
apps/desktop/desktop_native/macos_provider/README.md
Normal file
35
apps/desktop/desktop_native/macos_provider/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Explainer: Mac OS Native Passkey Provider
|
||||
|
||||
This document describes the changes introduced in https://github.com/bitwarden/clients/pull/13963, where we introduce the MacOS Native Passkey Provider. It gives the high level explanation of the architecture and some of the quirks and additional good to know context.
|
||||
|
||||
## The high level
|
||||
MacOS has native APIs (similar to iOS) to allow Credential Managers to provide credentials to the MacOS autofill system (in the PR referenced above, we only provide passkeys).
|
||||
|
||||
We’ve written a Swift-based native autofill-extension. It’s bundled in the app-bundle in PlugIns, similar to the safari-extension.
|
||||
|
||||
This swift extension currently communicates with our Electron app through IPC based on a unix socket. The IPC implementation is done in Rust and utilized through UniFFI + NAPI bindings.
|
||||
|
||||
Footnotes:
|
||||
|
||||
* We're not using the IPC framework as the implementation pre-dates the IPC framework.
|
||||
* Alternatives like XPC or CFMessagePort may have better support for when the app is sandboxed.
|
||||
|
||||
Electron receives the messages and passes it to Angular (through the electron-renderer event system).
|
||||
|
||||
Our existing fido2 services in the renderer respond to events, displaying UI as necessary, and returns the signature back through the same mechanism, allowing people to authenticate with passkeys through the native system + UI. See [Mac OS Native Passkey Workflows](https://bitwarden.atlassian.net/wiki/spaces/EN/pages/1828356098/Mac+OS+Native+Passkey+Workflows) for demo videos.
|
||||
|
||||
## Typescript + UI implementations
|
||||
|
||||
We utilize the same FIDO2 implementation and interface that is already present for our browser authentication. It was designed by @coroiu with multiple ‘ui environments' in mind.
|
||||
|
||||
Therefore, a lot of the plumbing is implemented in /autofill/services/desktop-fido2-user-interface.service.ts, which implements the interface that our fido2 authenticator/client expects to drive UI related behaviors.
|
||||
|
||||
We’ve also implemented a couple FIDO2 UI components to handle registration/sign in flows, but also improved the “modal mode” of the desktop app.
|
||||
|
||||
## Modal mode
|
||||
|
||||
When modal mode is activated, the desktop app turns into a smaller modal that is always on top and cannot be resized. This is done to improve the UX of performing a passkey operation (or SSH operation). Once the operation is completed, the app returns to normal mode and its previous position.
|
||||
|
||||
We are not using electron modal windows, for a couple reason. It would require us to send data in yet another layer of IPC, but also because we'd need to bootstrap entire renderer/app instead of reusing the existing window.
|
||||
|
||||
Some modal modes may hide the 'traffic buttons' (window controls) due to design requirements.
|
||||
@@ -8,6 +8,9 @@ rm -r tmp
|
||||
mkdir -p ./tmp/target/universal-darwin/release/
|
||||
|
||||
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
|
||||
cargo build --package macos_provider --target aarch64-apple-darwin --release
|
||||
cargo build --package macos_provider --target x86_64-apple-darwin --release
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@ trait Callback: Send + Sync {
|
||||
fn error(&self, error: BitwardenError);
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum, Debug)]
|
||||
/// Store the connection status between the macOS credential provider extension
|
||||
/// and the desktop application's IPC server.
|
||||
pub enum ConnectionStatus {
|
||||
Connected,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct MacOSProviderClient {
|
||||
to_server_send: tokio::sync::mpsc::Sender<String>,
|
||||
@@ -65,8 +73,24 @@ pub struct MacOSProviderClient {
|
||||
response_callbacks_counter: AtomicU32,
|
||||
#[allow(clippy::type_complexity)]
|
||||
response_callbacks_queue: Arc<Mutex<HashMap<u32, (Box<dyn Callback>, Instant)>>>,
|
||||
|
||||
// Flag to track connection status - atomic for thread safety without locks
|
||||
connection_status: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/// Store native desktop status information to use for IPC communication
|
||||
/// between the application and the macOS credential provider.
|
||||
pub struct NativeStatus {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
// In our callback management, 0 is a reserved sequence number indicating that a message does not
|
||||
// have a callback.
|
||||
const NO_CALLBACK_INDICATOR: u32 = 0;
|
||||
|
||||
#[uniffi::export]
|
||||
impl MacOSProviderClient {
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
@@ -93,13 +117,16 @@ impl MacOSProviderClient {
|
||||
|
||||
let client = MacOSProviderClient {
|
||||
to_server_send,
|
||||
response_callbacks_counter: AtomicU32::new(0),
|
||||
response_callbacks_counter: AtomicU32::new(1), /* Start at 1 since 0 is reserved for
|
||||
* "no callback" scenarios */
|
||||
response_callbacks_queue: Arc::new(Mutex::new(HashMap::new())),
|
||||
connection_status: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
let path = desktop_core::ipc::path("af");
|
||||
|
||||
let queue = client.response_callbacks_queue.clone();
|
||||
let connection_status = client.connection_status.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
@@ -117,9 +144,11 @@ impl MacOSProviderClient {
|
||||
match serde_json::from_str::<SerializedMessage>(&message) {
|
||||
Ok(SerializedMessage::Command(CommandMessage::Connected)) => {
|
||||
info!("Connected to server");
|
||||
connection_status.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
Ok(SerializedMessage::Command(CommandMessage::Disconnected)) => {
|
||||
info!("Disconnected from server");
|
||||
connection_status.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
Ok(SerializedMessage::Message {
|
||||
sequence_number,
|
||||
@@ -157,12 +186,17 @@ impl MacOSProviderClient {
|
||||
client
|
||||
}
|
||||
|
||||
pub fn send_native_status(&self, key: String, value: String) {
|
||||
let status = NativeStatus { key, value };
|
||||
self.send_message(status, None);
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_registration(
|
||||
&self,
|
||||
request: PasskeyRegistrationRequest,
|
||||
callback: Arc<dyn PreparePasskeyRegistrationCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion(
|
||||
@@ -170,7 +204,7 @@ impl MacOSProviderClient {
|
||||
request: PasskeyAssertionRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
pub fn prepare_passkey_assertion_without_user_interface(
|
||||
@@ -178,7 +212,18 @@ impl MacOSProviderClient {
|
||||
request: PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
callback: Arc<dyn PreparePasskeyAssertionCallback>,
|
||||
) {
|
||||
self.send_message(request, Box::new(callback));
|
||||
self.send_message(request, Some(Box::new(callback)));
|
||||
}
|
||||
|
||||
pub fn get_connection_status(&self) -> ConnectionStatus {
|
||||
let is_connected = self
|
||||
.connection_status
|
||||
.load(std::sync::atomic::Ordering::Relaxed);
|
||||
if is_connected {
|
||||
ConnectionStatus::Connected
|
||||
} else {
|
||||
ConnectionStatus::Disconnected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +245,6 @@ enum SerializedMessage {
|
||||
}
|
||||
|
||||
impl MacOSProviderClient {
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn add_callback(&self, callback: Box<dyn Callback>) -> u32 {
|
||||
let sequence_number = self
|
||||
@@ -209,20 +253,23 @@ impl MacOSProviderClient {
|
||||
|
||||
self.response_callbacks_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.expect("response callbacks queue mutex should not be poisoned")
|
||||
.insert(sequence_number, (callback, Instant::now()));
|
||||
|
||||
sequence_number
|
||||
}
|
||||
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn send_message(
|
||||
&self,
|
||||
message: impl Serialize + DeserializeOwned,
|
||||
callback: Box<dyn Callback>,
|
||||
callback: Option<Box<dyn Callback>>,
|
||||
) {
|
||||
let sequence_number = self.add_callback(callback);
|
||||
let sequence_number = if let Some(callback) = callback {
|
||||
self.add_callback(callback)
|
||||
} else {
|
||||
NO_CALLBACK_INDICATOR
|
||||
};
|
||||
|
||||
let message = serde_json::to_string(&SerializedMessage::Message {
|
||||
sequence_number,
|
||||
@@ -232,15 +279,17 @@ impl MacOSProviderClient {
|
||||
|
||||
if let Err(e) = self.to_server_send.blocking_send(message) {
|
||||
// Make sure we remove the callback from the queue if we can't send the message
|
||||
if let Some((cb, _)) = self
|
||||
.response_callbacks_queue
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&sequence_number)
|
||||
{
|
||||
cb.error(BitwardenError::Internal(format!(
|
||||
"Error sending message: {e}"
|
||||
)));
|
||||
if sequence_number != NO_CALLBACK_INDICATOR {
|
||||
if let Some((callback, _)) = self
|
||||
.response_callbacks_queue
|
||||
.lock()
|
||||
.expect("response callbacks queue mutex should not be poisoned")
|
||||
.remove(&sequence_number)
|
||||
{
|
||||
callback.error(BitwardenError::Internal(format!(
|
||||
"Error sending message: {e}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub struct PasskeyRegistrationRequest {
|
||||
user_verification: UserVerification,
|
||||
supported_algorithms: Vec<i32>,
|
||||
window_xy: Position,
|
||||
excluded_credentials: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record, Serialize, Deserialize)]
|
||||
|
||||
418
apps/desktop/desktop_native/napi/index.d.ts
vendored
418
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -1,125 +1,7 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
/**
|
||||
* Fetch the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function getPassword(service: string, account: string): Promise<string>
|
||||
/**
|
||||
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
|
||||
* existing entry.
|
||||
*/
|
||||
export function setPassword(service: string, account: string, password: string): Promise<void>
|
||||
/**
|
||||
* Delete the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
/** Checks if the os secure storage is available */
|
||||
export function isAvailable(): Promise<boolean>
|
||||
}
|
||||
export declare namespace biometrics {
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function available(): Promise<boolean>
|
||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
||||
/**
|
||||
* Retrieves the biometric secret for the given service and account.
|
||||
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
|
||||
*/
|
||||
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
|
||||
/**
|
||||
* Derives key material from biometric data. Returns a string encoded with a
|
||||
* base64 encoded key and the base64 encoded challenge used to create it
|
||||
* separated by a `|` character.
|
||||
*
|
||||
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
|
||||
* be generated.
|
||||
*
|
||||
* `format!("<key_base64>|<iv_base64>")`
|
||||
*/
|
||||
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
|
||||
export interface KeyMaterial {
|
||||
osKeyPartB64: string
|
||||
clientKeyPartB64?: string
|
||||
}
|
||||
export interface OsDerivedKey {
|
||||
keyB64: string
|
||||
ivB64: string
|
||||
}
|
||||
}
|
||||
export declare namespace biometrics_v2 {
|
||||
export function initBiometricSystem(): BiometricLockSystem
|
||||
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
|
||||
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
|
||||
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
|
||||
export class BiometricLockSystem { }
|
||||
}
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
export declare namespace sshagent {
|
||||
export interface PrivateKey {
|
||||
privateKey: string
|
||||
name: string
|
||||
cipherId: string
|
||||
}
|
||||
export interface SshKey {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
|
||||
export function stop(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export class SshAgentState { }
|
||||
}
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function isolateProcess(): Promise<void>
|
||||
}
|
||||
export declare namespace powermonitors {
|
||||
export function onLock(callback: (err: Error | null, ) => any): Promise<void>
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
}
|
||||
export declare namespace windows_registry {
|
||||
export function createKey(key: string, subkey: string, value: string): Promise<void>
|
||||
export function deleteKey(key: string, subkey: string): Promise<void>
|
||||
}
|
||||
export declare namespace ipc {
|
||||
export interface IpcMessage {
|
||||
clientId: number
|
||||
kind: IpcMessageType
|
||||
message?: string
|
||||
}
|
||||
export const enum IpcMessageType {
|
||||
Connected = 0,
|
||||
Disconnected = 1,
|
||||
Message = 2
|
||||
}
|
||||
export class IpcServer {
|
||||
/* eslint-disable */
|
||||
export declare namespace autofill {
|
||||
export class AutofillIpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
@@ -127,49 +9,18 @@ export declare namespace ipc {
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void, nativeStatusCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void): Promise<AutofillIpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
/**
|
||||
* Send a message over the IPC server to all the connected clients
|
||||
*
|
||||
* @return The number of clients that the message was sent to. Note that the number of
|
||||
* messages actually received may be less, as some clients could disconnect before
|
||||
* receiving the message.
|
||||
*/
|
||||
send(message: string): number
|
||||
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
|
||||
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
|
||||
completeError(clientId: number, sequenceNumber: number, error: string): number
|
||||
}
|
||||
}
|
||||
export declare namespace autostart {
|
||||
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
|
||||
}
|
||||
export declare namespace autofill {
|
||||
export function runCommand(value: string): Promise<string>
|
||||
export const enum UserVerification {
|
||||
Preferred = 'preferred',
|
||||
Required = 'required',
|
||||
Discouraged = 'discouraged'
|
||||
}
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
export interface PasskeyRegistrationRequest {
|
||||
rpId: string
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
userVerification: UserVerification
|
||||
supportedAlgorithms: Array<number>
|
||||
windowXy: Position
|
||||
}
|
||||
export interface PasskeyRegistrationResponse {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
credentialId: Array<number>
|
||||
attestationObject: Array<number>
|
||||
export interface NativeStatus {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
export interface PasskeyAssertionRequest {
|
||||
rpId: string
|
||||
@@ -178,6 +29,14 @@ export declare namespace autofill {
|
||||
allowedCredentials: Array<Array<number>>
|
||||
windowXy: Position
|
||||
}
|
||||
export interface PasskeyAssertionResponse {
|
||||
rpId: string
|
||||
userHandle: Array<number>
|
||||
signature: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
authenticatorData: Array<number>
|
||||
credentialId: Array<number>
|
||||
}
|
||||
export interface PasskeyAssertionWithoutUserInterfaceRequest {
|
||||
rpId: string
|
||||
credentialId: Array<number>
|
||||
@@ -188,50 +47,93 @@ export declare namespace autofill {
|
||||
userVerification: UserVerification
|
||||
windowXy: Position
|
||||
}
|
||||
export interface PasskeyAssertionResponse {
|
||||
export interface PasskeyRegistrationRequest {
|
||||
rpId: string
|
||||
userName: string
|
||||
userHandle: Array<number>
|
||||
signature: Array<number>
|
||||
clientDataHash: Array<number>
|
||||
authenticatorData: Array<number>
|
||||
userVerification: UserVerification
|
||||
supportedAlgorithms: Array<number>
|
||||
windowXy: Position
|
||||
excludedCredentials: Array<Array<number>>
|
||||
}
|
||||
export interface PasskeyRegistrationResponse {
|
||||
rpId: string
|
||||
clientDataHash: Array<number>
|
||||
credentialId: Array<number>
|
||||
attestationObject: Array<number>
|
||||
}
|
||||
export class IpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise<IpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
completeRegistration(clientId: number, sequenceNumber: number, response: PasskeyRegistrationResponse): number
|
||||
completeAssertion(clientId: number, sequenceNumber: number, response: PasskeyAssertionResponse): number
|
||||
completeError(clientId: number, sequenceNumber: number, error: string): number
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
export function runCommand(value: string): Promise<string>
|
||||
export const enum UserVerification {
|
||||
Preferred = 'preferred',
|
||||
Required = 'required',
|
||||
Discouraged = 'discouraged'
|
||||
}
|
||||
}
|
||||
export declare namespace passkey_authenticator {
|
||||
export function register(): void
|
||||
|
||||
export declare namespace autostart {
|
||||
export function setAutostart(autostart: boolean, params: Array<string>): Promise<void>
|
||||
}
|
||||
export declare namespace logging {
|
||||
export const enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4
|
||||
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
}
|
||||
|
||||
export declare namespace biometrics {
|
||||
export function available(): Promise<boolean>
|
||||
/**
|
||||
* Derives key material from biometric data. Returns a string encoded with a
|
||||
* base64 encoded key and the base64 encoded challenge used to create it
|
||||
* separated by a `|` character.
|
||||
*
|
||||
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
|
||||
* be generated.
|
||||
*
|
||||
* `format!("<key_base64>|<iv_base64>")`
|
||||
*/
|
||||
export function deriveKeyMaterial(iv?: string | undefined | null): Promise<OsDerivedKey>
|
||||
/**
|
||||
* Retrieves the biometric secret for the given service and account.
|
||||
* Throws Error with message [`passwords::PASSWORD_NOT_FOUND`] if the secret does not exist.
|
||||
*/
|
||||
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
|
||||
export interface KeyMaterial {
|
||||
osKeyPartB64: string
|
||||
clientKeyPartB64?: string
|
||||
}
|
||||
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
|
||||
export interface OsDerivedKey {
|
||||
keyB64: string
|
||||
ivB64: string
|
||||
}
|
||||
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
|
||||
}
|
||||
|
||||
export declare namespace biometrics_v2 {
|
||||
export class BiometricLockSystem {
|
||||
|
||||
}
|
||||
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
|
||||
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
|
||||
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
export function initBiometricSystem(): BiometricLockSystem
|
||||
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
|
||||
export function unenroll(biometricLockSystem: BiometricLockSystem, userId: string): Promise<void>
|
||||
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
|
||||
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
|
||||
}
|
||||
|
||||
export declare namespace chromium_importer {
|
||||
export interface ProfileInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
export interface Login {
|
||||
url: string
|
||||
username: string
|
||||
@@ -252,12 +154,130 @@ export declare namespace chromium_importer {
|
||||
loaders: Array<string>
|
||||
instructions: string
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
export interface ProfileInfo {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
|
||||
export declare namespace clipboards {
|
||||
export function read(): Promise<string>
|
||||
export function write(text: string, password: boolean): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace ipc {
|
||||
export class NativeIpcServer {
|
||||
/**
|
||||
* Create and start the IPC server without blocking.
|
||||
*
|
||||
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
* connection and must be the same for both the server and client. @param callback
|
||||
* This function will be called whenever a message is received from a client.
|
||||
*/
|
||||
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<NativeIpcServer>
|
||||
/** Return the path to the IPC server. */
|
||||
getPath(): string
|
||||
/** Stop the IPC server. */
|
||||
stop(): void
|
||||
/**
|
||||
* Send a message over the IPC server to all the connected clients
|
||||
*
|
||||
* @return The number of clients that the message was sent to. Note that the number of
|
||||
* messages actually received may be less, as some clients could disconnect before
|
||||
* receiving the message.
|
||||
*/
|
||||
send(message: string): number
|
||||
}
|
||||
export interface IpcMessage {
|
||||
clientId: number
|
||||
kind: IpcMessageType
|
||||
message?: string
|
||||
}
|
||||
export const enum IpcMessageType {
|
||||
Connected = 0,
|
||||
Disconnected = 1,
|
||||
Message = 2
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace logging {
|
||||
export function initNapiLog(jsLogFn: ((err: Error | null, arg0: LogLevel, arg1: string) => any)): void
|
||||
export const enum LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4
|
||||
}
|
||||
}
|
||||
|
||||
export declare namespace passkey_authenticator {
|
||||
export function register(): void
|
||||
}
|
||||
|
||||
export declare namespace passwords {
|
||||
/**
|
||||
* Delete the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function deletePassword(service: string, account: string): Promise<void>
|
||||
/**
|
||||
* Fetch the stored password from the keychain.
|
||||
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
|
||||
*/
|
||||
export function getPassword(service: string, account: string): Promise<string>
|
||||
/** Checks if the os secure storage is available */
|
||||
export function isAvailable(): Promise<boolean>
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
/**
|
||||
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
|
||||
* existing entry.
|
||||
*/
|
||||
export function setPassword(service: string, account: string, password: string): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace powermonitors {
|
||||
export function isLockMonitorAvailable(): Promise<boolean>
|
||||
export function onLock(callback: ((err: Error | null, ) => any)): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace processisolations {
|
||||
export function disableCoredumps(): Promise<void>
|
||||
export function isCoreDumpingDisabled(): Promise<boolean>
|
||||
export function isolateProcess(): Promise<void>
|
||||
}
|
||||
|
||||
export declare namespace sshagent {
|
||||
export class SshAgentState {
|
||||
|
||||
}
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export interface PrivateKey {
|
||||
privateKey: string
|
||||
name: string
|
||||
cipherId: string
|
||||
}
|
||||
export function serve(callback: ((err: Error | null, arg: SshUiRequest) => Promise<boolean>)): Promise<SshAgentState>
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export interface SshKey {
|
||||
privateKey: string
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export function stop(agentState: SshAgentState): void
|
||||
}
|
||||
|
||||
export declare namespace windows_registry {
|
||||
export function createKey(key: string, subkey: string, value: string): Promise<void>
|
||||
export function deleteKey(key: string, subkey: string): Promise<void>
|
||||
}
|
||||
|
||||
@@ -82,20 +82,20 @@ switch (platform) {
|
||||
switch (arch) {
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-musl",
|
||||
["desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-gnu",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-musl",
|
||||
["desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-gnu",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-musl",
|
||||
["desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-gnu",
|
||||
);
|
||||
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
|
||||
try {
|
||||
|
||||
@@ -3,27 +3,23 @@
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "napi build --platform --js false",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "cargo test"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "2.18.4"
|
||||
"@napi-rs/cli": "3.2.0"
|
||||
},
|
||||
"napi": {
|
||||
"name": "desktop_napi",
|
||||
"triples": {
|
||||
"defaults": true,
|
||||
"additional": [
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"i686-pc-windows-msvc",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-pc-windows-msvc"
|
||||
]
|
||||
}
|
||||
"binaryName": "desktop_napi",
|
||||
"targets": [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
22
apps/desktop/desktop_native/napi/scripts/build.js
Normal file
22
apps/desktop/desktop_native/napi/scripts/build.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const isRelease = args.includes('--release');
|
||||
|
||||
const argsString = args.join(' ');
|
||||
|
||||
if (isRelease) {
|
||||
console.log('Building release mode.');
|
||||
|
||||
execSync(`napi build --platform --no-js ${argsString}`, { stdio: 'inherit'});
|
||||
|
||||
} else {
|
||||
console.log('Building debug mode.');
|
||||
|
||||
execSync(`napi build --platform --no-js ${argsString}`, {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, RUST_LOG: 'debug' }
|
||||
});
|
||||
}
|
||||
@@ -290,7 +290,7 @@ pub mod sshagent {
|
||||
|
||||
use napi::{
|
||||
bindgen_prelude::Promise,
|
||||
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use tokio::{self, sync::Mutex};
|
||||
use tracing::error;
|
||||
@@ -326,13 +326,15 @@ pub mod sshagent {
|
||||
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||||
callback: ThreadsafeFunction<SshUIRequest, Promise<bool>>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(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));
|
||||
// Wrap callback in Arc so it can be shared across spawned tasks
|
||||
let callback = Arc::new(callback);
|
||||
tokio::spawn(async move {
|
||||
let _ = auth_response_rx;
|
||||
|
||||
@@ -342,42 +344,50 @@ pub mod sshagent {
|
||||
tokio::spawn(async move {
|
||||
let auth_response_tx_arc = cloned_response_tx_arc;
|
||||
let callback = cloned_callback;
|
||||
let promise_result: Result<Promise<bool>, napi::Error> = callback
|
||||
.call_async(Ok(SshUIRequest {
|
||||
// In NAPI v3, obtain the JS callback return as a Promise<boolean> and await it
|
||||
// in Rust
|
||||
let (tx, rx) = std::sync::mpsc::channel::<Promise<bool>>();
|
||||
let status = callback.call_with_return_value(
|
||||
Ok(SshUIRequest {
|
||||
cipher_id: request.cipher_id,
|
||||
is_list: request.is_list,
|
||||
process_name: request.process_name,
|
||||
is_forwarding: request.is_forwarding,
|
||||
namespace: request.namespace,
|
||||
}))
|
||||
.await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
Ok(result) => {
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback promise was rejected");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
}),
|
||||
ThreadsafeFunctionCallMode::Blocking,
|
||||
move |ret: Result<Promise<bool>, napi::Error>, _env| {
|
||||
if let Ok(p) = ret {
|
||||
let _ = tx.send(p);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Calling UI callback could not create promise");
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, false))
|
||||
.expect("should be able to send auth response to agent");
|
||||
);
|
||||
|
||||
let result = if status == napi::Status::Ok {
|
||||
match rx.recv() {
|
||||
Ok(promise) => match promise.await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(error = %e, "UI callback promise rejected");
|
||||
false
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!(error = %e, "Failed to receive UI callback promise");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(error = ?status, "Calling UI callback failed");
|
||||
false
|
||||
};
|
||||
|
||||
let _ = auth_response_tx_arc
|
||||
.lock()
|
||||
.await
|
||||
.send((request.request_id, result))
|
||||
.expect("should be able to send auth response to agent");
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -465,14 +475,12 @@ pub mod processisolations {
|
||||
#[napi]
|
||||
pub mod powermonitors {
|
||||
use napi::{
|
||||
threadsafe_function::{
|
||||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
},
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
tokio,
|
||||
};
|
||||
|
||||
#[napi]
|
||||
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
|
||||
pub async fn on_lock(callback: ThreadsafeFunction<()>) -> napi::Result<()> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
|
||||
desktop_core::powermonitor::on_lock(tx)
|
||||
.await
|
||||
@@ -511,9 +519,7 @@ pub mod windows_registry {
|
||||
#[napi]
|
||||
pub mod ipc {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
};
|
||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct IpcMessage {
|
||||
@@ -550,12 +556,12 @@ pub mod ipc {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
pub struct NativeIpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
impl NativeIpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
@@ -566,7 +572,7 @@ pub mod ipc {
|
||||
pub async fn listen(
|
||||
name: String,
|
||||
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
|
||||
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
|
||||
callback: ThreadsafeFunction<IpcMessage>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
@@ -583,7 +589,7 @@ pub mod ipc {
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
Ok(NativeIpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
@@ -630,8 +636,9 @@ pub mod autostart {
|
||||
#[napi]
|
||||
pub mod autofill {
|
||||
use desktop_core::ipc::server::{Message, MessageType};
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
use napi::{
|
||||
bindgen_prelude::FnArgs,
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tracing::error;
|
||||
@@ -686,6 +693,7 @@ pub mod autofill {
|
||||
pub user_verification: UserVerification,
|
||||
pub supported_algorithms: Vec<i32>,
|
||||
pub window_xy: Position,
|
||||
pub excluded_credentials: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
@@ -724,6 +732,14 @@ pub mod autofill {
|
||||
pub window_xy: Position,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NativeStatus {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -737,14 +753,14 @@ pub mod autofill {
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub struct IpcServer {
|
||||
pub struct AutofillIpcServer {
|
||||
server: desktop_core::ipc::server::Server,
|
||||
}
|
||||
|
||||
// FIXME: Remove unwraps! They panic and terminate the whole application.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[napi]
|
||||
impl IpcServer {
|
||||
impl AutofillIpcServer {
|
||||
/// Create and start the IPC server without blocking.
|
||||
///
|
||||
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
|
||||
@@ -760,23 +776,24 @@ pub mod autofill {
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
|
||||
)]
|
||||
registration_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyRegistrationRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyRegistrationRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
|
||||
)]
|
||||
assertion_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyAssertionRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
|
||||
)]
|
||||
assertion_without_user_interface_callback: ThreadsafeFunction<
|
||||
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
|
||||
ErrorStrategy::CalleeHandled,
|
||||
FnArgs<(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest)>,
|
||||
>,
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: NativeStatus) => void"
|
||||
)]
|
||||
native_status_callback: ThreadsafeFunction<(u32, u32, NativeStatus)>,
|
||||
) -> napi::Result<Self> {
|
||||
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
|
||||
tokio::spawn(async move {
|
||||
@@ -801,7 +818,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_callback
|
||||
@@ -820,7 +837,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
|
||||
assertion_without_user_interface_callback
|
||||
@@ -838,7 +855,7 @@ pub mod autofill {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map(|value| (client_id, msg.sequence_number, value).into())
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
registration_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
@@ -849,6 +866,21 @@ pub mod autofill {
|
||||
}
|
||||
}
|
||||
|
||||
match serde_json::from_str::<PasskeyMessage<NativeStatus>>(&message) {
|
||||
Ok(msg) => {
|
||||
let value = msg
|
||||
.value
|
||||
.map(|value| (client_id, msg.sequence_number, value))
|
||||
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
|
||||
native_status_callback
|
||||
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
error!(%error, "Unable to deserialze native status.");
|
||||
}
|
||||
}
|
||||
|
||||
error!(message, "Received an unknown message2");
|
||||
}
|
||||
}
|
||||
@@ -863,7 +895,7 @@ pub mod autofill {
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(IpcServer { server })
|
||||
Ok(AutofillIpcServer { server })
|
||||
}
|
||||
|
||||
/// Return the path to the IPC server.
|
||||
@@ -956,19 +988,20 @@ pub mod logging {
|
||||
|
||||
use std::{fmt::Write, sync::OnceLock};
|
||||
|
||||
use napi::threadsafe_function::{
|
||||
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
|
||||
use napi::{
|
||||
bindgen_prelude::FnArgs,
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode},
|
||||
};
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::{
|
||||
filter::{EnvFilter, LevelFilter},
|
||||
filter::EnvFilter,
|
||||
fmt::format::{DefaultVisitor, Writer},
|
||||
layer::SubscriberExt,
|
||||
util::SubscriberInitExt,
|
||||
Layer,
|
||||
};
|
||||
|
||||
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
|
||||
struct JsLogger(OnceLock<ThreadsafeFunction<FnArgs<(LogLevel, String)>>>);
|
||||
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
|
||||
|
||||
#[napi]
|
||||
@@ -1040,18 +1073,26 @@ pub mod logging {
|
||||
let msg = (event.metadata().level().into(), buffer);
|
||||
|
||||
if let Some(logger) = JS_LOGGER.0.get() {
|
||||
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
let _ = logger.call(Ok(msg.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
|
||||
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<FnArgs<(LogLevel, String)>>) {
|
||||
let _ = JS_LOGGER.0.set(js_log_fn);
|
||||
|
||||
// the log level hierarchy is determined by:
|
||||
// - if RUST_LOG is detected at runtime
|
||||
// - if RUST_LOG is provided at compile time
|
||||
// - default to INFO
|
||||
let filter = EnvFilter::builder()
|
||||
// set the default log level to INFO.
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.with_default_directive(
|
||||
option_env!("RUST_LOG")
|
||||
.unwrap_or("info")
|
||||
.parse()
|
||||
.expect("should provide valid log level at compile time."),
|
||||
)
|
||||
// parse directives from the RUST_LOG environment variable,
|
||||
// overriding the default directive for matching targets.
|
||||
.from_env_lossy();
|
||||
@@ -1109,8 +1150,8 @@ pub mod chromium_importer {
|
||||
#[napi(object)]
|
||||
pub struct NativeImporterMetadata {
|
||||
pub id: String,
|
||||
pub loaders: Vec<&'static str>,
|
||||
pub instructions: &'static str,
|
||||
pub loaders: Vec<String>,
|
||||
pub instructions: String,
|
||||
}
|
||||
|
||||
impl From<_LoginImportResult> for LoginImportResult {
|
||||
@@ -1187,7 +1228,7 @@ pub mod chromium_importer {
|
||||
#[napi]
|
||||
pub mod autotype {
|
||||
#[napi]
|
||||
pub fn get_foreground_window_title() -> napi::Result<String, napi::Status> {
|
||||
pub fn get_foreground_window_title() -> napi::Result<String> {
|
||||
autotype::get_foreground_window_title().map_err(|_| {
|
||||
napi::Error::from_reason(
|
||||
"Autotype Error: failed to get foreground window title".to_string(),
|
||||
|
||||
@@ -14,8 +14,8 @@ tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
cc = "=1.2.46"
|
||||
glob = "=0.3.2"
|
||||
cc = "=1.2.49"
|
||||
glob = "=0.3.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -14,40 +14,64 @@ void runSync(void* context, NSDictionary *params) {
|
||||
|
||||
// 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 (@available(macos 14, *)) {
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
@try {
|
||||
NSString *type = credential[@"type"];
|
||||
|
||||
if ([type isEqualToString:@"password"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
NSString *uri = credential[@"uri"];
|
||||
NSString *username = credential[@"username"];
|
||||
|
||||
// Skip credentials with null username since MacOS crashes if we send credentials with empty usernames
|
||||
if ([username isKindOfClass:[NSNull class]] || username.length == 0) {
|
||||
NSLog(@"Skipping credential, username is empty: %@", credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
|
||||
id credential = [[passkeyCredentialIdentityClass alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
ASCredentialServiceIdentifier *serviceId = [[ASCredentialServiceIdentifier alloc]
|
||||
initWithIdentifier:uri type:ASCredentialServiceIdentifierTypeURL];
|
||||
ASPasswordCredentialIdentity *passwordIdentity = [[ASPasswordCredentialIdentity alloc]
|
||||
initWithServiceIdentifier:serviceId user:username recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
[mappedCredentials addObject:passwordIdentity];
|
||||
}
|
||||
else if (@available(macos 14, *)) {
|
||||
// Fido2CredentialView uses `userName` (camelCase) while Login uses `username`.
|
||||
// This is intentional. Fido2 fields are flattened from the FIDO2 spec's nested structure
|
||||
// (user.name -> userName, rp.id -> rpId) to maintain a clear distinction between these fields.
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
|
||||
// Skip credentials with null username since MacOS crashes if we send credentials with empty usernames
|
||||
if ([userName isKindOfClass:[NSNull class]] || userName.length == 0) {
|
||||
NSLog(@"Skipping credential, username is empty: %@", credential);
|
||||
continue;
|
||||
}
|
||||
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
|
||||
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
|
||||
id passkeyIdentity = [[passkeyCredentialIdentityClass alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:passkeyIdentity];
|
||||
}
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
// Silently skip any credential that causes an exception
|
||||
// to make sure we don't fail the entire sync
|
||||
// There is likely some invalid data in the credential, and not something the user should/could be asked to correct.
|
||||
NSLog(@"ERROR: Exception processing credential: %@ - %@", exception.name, exception.reason);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,26 @@ NSString *serializeJson(NSDictionary *dictionary, NSError *error) {
|
||||
}
|
||||
|
||||
NSData *decodeBase64URL(NSString *base64URLString) {
|
||||
if (base64URLString.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Replace URL-safe characters with standard base64 characters
|
||||
NSString *base64String = [base64URLString stringByReplacingOccurrencesOfString:@"-" withString:@"+"];
|
||||
base64String = [base64String stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
|
||||
|
||||
|
||||
// Add padding if needed
|
||||
// Base 64 strings should be a multiple of 4 in length
|
||||
NSUInteger paddingLength = 4 - (base64String.length % 4);
|
||||
if (paddingLength < 4) {
|
||||
NSMutableString *paddedString = [NSMutableString stringWithString:base64String];
|
||||
for (NSUInteger i = 0; i < paddingLength; i++) {
|
||||
[paddedString appendString:@"="];
|
||||
}
|
||||
base64String = paddedString;
|
||||
}
|
||||
|
||||
// Decode the string
|
||||
NSData *nsdataFromBase64String = [[NSData alloc]
|
||||
initWithBase64EncodedString:base64String options:0];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "1.87.0"
|
||||
channel = "1.91.1"
|
||||
components = [ "rustfmt", "clippy" ]
|
||||
profile = "minimal"
|
||||
|
||||
@@ -153,7 +153,7 @@ fn add_authenticator() -> std::result::Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "cdecl" fn(
|
||||
type EXPERIMENTAL_WebAuthNPluginAddAuthenticatorFnDeclaration = unsafe extern "C" fn(
|
||||
pPluginAddAuthenticatorOptions: *const webauthn::ExperimentalWebAuthnPluginAddAuthenticatorOptions,
|
||||
ppPluginAddAuthenticatorResponse: *mut *mut webauthn::ExperimentalWebAuthnPluginAddAuthenticatorResponse,
|
||||
) -> HRESULT;
|
||||
|
||||
@@ -8,63 +8,56 @@
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="CredentialProviderViewController" customModule="autofill_extension" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="logoImageView" destination="logoImageView" id="logoImageViewOutlet"/>
|
||||
<outlet property="statusLabel" destination="statusLabel" id="statusLabelOutlet"/>
|
||||
<outlet property="view" destination="1" id="2"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView hidden="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1">
|
||||
<rect key="frame" x="0.0" y="0.0" width="378" height="94"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="120"/>
|
||||
<subviews>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1uM-r7-H1c">
|
||||
<rect key="frame" x="184" y="3" width="191" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Return Example Password" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="2l4-PO-we5">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent">D</string>
|
||||
<modifierMask key="keyEquivalentModifierMask" command="YES"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="passwordSelected:" target="-2" id="yic-EC-GGk"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="NVE-vN-dkz">
|
||||
<rect key="frame" x="114" y="3" width="76" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="6Up-t3-mwm">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="60" id="cP1-hK-9ZX"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="Qav-AK-DGt"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="aNc-0i-CWK">
|
||||
<rect key="frame" x="112" y="63" width="154" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="autofill-extension hello" id="0xp-rC-2gr">
|
||||
<font key="font" metaFont="systemBold"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="centerY" spacing="20" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="configStackView">
|
||||
<rect key="frame" x="89" y="35" width="223" height="50"/>
|
||||
<subviews>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="logoImageView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="logoImageHeight"/>
|
||||
<constraint firstAttribute="width" constant="50" id="logoImageWidth"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="bitwarden-icon" id="logoImageCell"/>
|
||||
</imageView>
|
||||
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="statusLabel">
|
||||
<rect key="frame" x="68" y="16" width="157" height="19"/>
|
||||
<textFieldCell key="cell" sendsActionOnEndEditing="YES" alignment="left" title="Enabling Bitwarden..." id="statusLabelCell">
|
||||
<font key="font" metaFont="system" size="16"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<visibilityPriorities>
|
||||
<integer value="1000"/>
|
||||
<integer value="1000"/>
|
||||
</visibilityPriorities>
|
||||
<customSpacing>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
<real value="3.4028234663852886e+38"/>
|
||||
</customSpacing>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="1UO-J1-LbJ"/>
|
||||
<constraint firstItem="NVE-vN-dkz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" symbolic="YES" id="3N9-qo-UfS"/>
|
||||
<constraint firstAttribute="bottom" secondItem="1uM-r7-H1c" secondAttribute="bottom" constant="10" id="4wH-De-nMF"/>
|
||||
<constraint firstItem="NVE-vN-dkz" firstAttribute="firstBaseline" secondItem="aNc-0i-CWK" secondAttribute="baseline" constant="50" id="Dpq-cK-cPE"/>
|
||||
<constraint firstAttribute="bottom" secondItem="NVE-vN-dkz" secondAttribute="bottom" constant="10" id="USG-Gg-of3"/>
|
||||
<constraint firstItem="1uM-r7-H1c" firstAttribute="leading" secondItem="NVE-vN-dkz" secondAttribute="trailing" constant="8" id="a8N-vS-Ew9"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1uM-r7-H1c" secondAttribute="trailing" constant="10" id="qfT-cw-QQ2"/>
|
||||
<constraint firstAttribute="centerX" secondItem="aNc-0i-CWK" secondAttribute="centerX" id="uV3-Wn-RA3"/>
|
||||
<constraint firstItem="aNc-0i-CWK" firstAttribute="top" secondItem="1" secondAttribute="top" constant="15" id="vpR-tf-ebx"/>
|
||||
<constraint firstItem="configStackView" firstAttribute="centerX" secondItem="1" secondAttribute="centerX" id="stackCenterX"/>
|
||||
<constraint firstItem="configStackView" firstAttribute="centerY" secondItem="1" secondAttribute="centerY" id="stackCenterY"/>
|
||||
<constraint firstItem="configStackView" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="1" secondAttribute="leading" constant="20" id="stackLeading"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="configStackView" secondAttribute="trailing" constant="20" id="stackTrailing"/>
|
||||
</constraints>
|
||||
<point key="canvasLocation" x="162" y="146"/>
|
||||
<point key="canvasLocation" x="200" y="60"/>
|
||||
</customView>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="bitwarden-icon" width="64" height="64"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -11,63 +11,138 @@ import os
|
||||
class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
let logger: Logger
|
||||
|
||||
// There is something a bit strange about the initialization/deinitialization in this class.
|
||||
// Sometimes deinit won't be called after a request has successfully finished,
|
||||
// which would leave this class hanging in memory and the IPC connection open.
|
||||
//
|
||||
// If instead I make this a static, the deinit gets called correctly after each request.
|
||||
// I think we still might want a static regardless, to be able to reuse the connection if possible.
|
||||
let client: MacOsProviderClient = {
|
||||
let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
@IBOutlet weak var statusLabel: NSTextField!
|
||||
@IBOutlet weak var logoImageView: NSImageView!
|
||||
|
||||
// The IPC client to communicate with the Bitwarden desktop app
|
||||
private var client: MacOsProviderClient?
|
||||
|
||||
// Timer for checking connection status
|
||||
private var connectionMonitorTimer: Timer?
|
||||
private var lastConnectionStatus: ConnectionStatus = .disconnected
|
||||
|
||||
// We changed the getClient method to be async, here's why:
|
||||
// This is so that we can check if the app is running, and launch it, without blocking the main thread
|
||||
// Blocking the main thread caused MacOS layouting to 'fail' or at least be very delayed, which caused our getWindowPositioning code to sent 0,0.
|
||||
// We also properly retry the IPC connection which sometimes would take some time to be up and running, depending on CPU load, phase of jupiters moon, etc.
|
||||
private func getClient() async -> MacOsProviderClient {
|
||||
if let client = self.client {
|
||||
return client
|
||||
}
|
||||
|
||||
let logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
|
||||
// Check if the Electron app is running
|
||||
let workspace = NSWorkspace.shared
|
||||
let isRunning = workspace.runningApplications.contains { app in
|
||||
app.bundleIdentifier == "com.bitwarden.desktop"
|
||||
}
|
||||
|
||||
|
||||
if !isRunning {
|
||||
logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch")
|
||||
|
||||
// Try to launch the app
|
||||
logger.log("[autofill-extension] Bitwarden Desktop not running, attempting to launch")
|
||||
|
||||
// Launch the app and wait for it to be ready
|
||||
if let appURL = workspace.urlForApplication(withBundleIdentifier: "com.bitwarden.desktop") {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
workspace.openApplication(at: appURL,
|
||||
configuration: NSWorkspace.OpenConfiguration()) { app, error in
|
||||
if let error = error {
|
||||
logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: \(error.localizedDescription)")
|
||||
} else if let app = app {
|
||||
logger.log("[autofill-extension] Successfully launched Bitwarden Desktop")
|
||||
} else {
|
||||
logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: unknown error")
|
||||
await withCheckedContinuation { continuation in
|
||||
workspace.openApplication(at: appURL, configuration: NSWorkspace.OpenConfiguration()) { app, error in
|
||||
if let error = error {
|
||||
logger.error("[autofill-extension] Failed to launch Bitwarden Desktop: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.log("[autofill-extension] Successfully launched Bitwarden Desktop")
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
// Wait for launch completion with timeout
|
||||
_ = semaphore.wait(timeout: .now() + 5.0)
|
||||
|
||||
// Add a small delay to allow for initialization
|
||||
Thread.sleep(forTimeInterval: 1.0)
|
||||
} else {
|
||||
logger.error("[autofill-extension] Could not find Bitwarden Desktop app")
|
||||
}
|
||||
} else {
|
||||
logger.log("[autofill-extension] Bitwarden Desktop is running")
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] Connecting to Bitwarden over IPC")
|
||||
|
||||
// Retry connecting to the Bitwarden IPC with an increasing delay
|
||||
let maxRetries = 20
|
||||
let delayMs = 500
|
||||
var newClient: MacOsProviderClient?
|
||||
|
||||
for attempt in 1...maxRetries {
|
||||
logger.log("[autofill-extension] Connection attempt \(attempt)")
|
||||
|
||||
// Create a new client instance for each retry
|
||||
newClient = MacOsProviderClient.connect()
|
||||
try? await Task.sleep(nanoseconds: UInt64(100 * attempt + (delayMs * 1_000_000))) // Convert ms to nanoseconds
|
||||
let connectionStatus = newClient!.getConnectionStatus()
|
||||
|
||||
logger.log("[autofill-extension] Connection attempt \(attempt), status: \(connectionStatus == .connected ? "connected" : "disconnected")")
|
||||
|
||||
if connectionStatus == .connected {
|
||||
logger.log("[autofill-extension] Successfully connected to Bitwarden (attempt \(attempt))")
|
||||
break
|
||||
} else {
|
||||
if attempt < maxRetries {
|
||||
logger.log("[autofill-extension] Retrying connection")
|
||||
} else {
|
||||
logger.error("[autofill-extension] Failed to connect after \(maxRetries) attempts, final status: \(connectionStatus == .connected ? "connected" : "disconnected")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] Connecting to Bitwarden over IPC")
|
||||
|
||||
return MacOsProviderClient.connect()
|
||||
}()
|
||||
self.client = newClient
|
||||
return newClient!
|
||||
}
|
||||
|
||||
// Setup the connection monitoring timer
|
||||
private func setupConnectionMonitoring() {
|
||||
// Check connection status every 1 second
|
||||
connectionMonitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
|
||||
self?.checkConnectionStatus()
|
||||
}
|
||||
|
||||
// Make sure timer runs even when UI is busy
|
||||
RunLoop.current.add(connectionMonitorTimer!, forMode: .common)
|
||||
|
||||
// Initial check
|
||||
checkConnectionStatus()
|
||||
}
|
||||
|
||||
// Check the connection status by calling into Rust
|
||||
// If the connection is has changed and is now disconnected, cancel the request
|
||||
private func checkConnectionStatus() {
|
||||
// Only check connection status if the client has been initialized.
|
||||
// Initialization is done asynchronously, so we might be called before it's ready
|
||||
// In that case we just skip this check and wait for the next timer tick and re-check
|
||||
guard let client = self.client else {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current connection status from Rust
|
||||
let currentStatus = client.getConnectionStatus()
|
||||
|
||||
// Only post notification if state changed
|
||||
if currentStatus != lastConnectionStatus {
|
||||
if(currentStatus == .connected) {
|
||||
logger.log("[autofill-extension] Connection status changed: Connected")
|
||||
} else {
|
||||
logger.log("[autofill-extension] Connection status changed: Disconnected")
|
||||
}
|
||||
|
||||
// Save the new status
|
||||
lastConnectionStatus = currentStatus
|
||||
|
||||
// If we just disconnected, try to cancel the request
|
||||
if currentStatus == .disconnected {
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Bitwarden desktop app disconnected"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
logger = Logger(subsystem: "com.bitwarden.desktop.autofill-extension", category: "credential-provider")
|
||||
|
||||
logger.log("[autofill-extension] initializing extension")
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
super.init(nibName: "CredentialProviderViewController", bundle: nil)
|
||||
|
||||
// Setup connection monitoring now that self is available
|
||||
setupConnectionMonitoring()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@@ -76,45 +151,109 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
|
||||
deinit {
|
||||
logger.log("[autofill-extension] deinitializing extension")
|
||||
}
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: AnyObject?) {
|
||||
self.extensionContext.cancelRequest(withError: NSError(domain: ASExtensionErrorDomain, code: ASExtensionError.userCanceled.rawValue))
|
||||
}
|
||||
|
||||
@IBAction func passwordSelected(_ sender: AnyObject?) {
|
||||
let passwordCredential = ASPasswordCredential(user: "j_appleseed", password: "apple1234")
|
||||
self.extensionContext.completeRequest(withSelectedCredential: passwordCredential, completionHandler: nil)
|
||||
}
|
||||
|
||||
private func getWindowPosition() -> Position {
|
||||
let frame = self.view.window?.frame ?? .zero
|
||||
let screenHeight = NSScreen.main?.frame.height ?? 0
|
||||
|
||||
// frame.width and frame.height is always 0. Estimating works OK for now.
|
||||
let estimatedWidth:CGFloat = 400;
|
||||
let estimatedHeight:CGFloat = 200;
|
||||
let centerX = Int32(round(frame.origin.x + estimatedWidth/2))
|
||||
let centerY = Int32(round(screenHeight - (frame.origin.y + estimatedHeight/2)))
|
||||
|
||||
return Position(x: centerX, y:centerY)
|
||||
// Stop the connection monitor timer
|
||||
connectionMonitorTimer?.invalidate()
|
||||
connectionMonitorTimer = nil
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
let view = NSView()
|
||||
// Hide the native window since we only need the IPC connection
|
||||
view.isHidden = true
|
||||
self.view = view
|
||||
private func getWindowPosition() async -> Position {
|
||||
let screenHeight = NSScreen.main?.frame.height ?? 1440
|
||||
|
||||
logger.log("[autofill-extension] position: Getting window position")
|
||||
|
||||
// To whomever is reading this. Sorry. But MacOS couldn't give us an accurate window positioning, possibly due to animations
|
||||
// So I added some retry logic, as well as a fall back to the mouse position which is likely at the sort of the right place.
|
||||
// In my testing we often succed after 4-7 attempts.
|
||||
// Wait for window frame to stabilize (animation to complete)
|
||||
var lastFrame: CGRect = .zero
|
||||
var stableCount = 0
|
||||
let requiredStableChecks = 3
|
||||
let maxAttempts = 20
|
||||
var attempts = 0
|
||||
|
||||
while stableCount < requiredStableChecks && attempts < maxAttempts {
|
||||
let currentFrame: CGRect = self.view.window?.frame ?? .zero
|
||||
|
||||
if currentFrame.equalTo(lastFrame) && !currentFrame.equalTo(.zero) {
|
||||
stableCount += 1
|
||||
} else {
|
||||
stableCount = 0
|
||||
lastFrame = currentFrame
|
||||
}
|
||||
|
||||
try? await Task.sleep(nanoseconds: 16_666_666) // ~60fps (16.67ms)
|
||||
attempts += 1
|
||||
}
|
||||
|
||||
let finalWindowFrame = self.view.window?.frame ?? .zero
|
||||
logger.log("[autofill-extension] position: Final window frame: \(NSStringFromRect(finalWindowFrame))")
|
||||
|
||||
// Use stabilized window frame if available, otherwise fallback to mouse position
|
||||
if finalWindowFrame.origin.x != 0 || finalWindowFrame.origin.y != 0 {
|
||||
let centerX = Int32(round(finalWindowFrame.origin.x))
|
||||
let centerY = Int32(round(screenHeight - finalWindowFrame.origin.y))
|
||||
logger.log("[autofill-extension] position: Using window position: x=\(centerX), y=\(centerY)")
|
||||
return Position(x: centerX, y: centerY)
|
||||
} else {
|
||||
// Fallback to mouse position
|
||||
let mouseLocation = NSEvent.mouseLocation
|
||||
let mouseX = Int32(round(mouseLocation.x))
|
||||
let mouseY = Int32(round(screenHeight - mouseLocation.y))
|
||||
logger.log("[autofill-extension] position: Using mouse position fallback: x=\(mouseX), y=\(mouseY)")
|
||||
return Position(x: mouseX, y: mouseY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Initially hide the view
|
||||
self.view.isHidden = true
|
||||
}
|
||||
|
||||
override func prepareInterfaceForExtensionConfiguration() {
|
||||
// Show the configuration UI
|
||||
self.view.isHidden = false
|
||||
|
||||
// Set the localized message
|
||||
statusLabel.stringValue = NSLocalizedString("autofillConfigurationMessage", comment: "Message shown when Bitwarden is enabled in system settings")
|
||||
|
||||
// Send the native status request asynchronously
|
||||
Task {
|
||||
let client = await getClient()
|
||||
client.sendNativeStatus(key: "request-sync", value: "")
|
||||
}
|
||||
|
||||
// Complete the configuration after 2 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
|
||||
self?.extensionContext.completeExtensionConfigurationRequest()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
In order to implement this method, we need to query the state of the vault to be unlocked and have one and only one matching credential so that it doesn't need to show ui.
|
||||
If we do show UI, it's going to fail and disconnect after the platform timeout which is 3s.
|
||||
For now we just claim to always need UI displayed.
|
||||
*/
|
||||
override func provideCredentialWithoutUserInteraction(for credentialRequest: any ASCredentialRequest) {
|
||||
let error = ASExtensionError(.userInteractionRequired)
|
||||
self.extensionContext.cancelRequest(withError: error)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
UI and call this method. Show appropriate UI for authenticating the user then provide the password
|
||||
by completing the extension request with the associated ASPasswordCredential.
|
||||
*/
|
||||
override func prepareInterfaceToProvideCredential(for credentialRequest: ASCredentialRequest) {
|
||||
let timeoutTimer = createTimer()
|
||||
|
||||
if let request = credentialRequest as? ASPasskeyCredentialRequest {
|
||||
if let passkeyIdentity = request.credentialIdentity as? ASPasskeyCredentialIdentity {
|
||||
|
||||
logger.log("[autofill-extension] provideCredentialWithoutUserInteraction2(passkey) called \(request)")
|
||||
|
||||
logger.log("[autofill-extension] prepareInterfaceToProvideCredential (passkey) called \(request)")
|
||||
|
||||
class CallbackImpl: PreparePasskeyAssertionCallback {
|
||||
let ctx: ASCredentialProviderExtensionContext
|
||||
@@ -154,18 +293,25 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyAssertionWithoutUserInterfaceRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
credentialId: passkeyIdentity.credentialID,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
recordIdentifier: passkeyIdentity.recordIdentifier,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
windowXy: self.getWindowPosition()
|
||||
)
|
||||
|
||||
self.client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
/*
|
||||
We're still using the old request type here, because we're sending the same data, we're expecting a single credential to be used
|
||||
*/
|
||||
Task {
|
||||
let windowPosition = await self.getWindowPosition()
|
||||
let req = PasskeyAssertionWithoutUserInterfaceRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
credentialId: passkeyIdentity.credentialID,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
recordIdentifier: passkeyIdentity.recordIdentifier,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
windowXy: windowPosition
|
||||
)
|
||||
|
||||
let client = await getClient()
|
||||
client.preparePasskeyAssertionWithoutUserInterface(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -176,16 +322,6 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
self.extensionContext.cancelRequest(withError: BitwardenError.Internal("Invalid authentication request"))
|
||||
}
|
||||
|
||||
/*
|
||||
Implement this method if provideCredentialWithoutUserInteraction(for:) can fail with
|
||||
ASExtensionError.userInteractionRequired. In this case, the system may present your extension's
|
||||
UI and call this method. Show appropriate UI for authenticating the user then provide the password
|
||||
by completing the extension request with the associated ASPasswordCredential.
|
||||
|
||||
override func prepareInterfaceToProvideCredential(for credentialIdentity: ASPasswordCredentialIdentity) {
|
||||
}
|
||||
*/
|
||||
|
||||
private func createTimer() -> DispatchWorkItem {
|
||||
// Create a timer for 600 second timeout
|
||||
let timeoutTimer = DispatchWorkItem { [weak self] in
|
||||
@@ -246,18 +382,32 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyRegistrationRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) },
|
||||
windowXy: self.getWindowPosition()
|
||||
)
|
||||
// Convert excluded credentials to an array of credential IDs
|
||||
var excludedCredentialIds: [Data] = []
|
||||
if #available(macOSApplicationExtension 15.0, *) {
|
||||
if let excludedCreds = request.excludedCredentials {
|
||||
excludedCredentialIds = excludedCreds.map { $0.credentialID }
|
||||
}
|
||||
}
|
||||
|
||||
logger.log("[autofill-extension] prepareInterface(passkey) calling preparePasskeyRegistration")
|
||||
|
||||
self.client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
Task {
|
||||
let windowPosition = await self.getWindowPosition()
|
||||
let req = PasskeyRegistrationRequest(
|
||||
rpId: passkeyIdentity.relyingPartyIdentifier,
|
||||
userName: passkeyIdentity.userName,
|
||||
userHandle: passkeyIdentity.userHandle,
|
||||
clientDataHash: request.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
supportedAlgorithms: request.supportedAlgorithms.map{ Int32($0.rawValue) },
|
||||
windowXy: windowPosition,
|
||||
excludedCredentials: excludedCredentialIds
|
||||
)
|
||||
|
||||
let client = await getClient()
|
||||
client.preparePasskeyRegistration(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -310,18 +460,22 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
UserVerification.discouraged
|
||||
}
|
||||
|
||||
let req = PasskeyAssertionRequest(
|
||||
rpId: requestParameters.relyingPartyIdentifier,
|
||||
clientDataHash: requestParameters.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
allowedCredentials: requestParameters.allowedCredentials,
|
||||
windowXy: self.getWindowPosition()
|
||||
//extensionInput: requestParameters.extensionInput,
|
||||
)
|
||||
|
||||
let timeoutTimer = createTimer()
|
||||
|
||||
self.client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
Task {
|
||||
let windowPosition = await self.getWindowPosition()
|
||||
let req = PasskeyAssertionRequest(
|
||||
rpId: requestParameters.relyingPartyIdentifier,
|
||||
clientDataHash: requestParameters.clientDataHash,
|
||||
userVerification: userVerification,
|
||||
allowedCredentials: requestParameters.allowedCredentials,
|
||||
windowXy: windowPosition
|
||||
//extensionInput: requestParameters.extensionInput, // We don't support extensions yet
|
||||
)
|
||||
|
||||
let client = await getClient()
|
||||
client.preparePasskeyAssertion(request: req, callback: CallbackImpl(self.extensionContext, self.logger, timeoutTimer))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<dict>
|
||||
<key>ProvidesPasskeys</key>
|
||||
<true/>
|
||||
<key>ShowsConfigurationUI</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
|
||||
BIN
apps/desktop/macos/autofill-extension/bitwarden-icon.png
Normal file
BIN
apps/desktop/macos/autofill-extension/bitwarden-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,2 @@
|
||||
/* Message shown during passkey configuration */
|
||||
"autofillConfigurationMessage" = "Enabling Bitwarden...";
|
||||
@@ -9,6 +9,8 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
3368DB392C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */; };
|
||||
3368DB3B2C654F3800896B75 /* BitwardenMacosProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */; };
|
||||
9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */; };
|
||||
9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9AE2990D2DFB57A200AAE454 /* Localizable.strings */; };
|
||||
E1DF713F2B342F6900F29026 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1DF713E2B342F6900F29026 /* AuthenticationServices.framework */; };
|
||||
E1DF71422B342F6900F29026 /* CredentialProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */; };
|
||||
E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */; };
|
||||
@@ -18,6 +20,8 @@
|
||||
3368DB382C654B8100896B75 /* BitwardenMacosProviderFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = BitwardenMacosProviderFFI.xcframework; path = ../desktop_native/macos_provider/BitwardenMacosProviderFFI.xcframework; sourceTree = "<group>"; };
|
||||
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitwardenMacosProvider.swift; sourceTree = "<group>"; };
|
||||
968ED08A2C52A47200FFFEE6 /* ReleaseAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseAppStore.xcconfig; sourceTree = "<group>"; };
|
||||
9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "bitwarden-icon.png"; sourceTree = "<group>"; };
|
||||
9AE2990C2DFB57A200AAE454 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
D83832AB2D67B9AE003FB9F8 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
D83832AD2D67B9D0003FB9F8 /* ReleaseDeveloper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseDeveloper.xcconfig; sourceTree = "<group>"; };
|
||||
E1DF713C2B342F6900F29026 /* autofill-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "autofill-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -41,6 +45,14 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
9AE2990E2DFB57A200AAE454 /* en.lproj */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9AE2990D2DFB57A200AAE454 /* Localizable.strings */,
|
||||
);
|
||||
path = en.lproj;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1DF711D2B342E2800F29026 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -73,6 +85,8 @@
|
||||
E1DF71402B342F6900F29026 /* autofill-extension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9AE2990E2DFB57A200AAE454 /* en.lproj */,
|
||||
9AE299082DF9D82E00AAE454 /* bitwarden-icon.png */,
|
||||
3368DB3A2C654F3800896B75 /* BitwardenMacosProvider.swift */,
|
||||
E1DF71412B342F6900F29026 /* CredentialProviderViewController.swift */,
|
||||
E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */,
|
||||
@@ -124,6 +138,7 @@
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
sv,
|
||||
);
|
||||
mainGroup = E1DF711D2B342E2800F29026;
|
||||
productRefGroup = E1DF71272B342E2800F29026 /* Products */;
|
||||
@@ -141,6 +156,8 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E1DF71452B342F6900F29026 /* CredentialProviderViewController.xib in Resources */,
|
||||
9AE299122DFB57A200AAE454 /* Localizable.strings in Resources */,
|
||||
9AE299092DF9D82E00AAE454 /* bitwarden-icon.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -159,6 +176,14 @@
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
9AE2990D2DFB57A200AAE454 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
9AE2990C2DFB57A200AAE454 /* en */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E1DF71432B342F6900F29026 /* CredentialProviderViewController.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.1",
|
||||
"@types/node": "22.19.2",
|
||||
"typescript": "5.4.2"
|
||||
}
|
||||
},
|
||||
@@ -117,9 +117,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
||||
"version": "22.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.1",
|
||||
"@types/node": "22.19.2",
|
||||
"typescript": "5.4.2"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -18,6 +18,7 @@
|
||||
"scripts": {
|
||||
"postinstall": "electron-rebuild",
|
||||
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
||||
"build-native-macos": "cd desktop_native && ./macos_provider/build.sh && node build.js cross-platform",
|
||||
"build-native": "cd desktop_native && node build.js",
|
||||
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
||||
"build:dev": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\" \"npm run build:preload:dev\"",
|
||||
@@ -44,10 +45,9 @@
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
|
||||
"pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never",
|
||||
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",
|
||||
"pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never",
|
||||
"pack:mac:mas:with-extension": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never",
|
||||
"pack:mac:masdev": "npm run clean:dist && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:mac:masdev:with-extension": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:mac:mas": "npm run clean:dist && npm run build:macos-extension:mas && electron-builder --mac mas --universal -p never",
|
||||
"pack:mac:masdev": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never",
|
||||
"pack:local:mac": "npm run clean:dist && npm run build:macos-extension:masdev && electron-builder --mac mas-dev --universal -p never -c.mac.provisioningProfile=\"\" -c.mas.provisioningProfile=\"\"",
|
||||
"pack:win": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:beta": "npm run clean:dist && electron-builder --config electron-builder.beta.json --win --x64 --arm64 --ia32 -p never -c.win.signtoolOptions.certificateSubjectName=\"8bit Solutions LLC\"",
|
||||
"pack:win:ci": "npm run clean:dist && electron-builder --win --x64 --arm64 --ia32 -p never",
|
||||
@@ -55,11 +55,8 @@
|
||||
"dist:lin": "npm run build && npm run pack:lin",
|
||||
"dist:lin:arm64": "npm run build && npm run pack:lin:arm64",
|
||||
"dist:mac": "npm run build && npm run pack:mac",
|
||||
"dist:mac:with-extension": "npm run build && npm run pack:mac:with-extension",
|
||||
"dist:mac:mas": "npm run build && npm run pack:mac:mas",
|
||||
"dist:mac:mas:with-extension": "npm run build && npm run pack:mac:mas:with-extension",
|
||||
"dist:mac:masdev": "npm run build:dev && npm run pack:mac:masdev",
|
||||
"dist:mac:masdev:with-extension": "npm run build:dev && npm run pack:mac:masdev:with-extension",
|
||||
"dist:mac:masdev": "npm run build && npm run pack:mac:masdev",
|
||||
"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",
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>LTZ2PFU5D6</string>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>LTZ2PFU5D6</string>
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
|
||||
<array>
|
||||
@@ -36,7 +36,5 @@
|
||||
<string>/Library/Application Support/Zen/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/net.imput.helium</string>
|
||||
</array>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -16,7 +16,7 @@ async function run(context) {
|
||||
const appPath = `${context.appOutDir}/${appName}.app`;
|
||||
const macBuild = context.electronPlatformName === "darwin";
|
||||
const copySafariExtension = ["darwin", "mas"].includes(context.electronPlatformName);
|
||||
const copyAutofillExtension = ["darwin", "mas"].includes(context.electronPlatformName);
|
||||
const copyAutofillExtension = ["darwin"].includes(context.electronPlatformName); // Disabled for mas builds
|
||||
|
||||
let shouldResign = false;
|
||||
|
||||
|
||||
@@ -37,6 +37,6 @@ concurrently(
|
||||
{
|
||||
prefix: "name",
|
||||
outputStream: process.stdout,
|
||||
killOthers: ["success", "failure"],
|
||||
killOthersOn: ["success", "failure"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -34,6 +34,6 @@ concurrently(
|
||||
{
|
||||
prefix: "name",
|
||||
outputStream: process.stdout,
|
||||
killOthers: ["success", "failure"],
|
||||
killOthersOn: ["success", "failure"],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-session-timeout-input
|
||||
<bit-session-timeout-input-legacy
|
||||
[vaultTimeoutOptions]="vaultTimeoutOptions"
|
||||
[formControl]="form.controls.vaultTimeout"
|
||||
ngDefaultControl
|
||||
>
|
||||
</bit-session-timeout-input>
|
||||
</bit-session-timeout-input-legacy>
|
||||
|
||||
<bit-form-field disableMargin>
|
||||
<bit-label for="vaultTimeoutAction">{{
|
||||
|
||||
@@ -187,7 +187,6 @@ describe("SettingsComponent", () => {
|
||||
i18nService.userSetLocale$ = of("en");
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
policyService.policiesByType$.mockReturnValue(of([null]));
|
||||
desktopAutotypeService.resolvedAutotypeEnabled$ = of(false);
|
||||
desktopAutotypeService.autotypeEnabledUserSetting$ = of(false);
|
||||
desktopAutotypeService.autotypeKeyboardShortcut$ = of(["Control", "Shift", "B"]);
|
||||
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
|
||||
|
||||
@@ -55,7 +55,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
import {
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SessionTimeoutSettingsComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
@@ -97,7 +97,7 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
|
||||
SectionHeaderComponent,
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
SessionTimeoutInputComponent,
|
||||
SessionTimeoutInputLegacyComponent,
|
||||
SessionTimeoutSettingsComponent,
|
||||
PermitCipherDetailsPopoverComponent,
|
||||
PremiumBadgeComponent,
|
||||
|
||||
@@ -42,14 +42,20 @@ import {
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AnonLayoutWrapperComponent, AnonLayoutWrapperData } from "@bitwarden/components";
|
||||
import { LockComponent, ConfirmKeyConnectorDomainComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
LockComponent,
|
||||
ConfirmKeyConnectorDomainComponent,
|
||||
RemovePasswordComponent,
|
||||
} from "@bitwarden/key-management-ui";
|
||||
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { reactiveUnlockVaultGuard } from "../autofill/guards/reactive-vault-guard";
|
||||
import { Fido2CreateComponent } from "../autofill/modal/credentials/fido2-create.component";
|
||||
import { Fido2ExcludedCiphersComponent } from "../autofill/modal/credentials/fido2-excluded-ciphers.component";
|
||||
import { Fido2VaultComponent } from "../autofill/modal/credentials/fido2-vault.component";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
import { VaultComponent } from "../vault/app/vault-v3/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
import { DesktopLayoutComponent } from "./layout/desktop-layout.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
import { SendV2Component } from "./tools/send-v2/send-v2.component";
|
||||
@@ -115,17 +121,16 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
path: "fido2-assertion",
|
||||
component: Fido2VaultComponent,
|
||||
},
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
path: "fido2-creation",
|
||||
component: Fido2CreateComponent,
|
||||
},
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
path: "fido2-excluded",
|
||||
component: Fido2ExcludedCiphersComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
@@ -271,7 +276,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
canActivate: [lockGuard(), reactiveUnlockVaultGuard],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: {
|
||||
@@ -320,13 +325,24 @@ const routes: Routes = [
|
||||
pageIcon: LockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "remove-password",
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
pageIcon: LockIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "confirm-key-connector-domain",
|
||||
component: ConfirmKeyConnectorDomainComponent,
|
||||
canActivate: [],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "confirmKeyConnectorDomain",
|
||||
key: "verifyYourOrganization",
|
||||
},
|
||||
pageIcon: DomainIcon,
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
|
||||
@@ -104,7 +104,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
|
||||
<ng-template #exportVault></ng-template>
|
||||
<ng-template #appGenerator></ng-template>
|
||||
<ng-template #loginApproval></ng-template>
|
||||
<app-header></app-header>
|
||||
<app-header *ngIf="showHeader$ | async"></app-header>
|
||||
|
||||
<div id="container">
|
||||
<div class="loading" *ngIf="loading">
|
||||
@@ -141,6 +141,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
|
||||
loginApprovalModalRef: ViewContainerRef;
|
||||
|
||||
showHeader$ = this.accountService.showHeader$;
|
||||
loading = false;
|
||||
|
||||
private lastActivity: Date = null;
|
||||
|
||||
@@ -15,7 +15,6 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
|
||||
import { VaultFilterModule } from "../vault/app/vault/vault-filter/vault-filter.module";
|
||||
import { VaultV2Component } from "../vault/app/vault/vault-v2.component";
|
||||
|
||||
@@ -50,7 +49,6 @@ import { SharedModule } from "./shared/shared.module";
|
||||
ColorPasswordCountPipe,
|
||||
HeaderComponent,
|
||||
PremiumComponent,
|
||||
RemovePasswordComponent,
|
||||
SearchComponent,
|
||||
],
|
||||
providers: [SshAgentService],
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<div
|
||||
style="background:white; display:flex; justify-content: center; align-items: center; flex-direction: column"
|
||||
>
|
||||
<h1 style="color: black">Select your passkey</h1>
|
||||
|
||||
<div *ngFor="let item of cipherIds$ | async">
|
||||
<button
|
||||
style="color:black; padding: 10px 20px; border: 1px solid blue; margin: 10px"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
(click)="chooseCipher(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<button
|
||||
style="color:black; padding: 10px 20px; border: 1px solid black; margin: 10px"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
(click)="confirmPasskey()"
|
||||
>
|
||||
Confirm passkey
|
||||
</button>
|
||||
<button
|
||||
style="color:black; padding: 10px 20px; border: 1px solid black; margin: 10px"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
|
||||
cipherIds$: Observable<string[]>;
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
this.cipherIds$ = this.session?.availableCipherIds$;
|
||||
}
|
||||
|
||||
async chooseCipher(cipherId: string) {
|
||||
// For now: Set UV to true
|
||||
this.session?.confirmChosenCipher(cipherId, true);
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject
|
||||
}
|
||||
|
||||
async confirmPasskey() {
|
||||
try {
|
||||
// Retrieve the current UI session to control the flow
|
||||
if (!this.session) {
|
||||
// todo: handle error
|
||||
throw new Error("No session found");
|
||||
}
|
||||
|
||||
// If we want to we could submit information to the session in order to create the credential
|
||||
// const cipher = await session.createCredential({
|
||||
// userHandle: "userHandle2",
|
||||
// userName: "username2",
|
||||
// credentialName: "zxsd2",
|
||||
// rpId: "webauthn.io",
|
||||
// userVerification: true,
|
||||
// });
|
||||
|
||||
this.session.notifyConfirmNewCredential(true);
|
||||
|
||||
// Not sure this clean up should happen here or in session.
|
||||
// The session currently toggles modal on and send us here
|
||||
// But if this route is somehow opened outside of session we want to make sure we clean up?
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
} catch {
|
||||
// TODO: Handle error appropriately
|
||||
}
|
||||
}
|
||||
|
||||
async closeModal() {
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
|
||||
this.session.notifyConfirmNewCredential(false);
|
||||
// little bit hacky:
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<bit-layout>
|
||||
<bit-layout class="!tw-h-full">
|
||||
<app-side-nav slot="side-nav">
|
||||
<bit-nav-logo [openIcon]="logo" route="." [label]="'passwordManager' | i18n"></bit-nav-logo>
|
||||
|
||||
<bit-nav-item icon="bwi-vault" [text]="'vault' | i18n" route="new-vault"></bit-nav-item>
|
||||
<bit-nav-item icon="bwi-send" [text]="'send' | i18n" route="new-sends"></bit-nav-item>
|
||||
<app-send-filters-nav></app-send-filters-nav>
|
||||
</app-side-nav>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
@@ -5,8 +6,18 @@ import { mock } from "jest-mock-extended";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopLayoutComponent } from "./desktop-layout.component";
|
||||
|
||||
// Mock the child component to isolate DesktopLayoutComponent testing
|
||||
@Component({
|
||||
selector: "app-send-filters-nav",
|
||||
template: "",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
class MockSendFiltersNavComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
@@ -34,7 +45,12 @@ describe("DesktopLayoutComponent", () => {
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
remove: { imports: [SendFiltersNavComponent] },
|
||||
add: { imports: [MockSendFiltersNavComponent] },
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DesktopLayoutComponent);
|
||||
component = fixture.componentInstance;
|
||||
@@ -58,4 +74,11 @@ describe("DesktopLayoutComponent", () => {
|
||||
|
||||
expect(ngContent).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders send filters navigation component", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const sendFiltersNav = compiled.querySelector("app-send-filters-nav");
|
||||
|
||||
expect(sendFiltersNav).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,13 +5,22 @@ import { PasswordManagerLogo } from "@bitwarden/assets/svg";
|
||||
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-layout",
|
||||
imports: [RouterModule, I18nPipe, LayoutComponent, NavigationModule, DesktopSideNavComponent],
|
||||
imports: [
|
||||
RouterModule,
|
||||
I18nPipe,
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
DesktopSideNavComponent,
|
||||
SendFiltersNavComponent,
|
||||
],
|
||||
templateUrl: "./desktop-layout.component.html",
|
||||
})
|
||||
export class DesktopLayoutComponent {
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
@@ -62,6 +63,7 @@ import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypt
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
@@ -101,6 +103,7 @@ import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/s
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
KdfConfigService,
|
||||
@@ -128,7 +131,7 @@ import { DesktopBiometricsService } from "../../key-management/biometrics/deskto
|
||||
import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
|
||||
import { ElectronKeyService } from "../../key-management/electron-key.service";
|
||||
import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service";
|
||||
import { DesktopSessionTimeoutSettingsComponentService } from "../../key-management/session-timeout/services/desktop-session-timeout-settings-component.service";
|
||||
import { DesktopSessionTimeoutTypeService } from "../../key-management/session-timeout/services/desktop-session-timeout-type.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
|
||||
@@ -165,12 +168,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
deps: [TokenService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopBiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
deps: [TokenService],
|
||||
}),
|
||||
safeProvider(NativeMessagingService),
|
||||
safeProvider(BiometricMessageHandlerService),
|
||||
@@ -200,8 +203,16 @@ const safeProviders: SafeProvider[] = [
|
||||
// We manually override the value of SUPPORTS_SECURE_STORAGE here to avoid
|
||||
// the TokenService having to inject the PlatformUtilsService which introduces a
|
||||
// circular dependency on Desktop only.
|
||||
//
|
||||
// For Windows portable builds, we disable secure storage to ensure tokens are
|
||||
// stored on disk (in bitwarden-appdata) rather than in Windows Credential
|
||||
// Manager, making them portable across machines. This allows users to move the USB drive
|
||||
// between computers while maintaining authentication.
|
||||
//
|
||||
// Note: Portable mode does not use secure storage for read/write/clear operations,
|
||||
// preventing any collision with tokens from a regular desktop installation.
|
||||
provide: SUPPORTS_SECURE_STORAGE,
|
||||
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE,
|
||||
useValue: ELECTRON_SUPPORTS_SECURE_STORAGE && !ipc.platform.isWindowsPortable,
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DEFAULT_VAULT_TIMEOUT,
|
||||
@@ -344,6 +355,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ConfigService,
|
||||
Fido2AuthenticatorServiceAbstraction,
|
||||
AccountService,
|
||||
AuthService,
|
||||
PlatformUtilsService,
|
||||
],
|
||||
}),
|
||||
@@ -477,6 +489,7 @@ const safeProviders: SafeProvider[] = [
|
||||
PlatformUtilsServiceAbstraction,
|
||||
BillingAccountProfileStateService,
|
||||
DesktopAutotypeDefaultSettingPolicy,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -484,15 +497,20 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DesktopAutotypeDefaultSettingPolicy,
|
||||
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutTypeService,
|
||||
useClass: DesktopSessionTimeoutTypeService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SessionTimeoutSettingsComponentService,
|
||||
useClass: DesktopSessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
useClass: SessionTimeoutSettingsComponentService,
|
||||
deps: [I18nServiceAbstraction, SessionTimeoutTypeService, PolicyServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [JslibServicesModule],
|
||||
imports: [JslibServicesModule, GeneratorServicesModule],
|
||||
declarations: [],
|
||||
// Do not register your dependency here! Add it to the typesafeProviders array using the helper function
|
||||
providers: safeProviders,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<bit-dialog #dialog dialogSize="large">
|
||||
<span bitDialogTitle>{{ "exportVault" | i18n }}</span>
|
||||
<span bitDialogTitle>{{ "exportNoun" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<tools-export
|
||||
(formLoading)="this.loading = $event"
|
||||
@@ -17,7 +17,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "exportVault" | i18n }}
|
||||
{{ "exportVerb" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<bit-dialog #dialog dialogSize="large" background="alt">
|
||||
<span bitDialogTitle>{{ "importData" | i18n }}</span>
|
||||
<span bitDialogTitle>{{ "importNoun" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<tools-import
|
||||
(formLoading)="this.loading = $event"
|
||||
(formDisabled)="this.disabled = $event"
|
||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
></tools-import>
|
||||
<div class="tw-relative">
|
||||
<tools-import
|
||||
(formLoading)="this.loading = $event"
|
||||
(formDisabled)="this.disabled = $event"
|
||||
(onSuccessfulImport)="this.onSuccessfulImport($event)"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
[class.tw-invisible]="loading"
|
||||
></tools-import>
|
||||
@if (loading) {
|
||||
<div class="tw-absolute tw-inset-0 tw-flex tw-items-center tw-justify-center">
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x tw-text-primary-600" aria-hidden="true"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
@@ -19,7 +27,7 @@
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
>
|
||||
{{ "importData" | i18n }}
|
||||
{{ "importVerb" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<bit-nav-group
|
||||
icon="bwi-send"
|
||||
[text]="'send' | i18n"
|
||||
route="new-sends"
|
||||
(click)="selectTypeAndNavigate()"
|
||||
>
|
||||
<bit-nav-item
|
||||
icon="bwi-send"
|
||||
[text]="'allSends' | i18n"
|
||||
(click)="selectTypeAndNavigate(null); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === null"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-file-text"
|
||||
[text]="'sendTypeText' | i18n"
|
||||
(click)="selectTypeAndNavigate(SendType.Text); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === SendType.Text"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item
|
||||
icon="bwi-file"
|
||||
[text]="'sendTypeFile' | i18n"
|
||||
(click)="selectTypeAndNavigate(SendType.File); $event.stopPropagation()"
|
||||
[forceActiveStyles]="activeSendType() === SendType.File"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ChangeDetectionStrategy, Component } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { Router, provideRouter } from "@angular/router";
|
||||
import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import { SendFiltersNavComponent } from "./send-filters-nav.component";
|
||||
|
||||
@Component({ template: "", changeDetection: ChangeDetectionStrategy.OnPush })
|
||||
class DummyComponent {}
|
||||
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
describe("SendFiltersNavComponent", () => {
|
||||
let component: SendFiltersNavComponent;
|
||||
let fixture: ComponentFixture<SendFiltersNavComponent>;
|
||||
let harness: RouterTestingHarness;
|
||||
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let mockSendListFiltersService: Partial<SendListFiltersService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
|
||||
sendType: null,
|
||||
});
|
||||
|
||||
mockSendListFiltersService = {
|
||||
filterForm: {
|
||||
value: { sendType: null },
|
||||
valueChanges: filterFormValueSubject.asObservable(),
|
||||
patchValue: jest.fn((value) => {
|
||||
mockSendListFiltersService.filterForm.value = {
|
||||
...mockSendListFiltersService.filterForm.value,
|
||||
...value,
|
||||
};
|
||||
filterFormValueSubject.next(mockSendListFiltersService.filterForm.value);
|
||||
}),
|
||||
} as any,
|
||||
filters$: filterFormValueSubject.asObservable(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendFiltersNavComponent, NavigationModule],
|
||||
providers: [
|
||||
provideRouter([
|
||||
{ path: "vault", component: DummyComponent },
|
||||
{ path: "new-sends", component: DummyComponent },
|
||||
]),
|
||||
{
|
||||
provide: SendListFiltersService,
|
||||
useValue: mockSendListFiltersService,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
t: jest.fn((key) => key),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
// Create harness and navigate to initial route
|
||||
harness = await RouterTestingHarness.create("/vault");
|
||||
|
||||
// Create the component fixture separately (not a routed component)
|
||||
fixture = TestBed.createComponent(SendFiltersNavComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("creates component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders bit-nav-group with Send icon and text", () => {
|
||||
const compiled = fixture.nativeElement;
|
||||
const navGroup = compiled.querySelector("bit-nav-group");
|
||||
|
||||
expect(navGroup).toBeTruthy();
|
||||
expect(navGroup.getAttribute("icon")).toBe("bwi-send");
|
||||
});
|
||||
|
||||
it("component exposes SendType enum for template", () => {
|
||||
expect(component["SendType"]).toBe(SendType);
|
||||
});
|
||||
|
||||
describe("isSendRouteActive", () => {
|
||||
it("returns true when on /new-sends route", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["isSendRouteActive"]()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when not on /new-sends route", () => {
|
||||
expect(component["isSendRouteActive"]()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeSendType", () => {
|
||||
it("returns the active send type when on send route and filter type is set", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
|
||||
filterFormValueSubject.next({ sendType: SendType.Text });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBe(SendType.Text);
|
||||
});
|
||||
|
||||
it("returns undefined when not on send route", () => {
|
||||
mockSendListFiltersService.filterForm.value = { sendType: SendType.Text };
|
||||
filterFormValueSubject.next({ sendType: SendType.Text });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns null when on send route but no type is selected", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
mockSendListFiltersService.filterForm.value = { sendType: null };
|
||||
filterFormValueSubject.next({ sendType: null });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component["activeSendType"]()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectTypeAndNavigate", () => {
|
||||
it("clears the sendType filter when called with no parameter", async () => {
|
||||
await component["selectTypeAndNavigate"]();
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates filter form with Text type", async () => {
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("updates filter form with File type", async () => {
|
||||
await component["selectTypeAndNavigate"](SendType.File);
|
||||
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.File,
|
||||
});
|
||||
});
|
||||
|
||||
it("navigates to /new-sends when not on send route", async () => {
|
||||
expect(harness.routeNativeElement?.textContent).toBeDefined();
|
||||
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
const currentUrl = TestBed.inject(Router).url;
|
||||
expect(currentUrl).toBe("/new-sends");
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not navigate when already on send route (component is reactive)", async () => {
|
||||
await harness.navigateByUrl("/new-sends");
|
||||
const router = TestBed.inject(Router);
|
||||
const navigateSpy = jest.spyOn(router, "navigate");
|
||||
|
||||
await component["selectTypeAndNavigate"](SendType.Text);
|
||||
|
||||
expect(navigateSpy).not.toHaveBeenCalled();
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: SendType.Text,
|
||||
});
|
||||
});
|
||||
|
||||
it("navigates when clearing filter from different route", async () => {
|
||||
await component["selectTypeAndNavigate"](); // No parameter = clear filter
|
||||
|
||||
const currentUrl = TestBed.inject(Router).url;
|
||||
expect(currentUrl).toBe("/new-sends");
|
||||
expect(mockSendListFiltersService.filterForm.patchValue).toHaveBeenCalledWith({
|
||||
sendType: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, computed, inject } from "@angular/core";
|
||||
import { toSignal } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { filter, map, startWith } from "rxjs";
|
||||
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
/**
|
||||
* Navigation component that renders Send filter options in the sidebar.
|
||||
* Fully reactive using signals - no manual subscriptions or method-based computed values.
|
||||
* - Parent "Send" nav-group clears filter (shows all sends)
|
||||
* - Child "Text"/"File" items set filter to specific type
|
||||
* - Active states computed reactively from filter signal + route signal
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-send-filters-nav",
|
||||
templateUrl: "./send-filters-nav.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [CommonModule, NavigationModule, I18nPipe],
|
||||
})
|
||||
export class SendFiltersNavComponent {
|
||||
protected readonly SendType = SendType;
|
||||
private readonly filtersService = inject(SendListFiltersService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly currentFilter = toSignal(this.filtersService.filters$);
|
||||
|
||||
// Track whether current route is the send route
|
||||
private readonly isSendRouteActive = toSignal(
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map((event) => (event as NavigationEnd).urlAfterRedirects.includes("/new-sends")),
|
||||
startWith(this.router.url.includes("/new-sends")),
|
||||
),
|
||||
{ initialValue: this.router.url.includes("/new-sends") },
|
||||
);
|
||||
|
||||
// Computed: Active send type (null when on send route with no filter, undefined when not on send route)
|
||||
protected readonly activeSendType = computed(() => {
|
||||
return this.isSendRouteActive() ? this.currentFilter()?.sendType : undefined;
|
||||
});
|
||||
|
||||
// Update send filter and navigate to /new-sends (only if not already there - send-v2 component reacts to filter changes)
|
||||
protected async selectTypeAndNavigate(type?: SendType): Promise<void> {
|
||||
this.filtersService.filterForm.patchValue({ sendType: type !== undefined ? type : null });
|
||||
|
||||
if (!this.router.url.includes("/new-sends")) {
|
||||
await this.router.navigate(["/new-sends"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
@@ -15,6 +19,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import * as utils from "../../../utils";
|
||||
import { SearchBarService } from "../../layout/search/search-bar.service";
|
||||
@@ -35,6 +40,8 @@ describe("SendV2Component", () => {
|
||||
let broadcasterService: MockProxy<BroadcasterService>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
let sendListFiltersService: SendListFiltersService;
|
||||
let changeDetectorRef: MockProxy<ChangeDetectorRef>;
|
||||
|
||||
beforeEach(async () => {
|
||||
sendService = mock<SendService>();
|
||||
@@ -42,6 +49,13 @@ describe("SendV2Component", () => {
|
||||
broadcasterService = mock<BroadcasterService>();
|
||||
accountService = mock<AccountService>();
|
||||
policyService = mock<PolicyService>();
|
||||
changeDetectorRef = mock<ChangeDetectorRef>();
|
||||
|
||||
// Create real SendListFiltersService with mocked dependencies
|
||||
const formBuilder = new FormBuilder();
|
||||
const i18nService = mock<I18nService>();
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
sendListFiltersService = new SendListFiltersService(i18nService, formBuilder);
|
||||
|
||||
// Mock sendViews$ observable
|
||||
sendService.sendViews$ = of([]);
|
||||
@@ -51,6 +65,10 @@ describe("SendV2Component", () => {
|
||||
accountService.activeAccount$ = of({ id: "test-user-id" } as any);
|
||||
policyService.policyAppliesToUser$ = jest.fn().mockReturnValue(of(false));
|
||||
|
||||
// Mock SearchService methods needed by base component
|
||||
const mockSearchService = mock<SearchService>();
|
||||
mockSearchService.isSearchable.mockResolvedValue(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SendV2Component],
|
||||
providers: [
|
||||
@@ -59,7 +77,7 @@ describe("SendV2Component", () => {
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
|
||||
{ provide: BroadcasterService, useValue: broadcasterService },
|
||||
{ provide: SearchService, useValue: mock<SearchService>() },
|
||||
{ provide: SearchService, useValue: mockSearchService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: SearchBarService, useValue: searchBarService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
@@ -67,6 +85,8 @@ describe("SendV2Component", () => {
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: SendListFiltersService, useValue: sendListFiltersService },
|
||||
{ provide: ChangeDetectorRef, useValue: changeDetectorRef },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -331,7 +351,6 @@ describe("SendV2Component", () => {
|
||||
describe("load", () => {
|
||||
it("sets loading states correctly", async () => {
|
||||
jest.spyOn(component, "search").mockResolvedValue();
|
||||
jest.spyOn(component, "selectAll");
|
||||
|
||||
expect(component.loaded).toBeFalsy();
|
||||
|
||||
@@ -341,14 +360,17 @@ describe("SendV2Component", () => {
|
||||
expect(component.loaded).toBe(true);
|
||||
});
|
||||
|
||||
it("calls selectAll when onSuccessfulLoad is not set", async () => {
|
||||
it("sets up sendViews$ subscription", async () => {
|
||||
const mockSends = [new SendView(), new SendView()];
|
||||
sendService.sendViews$ = of(mockSends);
|
||||
jest.spyOn(component, "search").mockResolvedValue();
|
||||
jest.spyOn(component, "selectAll");
|
||||
component.onSuccessfulLoad = null;
|
||||
|
||||
await component.load();
|
||||
|
||||
expect(component.selectAll).toHaveBeenCalled();
|
||||
// Give observable time to emit
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(component.sends).toEqual(mockSends);
|
||||
});
|
||||
|
||||
it("calls onSuccessfulLoad when it is set", async () => {
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit, OnDestroy, ViewChild, NgZone, ChangeDetectorRef } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { mergeMap } from "rxjs";
|
||||
import { mergeMap, Subscription } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
@@ -14,11 +15,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { SendView } from "@bitwarden/common/tools/send/models/view/send.view";
|
||||
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
|
||||
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
|
||||
import { invokeMenu, RendererMenuItem } from "../../../utils";
|
||||
import { SearchBarService } from "../../layout/search/search-bar.service";
|
||||
@@ -55,6 +58,9 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
// Tracks the current UI state: viewing list (None), adding new Send (Add), or editing existing Send (Edit)
|
||||
action: Action = Action.None;
|
||||
|
||||
// Subscription for sendViews$ cleanup
|
||||
private sendViewsSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
sendService: SendService,
|
||||
i18nService: I18nService,
|
||||
@@ -71,6 +77,7 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
toastService: ToastService,
|
||||
accountService: AccountService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private sendListFiltersService: SendListFiltersService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -88,12 +95,17 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
);
|
||||
|
||||
// Listen to search bar changes and update the Send list filter
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.searchBarService.searchText$.subscribe((searchText) => {
|
||||
this.searchBarService.searchText$.pipe(takeUntilDestroyed()).subscribe((searchText) => {
|
||||
this.searchText = searchText;
|
||||
this.searchTextChanged();
|
||||
setTimeout(() => this.cdr.detectChanges(), 250);
|
||||
});
|
||||
|
||||
// Listen to filter changes from sidebar navigation
|
||||
this.sendListFiltersService.filterForm.valueChanges
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((filters) => {
|
||||
this.applySendTypeFilter(filters);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize the component: enable search bar, subscribe to sync events, and load Send items
|
||||
@@ -103,6 +115,10 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
|
||||
await super.ngOnInit();
|
||||
|
||||
// Read current filter synchronously to avoid race condition on navigation
|
||||
const currentFilter = this.sendListFiltersService.filterForm.value;
|
||||
this.applySendTypeFilter(currentFilter);
|
||||
|
||||
// Listen for sync completion events to refresh the Send list
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
@@ -118,8 +134,18 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
await this.load();
|
||||
}
|
||||
|
||||
// Apply send type filter to display: centralized logic for initial load and filter changes
|
||||
private applySendTypeFilter(filters: Partial<{ sendType: SendType | null }>): void {
|
||||
if (filters.sendType === null || filters.sendType === undefined) {
|
||||
this.selectAll();
|
||||
} else {
|
||||
this.selectType(filters.sendType);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up subscriptions and disable search bar when component is destroyed
|
||||
ngOnDestroy() {
|
||||
this.sendViewsSubscription?.unsubscribe();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
this.searchBarService.setEnabled(false);
|
||||
}
|
||||
@@ -130,7 +156,12 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
// Note: The filter parameter is ignored in this implementation for desktop-specific behavior.
|
||||
async load(filter: (send: SendView) => boolean = null) {
|
||||
this.loading = true;
|
||||
this.sendService.sendViews$
|
||||
|
||||
// Recreate subscription on each load (required for sync refresh)
|
||||
// Manual cleanup in ngOnDestroy is intentional - load() is called multiple times
|
||||
this.sendViewsSubscription?.unsubscribe();
|
||||
|
||||
this.sendViewsSubscription = this.sendService.sendViews$
|
||||
.pipe(
|
||||
mergeMap(async (sends) => {
|
||||
this.sends = sends;
|
||||
@@ -143,9 +174,6 @@ export class SendV2Component extends BaseSendComponent implements OnInit, OnDest
|
||||
.subscribe();
|
||||
if (this.onSuccessfulLoad != null) {
|
||||
await this.onSuccessfulLoad();
|
||||
} else {
|
||||
// Default action
|
||||
this.selectAll();
|
||||
}
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow *ngIf="editMode && type === sendType.File">
|
||||
<label for="file">{{ "file" | i18n }}</label>
|
||||
<div class="row-main">{{ send.file.fileName }} ({{ send.file.sizeName }})</div>
|
||||
<div class="row-main tw-text-wrap tw-break-all">
|
||||
{{ send.file.fileName }} ({{ send.file.sizeName }})
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow *ngIf="type === sendType.Text">
|
||||
<label for="text">{{ "text" | i18n }}</label>
|
||||
|
||||
@@ -136,6 +136,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
@@ -145,4 +146,55 @@ describe("DesktopLoginComponentService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
|
||||
// Array of all permutations of isAppImage and isDev
|
||||
const permutations = [
|
||||
[true, false], // Case 1: isAppImage true
|
||||
[false, true], // Case 2: isDev true
|
||||
[true, true], // Case 3: all true
|
||||
[false, false], // Case 4: all false
|
||||
];
|
||||
|
||||
permutations.forEach(([isAppImage, isDev]) => {
|
||||
it("calls redirectToSso with orgSsoIdentifier", async () => {
|
||||
(global as any).ipc.platform.isAppImage = isAppImage;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const orgSsoIdentifier = "orgSsoId";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
|
||||
|
||||
if (isAppImage || isDev) {
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
} else {
|
||||
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,11 +48,12 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
// For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback
|
||||
// Otherwise, we launch the SSO component in a browser window and wait for the callback
|
||||
if (ipc.platform.isAppImage || ipc.platform.isDev) {
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge, orgSsoIdentifier);
|
||||
} else {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
@@ -66,6 +67,7 @@ export class DesktopLoginComponentService
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(ssoWebAppUrl);
|
||||
@@ -76,9 +78,15 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
challenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(challenge, state, email);
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(
|
||||
challenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
|
||||
42
apps/desktop/src/autofill/guards/reactive-vault-guard.ts
Normal file
42
apps/desktop/src/autofill/guards/reactive-vault-guard.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { CanActivateFn, Router } from "@angular/router";
|
||||
import { combineLatest, map, switchMap, distinctUntilChanged } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
||||
/**
|
||||
* Reactive route guard that redirects to the unlocked vault.
|
||||
* Redirects to vault when unlocked in main window.
|
||||
*/
|
||||
export const reactiveUnlockVaultGuard: CanActivateFn = () => {
|
||||
const router = inject(Router);
|
||||
const authService = inject(AuthService);
|
||||
const accountService = inject(AccountService);
|
||||
const desktopSettingsService = inject(DesktopSettingsService);
|
||||
|
||||
return combineLatest([accountService.activeAccount$, desktopSettingsService.modalMode$]).pipe(
|
||||
switchMap(([account, modalMode]) => {
|
||||
if (!account) {
|
||||
return [true];
|
||||
}
|
||||
|
||||
// Monitor when the vault has been unlocked.
|
||||
return authService.authStatusFor$(account.id).pipe(
|
||||
distinctUntilChanged(),
|
||||
map((authStatus) => {
|
||||
// If vault is unlocked and we're not in modal mode, redirect to vault
|
||||
if (authStatus === AuthenticationStatus.Unlocked && !modalMode?.isModalModeActive) {
|
||||
return router.createUrlTree(["/vault"]);
|
||||
}
|
||||
|
||||
// Otherwise keep user on the lock screen
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -5,51 +5,46 @@ import { LogService } from "@bitwarden/logging";
|
||||
|
||||
import { WindowMain } from "../../main/window.main";
|
||||
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
|
||||
import { AutotypeConfig } from "../models/autotype-config";
|
||||
import { AutotypeMatchError } from "../models/autotype-errors";
|
||||
import { AutotypeVaultData } from "../models/autotype-vault-data";
|
||||
import { AUTOTYPE_IPC_CHANNELS } from "../models/ipc-channels";
|
||||
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
|
||||
|
||||
export class MainDesktopAutotypeService {
|
||||
autotypeKeyboardShortcut: AutotypeKeyboardShortcut;
|
||||
private autotypeKeyboardShortcut: AutotypeKeyboardShortcut;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {
|
||||
this.autotypeKeyboardShortcut = new AutotypeKeyboardShortcut();
|
||||
|
||||
this.registerIpcListeners();
|
||||
}
|
||||
|
||||
init() {
|
||||
ipcMain.on("autofill.configureAutotype", (event, data) => {
|
||||
if (data.enabled) {
|
||||
const newKeyboardShortcut = new AutotypeKeyboardShortcut();
|
||||
const newKeyboardShortcutIsValid = newKeyboardShortcut.set(data.keyboardShortcut);
|
||||
|
||||
if (newKeyboardShortcutIsValid) {
|
||||
this.disableAutotype();
|
||||
this.autotypeKeyboardShortcut = newKeyboardShortcut;
|
||||
this.enableAutotype();
|
||||
} else {
|
||||
this.logService.error(
|
||||
"Attempting to configure autotype but the shortcut given is invalid.",
|
||||
);
|
||||
}
|
||||
registerIpcListeners() {
|
||||
ipcMain.on(AUTOTYPE_IPC_CHANNELS.TOGGLE, (_event, enable: boolean) => {
|
||||
if (enable) {
|
||||
this.enableAutotype();
|
||||
} else {
|
||||
this.disableAutotype();
|
||||
|
||||
// Deregister the incoming keyboard shortcut if needed
|
||||
const setCorrectly = this.autotypeKeyboardShortcut.set(data.keyboardShortcut);
|
||||
if (
|
||||
setCorrectly &&
|
||||
globalShortcut.isRegistered(this.autotypeKeyboardShortcut.getElectronFormat())
|
||||
) {
|
||||
globalShortcut.unregister(this.autotypeKeyboardShortcut.getElectronFormat());
|
||||
this.logService.info("Autotype disabled.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeAutotypeRequest", (_event, vaultData: AutotypeVaultData) => {
|
||||
ipcMain.on(AUTOTYPE_IPC_CHANNELS.CONFIGURE, (_event, config: AutotypeConfig) => {
|
||||
const newKeyboardShortcut = new AutotypeKeyboardShortcut();
|
||||
const newKeyboardShortcutIsValid = newKeyboardShortcut.set(config.keyboardShortcut);
|
||||
|
||||
if (!newKeyboardShortcutIsValid) {
|
||||
this.logService.error("Configure autotype failed: the keyboard shortcut is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setKeyboardShortcut(newKeyboardShortcut);
|
||||
});
|
||||
|
||||
ipcMain.on(AUTOTYPE_IPC_CHANNELS.EXECUTE, (_event, vaultData: AutotypeVaultData) => {
|
||||
if (
|
||||
stringIsNotUndefinedNullAndEmpty(vaultData.username) &&
|
||||
stringIsNotUndefinedNullAndEmpty(vaultData.password)
|
||||
@@ -67,30 +62,74 @@ export class MainDesktopAutotypeService {
|
||||
});
|
||||
}
|
||||
|
||||
// Deregister the keyboard shortcut if registered.
|
||||
disableAutotype() {
|
||||
// Deregister the current keyboard shortcut if needed
|
||||
const formattedKeyboardShortcut = this.autotypeKeyboardShortcut.getElectronFormat();
|
||||
|
||||
if (globalShortcut.isRegistered(formattedKeyboardShortcut)) {
|
||||
globalShortcut.unregister(formattedKeyboardShortcut);
|
||||
this.logService.info("Autotype disabled.");
|
||||
this.logService.debug("Autotype disabled.");
|
||||
} else {
|
||||
this.logService.debug("Autotype is not registered, implicitly disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
ipcMain.removeAllListeners(AUTOTYPE_IPC_CHANNELS.TOGGLE);
|
||||
ipcMain.removeAllListeners(AUTOTYPE_IPC_CHANNELS.CONFIGURE);
|
||||
ipcMain.removeAllListeners(AUTOTYPE_IPC_CHANNELS.EXECUTE);
|
||||
|
||||
// Also unregister the global shortcut
|
||||
this.disableAutotype();
|
||||
}
|
||||
|
||||
// Register the current keyboard shortcut if not already registered.
|
||||
private enableAutotype() {
|
||||
const formattedKeyboardShortcut = this.autotypeKeyboardShortcut.getElectronFormat();
|
||||
if (globalShortcut.isRegistered(formattedKeyboardShortcut)) {
|
||||
this.logService.debug(
|
||||
"Autotype is already enabled with this keyboard shortcut: " + formattedKeyboardShortcut,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = globalShortcut.register(
|
||||
this.autotypeKeyboardShortcut.getElectronFormat(),
|
||||
() => {
|
||||
const windowTitle = autotype.getForegroundWindowTitle();
|
||||
|
||||
this.windowMain.win.webContents.send("autofill.listenAutotypeRequest", {
|
||||
this.windowMain.win.webContents.send(AUTOTYPE_IPC_CHANNELS.LISTEN, {
|
||||
windowTitle,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
result
|
||||
? this.logService.info("Autotype enabled.")
|
||||
: this.logService.info("Enabling autotype failed.");
|
||||
? this.logService.debug("Autotype enabled.")
|
||||
: this.logService.error("Failed to enable Autotype.");
|
||||
}
|
||||
|
||||
// Set the keyboard shortcut if it differs from the present one. If
|
||||
// the keyboard shortcut is set, de-register the old shortcut first.
|
||||
private setKeyboardShortcut(keyboardShortcut: AutotypeKeyboardShortcut) {
|
||||
if (
|
||||
keyboardShortcut.getElectronFormat() !== this.autotypeKeyboardShortcut.getElectronFormat()
|
||||
) {
|
||||
const registered = globalShortcut.isRegistered(
|
||||
this.autotypeKeyboardShortcut.getElectronFormat(),
|
||||
);
|
||||
if (registered) {
|
||||
this.disableAutotype();
|
||||
}
|
||||
this.autotypeKeyboardShortcut = keyboardShortcut;
|
||||
if (registered) {
|
||||
this.enableAutotype();
|
||||
}
|
||||
} else {
|
||||
this.logService.debug(
|
||||
"setKeyboardShortcut() called but shortcut is not different from current.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private doAutotype(vaultData: AutotypeVaultData, keyboardShortcut: string[]) {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class MainSshAgentService {
|
||||
init() {
|
||||
// handle sign request passing to UI
|
||||
sshagent
|
||||
.serve(async (err: Error, sshUiRequest: sshagent.SshUiRequest) => {
|
||||
.serve(async (err: Error | null, sshUiRequest: sshagent.SshUiRequest): Promise<boolean> => {
|
||||
// clear all old (> SIGN_TIMEOUT) requests
|
||||
this.requestResponses = this.requestResponses.filter(
|
||||
(response) => response.timestamp > new Date(Date.now() - this.SIGN_TIMEOUT),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full tw-bg-background-alt">
|
||||
<bit-section
|
||||
disableMargin
|
||||
class="tw-sticky tw-top-0 tw-z-10 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">
|
||||
{{ "savePasskeyQuestion" | i18n }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
slot="end"
|
||||
class="tw-app-region-no-drag tw-mb-4 tw-mr-2"
|
||||
(click)="closeModal()"
|
||||
[label]="'close' | i18n"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</bit-section-header>
|
||||
</bit-section>
|
||||
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col">
|
||||
<div *ngIf="(ciphers$ | async)?.length === 0; else hasCiphers">
|
||||
<div class="tw-flex tw-items-center tw-flex-col tw-p-12 tw-gap-4">
|
||||
<bit-icon [icon]="Icons.NoResults" class="tw-text-main"></bit-icon>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
{{ "noMatchingLoginsForSite" | i18n }}
|
||||
</div>
|
||||
<button bitButton type="button" buttonType="primary" (click)="confirmPasskey()">
|
||||
{{ "savePasskeyNewLogin" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #hasCiphers>
|
||||
<bit-item *ngFor="let c of ciphers$ | async" class="">
|
||||
<button type="button" bit-item-content (click)="addCredentialToCipher(c)">
|
||||
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
|
||||
<button bitLink [title]="c.name" type="button">
|
||||
{{ c.name }}
|
||||
</button>
|
||||
<span slot="secondary">{{ c.subTitle }}</span>
|
||||
<span bitBadge slot="end">{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</bit-item>
|
||||
<bit-item class="">
|
||||
<button
|
||||
bitLink
|
||||
linkType="primary"
|
||||
type="button"
|
||||
bit-item-content
|
||||
(click)="confirmPasskey()"
|
||||
>
|
||||
<a bitLink linkType="primary" class="tw-font-medium tw-text-base">
|
||||
{{ "saveNewPasskey" | i18n }}
|
||||
</a>
|
||||
</button>
|
||||
</bit-item>
|
||||
</ng-template>
|
||||
</bit-section>
|
||||
</div>
|
||||
@@ -0,0 +1,240 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
import { Fido2CreateComponent } from "./fido2-create.component";
|
||||
|
||||
describe("Fido2CreateComponent", () => {
|
||||
let component: Fido2CreateComponent;
|
||||
let mockDesktopSettingsService: MockProxy<DesktopSettingsService>;
|
||||
let mockFido2UserInterfaceService: MockProxy<DesktopFido2UserInterfaceService>;
|
||||
let mockAccountService: MockProxy<AccountService>;
|
||||
let mockCipherService: MockProxy<CipherService>;
|
||||
let mockDesktopAutofillService: MockProxy<DesktopAutofillService>;
|
||||
let mockDialogService: MockProxy<DialogService>;
|
||||
let mockDomainSettingsService: MockProxy<DomainSettingsService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockPasswordRepromptService: MockProxy<PasswordRepromptService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockSession: MockProxy<DesktopFido2UserInterfaceSession>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
const activeAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: "test-user-id" as UserId,
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDesktopSettingsService = mock<DesktopSettingsService>();
|
||||
mockFido2UserInterfaceService = mock<DesktopFido2UserInterfaceService>();
|
||||
mockAccountService = mock<AccountService>();
|
||||
mockCipherService = mock<CipherService>();
|
||||
mockDesktopAutofillService = mock<DesktopAutofillService>();
|
||||
mockDialogService = mock<DialogService>();
|
||||
mockDomainSettingsService = mock<DomainSettingsService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockPasswordRepromptService = mock<PasswordRepromptService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockSession = mock<DesktopFido2UserInterfaceSession>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession);
|
||||
mockAccountService.activeAccount$ = activeAccountSubject;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
providers: [
|
||||
Fido2CreateComponent,
|
||||
{ provide: DesktopSettingsService, useValue: mockDesktopSettingsService },
|
||||
{ provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: CipherService, useValue: mockCipherService },
|
||||
{ provide: DesktopAutofillService, useValue: mockDesktopAutofillService },
|
||||
{ provide: DialogService, useValue: mockDialogService },
|
||||
{ provide: DomainSettingsService, useValue: mockDomainSettingsService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: PasswordRepromptService, useValue: mockPasswordRepromptService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
component = TestBed.inject(Fido2CreateComponent);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
function createMockCiphers(): CipherView[] {
|
||||
const cipher1 = new CipherView();
|
||||
cipher1.id = "cipher-1";
|
||||
cipher1.name = "Test Cipher 1";
|
||||
cipher1.type = CipherType.Login;
|
||||
cipher1.login = {
|
||||
username: "test1@example.com",
|
||||
uris: [{ uri: "https://example.com", match: null }],
|
||||
matchesUri: jest.fn().mockReturnValue(true),
|
||||
get hasFido2Credentials() {
|
||||
return false;
|
||||
},
|
||||
} as any;
|
||||
cipher1.reprompt = CipherRepromptType.None;
|
||||
cipher1.deletedDate = null;
|
||||
|
||||
return [cipher1];
|
||||
}
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
beforeEach(() => {
|
||||
mockSession.getRpId.mockResolvedValue("example.com");
|
||||
Object.defineProperty(mockDesktopAutofillService, "lastRegistrationRequest", {
|
||||
get: jest.fn().mockReturnValue({
|
||||
userHandle: new Uint8Array([1, 2, 3]),
|
||||
}),
|
||||
configurable: true,
|
||||
});
|
||||
mockDomainSettingsService.getUrlEquivalentDomains.mockReturnValue(of(new Set<string>()));
|
||||
});
|
||||
|
||||
it("should initialize session and set show header to false", async () => {
|
||||
const mockCiphers = createMockCiphers();
|
||||
mockCipherService.getAllDecrypted.mockResolvedValue(mockCiphers);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled();
|
||||
expect(component.session).toBe(mockSession);
|
||||
});
|
||||
|
||||
it("should show error dialog when no active session found", async () => {
|
||||
mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null);
|
||||
mockDialogService.openSimpleDialog.mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({
|
||||
title: { key: "unableToSavePasskey" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeThisWindow" },
|
||||
acceptAction: expect.any(Function),
|
||||
cancelButtonText: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("addCredentialToCipher", () => {
|
||||
beforeEach(() => {
|
||||
component.session = mockSession;
|
||||
});
|
||||
|
||||
it("should add passkey to cipher", async () => {
|
||||
const cipher = createMockCiphers()[0];
|
||||
|
||||
await component.addCredentialToCipher(cipher);
|
||||
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher);
|
||||
});
|
||||
|
||||
it("should not add passkey when password reprompt is cancelled", async () => {
|
||||
const cipher = createMockCiphers()[0];
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false);
|
||||
|
||||
await component.addCredentialToCipher(cipher);
|
||||
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher);
|
||||
});
|
||||
|
||||
it("should call openSimpleDialog when cipher already has a fido2 credential", async () => {
|
||||
const cipher = createMockCiphers()[0];
|
||||
Object.defineProperty(cipher.login, "hasFido2Credentials", {
|
||||
get: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockDialogService.openSimpleDialog.mockResolvedValue(true);
|
||||
|
||||
await component.addCredentialToCipher(cipher);
|
||||
|
||||
expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({
|
||||
title: { key: "overwritePasskey" },
|
||||
content: { key: "alreadyContainsPasskey" },
|
||||
type: "warning",
|
||||
});
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true, cipher);
|
||||
});
|
||||
|
||||
it("should not add passkey when user cancels overwrite dialog", async () => {
|
||||
const cipher = createMockCiphers()[0];
|
||||
Object.defineProperty(cipher.login, "hasFido2Credentials", {
|
||||
get: jest.fn().mockReturnValue(true),
|
||||
});
|
||||
mockDialogService.openSimpleDialog.mockResolvedValue(false);
|
||||
|
||||
await component.addCredentialToCipher(cipher);
|
||||
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false, cipher);
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirmPasskey", () => {
|
||||
beforeEach(() => {
|
||||
component.session = mockSession;
|
||||
});
|
||||
|
||||
it("should confirm passkey creation successfully", async () => {
|
||||
await component.confirmPasskey();
|
||||
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("should call openSimpleDialog when session is null", async () => {
|
||||
component.session = null;
|
||||
mockDialogService.openSimpleDialog.mockResolvedValue(false);
|
||||
|
||||
await component.confirmPasskey();
|
||||
|
||||
expect(mockDialogService.openSimpleDialog).toHaveBeenCalledWith({
|
||||
title: { key: "unableToSavePasskey" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeThisWindow" },
|
||||
acceptAction: expect.any(Function),
|
||||
cancelButtonText: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeModal", () => {
|
||||
it("should close modal and notify session", async () => {
|
||||
component.session = mockSession;
|
||||
|
||||
await component.closeModal();
|
||||
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false);
|
||||
expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { RouterModule, Router } from "@angular/router";
|
||||
import { combineLatest, map, Observable, Subject, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield, NoResults } from "@bitwarden/assets/svg";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DialogService,
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopAutofillService } from "../../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
ItemModule,
|
||||
BadgeModule,
|
||||
],
|
||||
templateUrl: "fido2-create.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Fido2CreateComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
ciphers$: Observable<CipherView[]>;
|
||||
private destroy$ = new Subject<void>();
|
||||
readonly Icons = { BitwardenShield, NoResults };
|
||||
|
||||
private get DIALOG_MESSAGES() {
|
||||
return {
|
||||
unexpectedErrorShort: {
|
||||
title: { key: "unexpectedErrorShort" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeThisWindow" },
|
||||
cancelButtonText: null as null,
|
||||
acceptAction: async () => this.dialogService.closeAll(),
|
||||
},
|
||||
unableToSavePasskey: {
|
||||
title: { key: "unableToSavePasskey" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeThisWindow" },
|
||||
cancelButtonText: null as null,
|
||||
acceptAction: async () => this.dialogService.closeAll(),
|
||||
},
|
||||
overwritePasskey: {
|
||||
title: { key: "overwritePasskey" },
|
||||
content: { key: "alreadyContainsPasskey" },
|
||||
type: "warning",
|
||||
},
|
||||
} as const satisfies Record<string, SimpleDialogOptions>;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly desktopAutofillService: DesktopAutofillService,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly domainSettingsService: DomainSettingsService,
|
||||
private readonly passwordRepromptService: PasswordRepromptService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
|
||||
if (this.session) {
|
||||
const rpid = await this.session.getRpId();
|
||||
this.initializeCiphersObservable(rpid);
|
||||
} else {
|
||||
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
await this.closeModal();
|
||||
}
|
||||
|
||||
async addCredentialToCipher(cipher: CipherView): Promise<void> {
|
||||
const isConfirmed = await this.validateCipherAccess(cipher);
|
||||
|
||||
try {
|
||||
if (!this.session) {
|
||||
throw new Error("Missing session");
|
||||
}
|
||||
|
||||
this.session.notifyConfirmCreateCredential(isConfirmed, cipher);
|
||||
} catch {
|
||||
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.closeModal();
|
||||
}
|
||||
|
||||
async confirmPasskey(): Promise<void> {
|
||||
try {
|
||||
if (!this.session) {
|
||||
throw new Error("Missing session");
|
||||
}
|
||||
|
||||
this.session.notifyConfirmCreateCredential(true);
|
||||
} catch {
|
||||
await this.showErrorDialog(this.DIALOG_MESSAGES.unableToSavePasskey);
|
||||
}
|
||||
|
||||
await this.closeModal();
|
||||
}
|
||||
|
||||
async closeModal(): Promise<void> {
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.accountService.setShowHeader(true);
|
||||
|
||||
if (this.session) {
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
|
||||
private initializeCiphersObservable(rpid: string): void {
|
||||
const lastRegistrationRequest = this.desktopAutofillService.lastRegistrationRequest;
|
||||
|
||||
if (!lastRegistrationRequest || !rpid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userHandle = Fido2Utils.bufferToString(
|
||||
new Uint8Array(lastRegistrationRequest.userHandle),
|
||||
);
|
||||
|
||||
this.ciphers$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
this.domainSettingsService.getUrlEquivalentDomains(rpid),
|
||||
]).pipe(
|
||||
switchMap(async ([activeUserId, equivalentDomains]) => {
|
||||
if (!activeUserId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const allCiphers = await this.cipherService.getAllDecrypted(activeUserId);
|
||||
return allCiphers.filter(
|
||||
(cipher) =>
|
||||
cipher != null &&
|
||||
cipher.type == CipherType.Login &&
|
||||
cipher.login?.matchesUri(rpid, equivalentDomains) &&
|
||||
Fido2Utils.cipherHasNoOtherPasskeys(cipher, userHandle) &&
|
||||
!cipher.deletedDate,
|
||||
);
|
||||
} catch {
|
||||
await this.showErrorDialog(this.DIALOG_MESSAGES.unexpectedErrorShort);
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async validateCipherAccess(cipher: CipherView): Promise<boolean> {
|
||||
if (cipher.login.hasFido2Credentials) {
|
||||
const overwriteConfirmed = await this.dialogService.openSimpleDialog(
|
||||
this.DIALOG_MESSAGES.overwritePasskey,
|
||||
);
|
||||
|
||||
if (!overwriteConfirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (cipher.reprompt) {
|
||||
return this.passwordRepromptService.showPasswordPrompt();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async showErrorDialog(config: SimpleDialogOptions): Promise<void> {
|
||||
await this.dialogService.openSimpleDialog(config);
|
||||
await this.closeModal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full">
|
||||
<bit-section
|
||||
disableMargin
|
||||
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">
|
||||
{{ "savePasskeyQuestion" | i18n }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
slot="end"
|
||||
class="tw-app-region-no-drag tw-mb-4 tw-mr-2"
|
||||
(click)="closeModal()"
|
||||
[label]="'close' | i18n"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</bit-section-header>
|
||||
</bit-section>
|
||||
|
||||
<div class="tw-h-full tw-items-start">
|
||||
<bit-section
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-start tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-flex tw-items-center tw-flex-col tw-p-12 tw-gap-4">
|
||||
<bit-icon [icon]="Icons.NoResults" class="tw-text-main"></bit-icon>
|
||||
<div class="tw-flex tw-flex-col tw-gap-2">
|
||||
<b>{{ "passkeyAlreadyExists" | i18n }}</b>
|
||||
{{ "applicationDoesNotSupportDuplicates" | i18n }}
|
||||
</div>
|
||||
<button bitButton type="button" buttonType="primary" (click)="closeModal()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
import { Fido2ExcludedCiphersComponent } from "./fido2-excluded-ciphers.component";
|
||||
|
||||
describe("Fido2ExcludedCiphersComponent", () => {
|
||||
let component: Fido2ExcludedCiphersComponent;
|
||||
let fixture: ComponentFixture<Fido2ExcludedCiphersComponent>;
|
||||
let mockDesktopSettingsService: MockProxy<DesktopSettingsService>;
|
||||
let mockFido2UserInterfaceService: MockProxy<DesktopFido2UserInterfaceService>;
|
||||
let mockAccountService: MockProxy<AccountService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockSession: MockProxy<DesktopFido2UserInterfaceSession>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDesktopSettingsService = mock<DesktopSettingsService>();
|
||||
mockFido2UserInterfaceService = mock<DesktopFido2UserInterfaceService>();
|
||||
mockAccountService = mock<AccountService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockSession = mock<DesktopFido2UserInterfaceSession>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Fido2ExcludedCiphersComponent],
|
||||
providers: [
|
||||
{ provide: DesktopSettingsService, useValue: mockDesktopSettingsService },
|
||||
{ provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Fido2ExcludedCiphersComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
it("should initialize session", async () => {
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled();
|
||||
expect(component.session).toBe(mockSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeModal", () => {
|
||||
it("should close modal and notify session when session exists", async () => {
|
||||
component.session = mockSession;
|
||||
|
||||
await component.closeModal();
|
||||
|
||||
expect(mockDesktopSettingsService.setModalMode).toHaveBeenCalledWith(false);
|
||||
expect(mockAccountService.setShowHeader).toHaveBeenCalledWith(true);
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { RouterModule, Router } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield, NoResults } from "@bitwarden/assets/svg";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
ItemModule,
|
||||
BadgeModule,
|
||||
],
|
||||
templateUrl: "fido2-excluded-ciphers.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Fido2ExcludedCiphersComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
readonly Icons = { BitwardenShield, NoResults };
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
await this.closeModal();
|
||||
}
|
||||
|
||||
async closeModal(): Promise<void> {
|
||||
// Clean up modal state
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.accountService.setShowHeader(true);
|
||||
|
||||
// Clean up session state
|
||||
if (this.session) {
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
|
||||
// Navigate away
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full">
|
||||
<bit-section
|
||||
disableMargin
|
||||
class="tw-sticky tw-top-0 tw-z-10 tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<bit-section-header class="tw-app-region-drag tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">{{ "passkeyLogin" | i18n }}</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
slot="end"
|
||||
class="tw-app-region-no-drag tw-mb-4 tw-mr-2"
|
||||
(click)="closeModal()"
|
||||
[label]="'close' | i18n"
|
||||
>
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</bit-section-header>
|
||||
</bit-section>
|
||||
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
|
||||
<bit-item *ngFor="let c of ciphers$ | async" class="">
|
||||
<button type="button" bit-item-content (click)="chooseCipher(c)">
|
||||
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
|
||||
<button bitLink [title]="c.name" type="button">
|
||||
{{ c.name }}
|
||||
</button>
|
||||
<span slot="secondary">{{ c.subTitle }}</span>
|
||||
<span bitBadge slot="end">{{ "select" | i18n }}</span>
|
||||
</button>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
</div>
|
||||
@@ -0,0 +1,196 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
import { Fido2VaultComponent } from "./fido2-vault.component";
|
||||
|
||||
describe("Fido2VaultComponent", () => {
|
||||
let component: Fido2VaultComponent;
|
||||
let fixture: ComponentFixture<Fido2VaultComponent>;
|
||||
let mockDesktopSettingsService: MockProxy<DesktopSettingsService>;
|
||||
let mockFido2UserInterfaceService: MockProxy<DesktopFido2UserInterfaceService>;
|
||||
let mockCipherService: MockProxy<CipherService>;
|
||||
let mockAccountService: MockProxy<AccountService>;
|
||||
let mockLogService: MockProxy<LogService>;
|
||||
let mockPasswordRepromptService: MockProxy<PasswordRepromptService>;
|
||||
let mockRouter: MockProxy<Router>;
|
||||
let mockSession: MockProxy<DesktopFido2UserInterfaceSession>;
|
||||
let mockI18nService: MockProxy<I18nService>;
|
||||
|
||||
const mockActiveAccount = { id: "test-user-id", email: "test@example.com" };
|
||||
const mockCipherIds = ["cipher-1", "cipher-2", "cipher-3"];
|
||||
|
||||
beforeEach(async () => {
|
||||
mockDesktopSettingsService = mock<DesktopSettingsService>();
|
||||
mockFido2UserInterfaceService = mock<DesktopFido2UserInterfaceService>();
|
||||
mockCipherService = mock<CipherService>();
|
||||
mockAccountService = mock<AccountService>();
|
||||
mockLogService = mock<LogService>();
|
||||
mockPasswordRepromptService = mock<PasswordRepromptService>();
|
||||
mockRouter = mock<Router>();
|
||||
mockSession = mock<DesktopFido2UserInterfaceSession>();
|
||||
mockI18nService = mock<I18nService>();
|
||||
|
||||
mockAccountService.activeAccount$ = of(mockActiveAccount as Account);
|
||||
mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(mockSession);
|
||||
mockSession.availableCipherIds$ = of(mockCipherIds);
|
||||
mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of([]));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Fido2VaultComponent],
|
||||
providers: [
|
||||
{ provide: DesktopSettingsService, useValue: mockDesktopSettingsService },
|
||||
{ provide: DesktopFido2UserInterfaceService, useValue: mockFido2UserInterfaceService },
|
||||
{ provide: CipherService, useValue: mockCipherService },
|
||||
{ provide: AccountService, useValue: mockAccountService },
|
||||
{ provide: LogService, useValue: mockLogService },
|
||||
{ provide: PasswordRepromptService, useValue: mockPasswordRepromptService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Fido2VaultComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
const mockCiphers: any[] = [
|
||||
{
|
||||
id: "cipher-1",
|
||||
name: "Test Cipher 1",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "test1@example.com",
|
||||
},
|
||||
reprompt: CipherRepromptType.None,
|
||||
deletedDate: null,
|
||||
},
|
||||
{
|
||||
id: "cipher-2",
|
||||
name: "Test Cipher 2",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "test2@example.com",
|
||||
},
|
||||
reprompt: CipherRepromptType.None,
|
||||
deletedDate: null,
|
||||
},
|
||||
{
|
||||
id: "cipher-3",
|
||||
name: "Test Cipher 3",
|
||||
type: CipherType.Login,
|
||||
login: {
|
||||
username: "test3@example.com",
|
||||
},
|
||||
reprompt: CipherRepromptType.Password,
|
||||
deletedDate: null,
|
||||
},
|
||||
];
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
it("should initialize session and load ciphers successfully", async () => {
|
||||
mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(mockCiphers));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(mockFido2UserInterfaceService.getCurrentSession).toHaveBeenCalled();
|
||||
expect(component.session).toBe(mockSession);
|
||||
expect(component.cipherIds$).toBe(mockSession.availableCipherIds$);
|
||||
expect(mockCipherService.cipherListViews$).toHaveBeenCalledWith(mockActiveAccount.id);
|
||||
});
|
||||
|
||||
it("should handle when no active session found", async () => {
|
||||
mockFido2UserInterfaceService.getCurrentSession.mockReturnValue(null);
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component.session).toBeNull();
|
||||
});
|
||||
|
||||
it("should filter out deleted ciphers", async () => {
|
||||
const ciphersWithDeleted = [
|
||||
...mockCiphers.slice(0, 1),
|
||||
{ ...mockCiphers[1], deletedDate: new Date() },
|
||||
...mockCiphers.slice(2),
|
||||
];
|
||||
mockCipherService.cipherListViews$ = jest.fn().mockReturnValue(of(ciphersWithDeleted));
|
||||
|
||||
await component.ngOnInit();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
let ciphersResult: CipherView[] = [];
|
||||
component.ciphers$.subscribe((ciphers) => {
|
||||
ciphersResult = ciphers;
|
||||
});
|
||||
|
||||
expect(ciphersResult).toHaveLength(2);
|
||||
expect(ciphersResult.every((cipher) => !cipher.deletedDate)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chooseCipher", () => {
|
||||
const cipher = mockCiphers[0];
|
||||
|
||||
beforeEach(() => {
|
||||
component.session = mockSession;
|
||||
});
|
||||
|
||||
it("should choose cipher when access is validated", async () => {
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
await component.chooseCipher(cipher);
|
||||
|
||||
expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]);
|
||||
});
|
||||
|
||||
it("should prompt for password when cipher requires reprompt", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(true);
|
||||
|
||||
await component.chooseCipher(cipher);
|
||||
|
||||
expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled();
|
||||
expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, true);
|
||||
});
|
||||
|
||||
it("should not choose cipher when password reprompt is cancelled", async () => {
|
||||
cipher.reprompt = CipherRepromptType.Password;
|
||||
mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false);
|
||||
|
||||
await component.chooseCipher(cipher);
|
||||
|
||||
expect(mockPasswordRepromptService.showPasswordPrompt).toHaveBeenCalled();
|
||||
expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(cipher.id, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("closeModal", () => {
|
||||
it("should close modal and notify session", async () => {
|
||||
component.session = mockSession;
|
||||
|
||||
await component.closeModal();
|
||||
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["/"]);
|
||||
expect(mockSession.notifyConfirmCreateCredential).toHaveBeenCalledWith(false);
|
||||
expect(mockSession.confirmChosenCipher).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { RouterModule, Router } from "@angular/router";
|
||||
import {
|
||||
firstValueFrom,
|
||||
map,
|
||||
combineLatest,
|
||||
of,
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield } from "@bitwarden/assets/svg";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
DialogService,
|
||||
IconModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
BitIconButtonComponent,
|
||||
SectionHeaderComponent,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../services/desktop-fido2-user-interface.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
ItemModule,
|
||||
BadgeModule,
|
||||
],
|
||||
templateUrl: "fido2-vault.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
private destroy$ = new Subject<void>();
|
||||
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
|
||||
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
|
||||
cipherIds$: Observable<string[]> | undefined;
|
||||
readonly Icons = { BitwardenShield };
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly logService: LogService,
|
||||
private readonly passwordRepromptService: PasswordRepromptService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
this.cipherIds$ = this.session?.availableCipherIds$;
|
||||
await this.loadCiphers();
|
||||
}
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async chooseCipher(cipher: CipherView): Promise<void> {
|
||||
if (!this.session) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "unexpectedErrorShort" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeThisWindow" },
|
||||
cancelButtonText: null,
|
||||
});
|
||||
await this.closeModal();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await this.validateCipherAccess(cipher);
|
||||
this.session.confirmChosenCipher(cipher.id, isConfirmed);
|
||||
|
||||
await this.closeModal();
|
||||
}
|
||||
|
||||
async closeModal(): Promise<void> {
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.accountService.setShowHeader(true);
|
||||
|
||||
if (this.session) {
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
|
||||
private async loadCiphers(): Promise<void> {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!activeUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine cipher list with optional cipher IDs filter
|
||||
combineLatest([this.cipherService.cipherListViews$(activeUserId), this.cipherIds$ || of(null)])
|
||||
.pipe(
|
||||
map(([ciphers, cipherIds]) => {
|
||||
// Filter out deleted ciphers
|
||||
const activeCiphers = ciphers.filter((cipher) => !cipher.deletedDate);
|
||||
|
||||
// If specific IDs provided, filter by them
|
||||
if (cipherIds?.length > 0) {
|
||||
return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id as string));
|
||||
}
|
||||
|
||||
return activeCiphers;
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe({
|
||||
next: (ciphers) => this.ciphersSubject.next(ciphers as CipherView[]),
|
||||
error: (error: unknown) => this.logService.error("Failed to load ciphers", error),
|
||||
});
|
||||
}
|
||||
|
||||
private async validateCipherAccess(cipher: CipherView): Promise<boolean> {
|
||||
if (cipher.reprompt !== CipherRepromptType.None) {
|
||||
return this.passwordRepromptService.showPasswordPrompt();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
3
apps/desktop/src/autofill/models/autotype-config.ts
Normal file
3
apps/desktop/src/autofill/models/autotype-config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface AutotypeConfig {
|
||||
keyboardShortcut: string[];
|
||||
}
|
||||
9
apps/desktop/src/autofill/models/ipc-channels.ts
Normal file
9
apps/desktop/src/autofill/models/ipc-channels.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const AUTOTYPE_IPC_CHANNELS = {
|
||||
INIT: "autofill.initAutotype",
|
||||
INITIALIZED: "autofill.autotypeIsInitialized",
|
||||
TOGGLE: "autofill.toggleAutotype",
|
||||
CONFIGURE: "autofill.configureAutotype",
|
||||
LISTEN: "autofill.listenAutotypeRequest",
|
||||
EXECUTION_ERROR: "autofill.autotypeExecutionError",
|
||||
EXECUTE: "autofill.executeAutotype",
|
||||
} as const;
|
||||
@@ -5,13 +5,17 @@ import type { autofill } from "@bitwarden/desktop-napi";
|
||||
import { Command } from "../platform/main/autofill/command";
|
||||
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
|
||||
|
||||
import { AutotypeConfig } from "./models/autotype-config";
|
||||
import { AutotypeMatchError } from "./models/autotype-errors";
|
||||
import { AutotypeVaultData } from "./models/autotype-vault-data";
|
||||
import { AUTOTYPE_IPC_CHANNELS } from "./models/ipc-channels";
|
||||
|
||||
export default {
|
||||
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
|
||||
ipcRenderer.invoke("autofill.runCommand", params),
|
||||
|
||||
listenerReady: () => ipcRenderer.send("autofill.listenerReady"),
|
||||
|
||||
listenPasskeyRegistration: (
|
||||
fn: (
|
||||
clientId: number,
|
||||
@@ -130,8 +134,29 @@ export default {
|
||||
},
|
||||
);
|
||||
},
|
||||
configureAutotype: (enabled: boolean, keyboardShortcut: string[]) => {
|
||||
ipcRenderer.send("autofill.configureAutotype", { enabled, keyboardShortcut });
|
||||
listenNativeStatus: (
|
||||
fn: (clientId: number, sequenceNumber: number, status: { key: string; value: string }) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.nativeStatus",
|
||||
(
|
||||
event,
|
||||
data: {
|
||||
clientId: number;
|
||||
sequenceNumber: number;
|
||||
status: { key: string; value: string };
|
||||
},
|
||||
) => {
|
||||
const { clientId, sequenceNumber, status } = data;
|
||||
fn(clientId, sequenceNumber, status);
|
||||
},
|
||||
);
|
||||
},
|
||||
configureAutotype: (config: AutotypeConfig) => {
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.CONFIGURE, config);
|
||||
},
|
||||
toggleAutotype: (enable: boolean) => {
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.TOGGLE, enable);
|
||||
},
|
||||
listenAutotypeRequest: (
|
||||
fn: (
|
||||
@@ -140,7 +165,7 @@ export default {
|
||||
) => void,
|
||||
) => {
|
||||
ipcRenderer.on(
|
||||
"autofill.listenAutotypeRequest",
|
||||
AUTOTYPE_IPC_CHANNELS.LISTEN,
|
||||
(
|
||||
_event,
|
||||
data: {
|
||||
@@ -155,11 +180,12 @@ export default {
|
||||
windowTitle,
|
||||
errorMessage: error.message,
|
||||
};
|
||||
ipcRenderer.send("autofill.completeAutotypeError", matchError);
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTION_ERROR, matchError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (vaultData !== null) {
|
||||
ipcRenderer.send("autofill.completeAutotypeRequest", vaultData);
|
||||
ipcRenderer.send(AUTOTYPE_IPC_CHANNELS.EXECUTE, vaultData);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import {
|
||||
Subject,
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
@@ -8,10 +10,11 @@ import {
|
||||
mergeMap,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
EMPTY,
|
||||
} from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
@@ -48,6 +51,7 @@ import type { NativeWindowObject } from "./desktop-fido2-user-interface.service"
|
||||
@Injectable()
|
||||
export class DesktopAutofillService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private registrationRequest: autofill.PasskeyRegistrationRequest;
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
@@ -55,6 +59,7 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
private configService: ConfigService,
|
||||
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
) {}
|
||||
|
||||
@@ -68,28 +73,56 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((enabled) => {
|
||||
if (!enabled) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
switchMap((userId) => this.cipherService.cipherViews$(userId)),
|
||||
filter((enabled) => enabled === true), // Only proceed if feature is enabled
|
||||
switchMap(() => {
|
||||
return combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
),
|
||||
this.authService.activeAccountStatus$,
|
||||
]).pipe(
|
||||
// Only proceed when the vault is unlocked
|
||||
filter(([, status]) => status === AuthenticationStatus.Unlocked),
|
||||
// Then get cipher views
|
||||
switchMap(([userId]) => this.cipherService.cipherViews$(userId)),
|
||||
);
|
||||
}),
|
||||
// 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.
|
||||
debounceTime(100), // just a precaution to not spam the sync if there are multiple changes (we typically observe a null change)
|
||||
// No filter for empty arrays here - we want to sync even if there are 0 items
|
||||
filter((cipherViewMap) => cipherViewMap !== null),
|
||||
|
||||
mergeMap((cipherViewMap) => this.sync(Object.values(cipherViewMap ?? []))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Listen for sign out to clear credentials
|
||||
this.authService.activeAccountStatus$
|
||||
.pipe(
|
||||
filter((status) => status === AuthenticationStatus.LoggedOut),
|
||||
mergeMap(() => this.sync([])), // sync an empty array
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.listenIpc();
|
||||
}
|
||||
|
||||
async adHocSync(): Promise<any> {
|
||||
this.logService.debug("Performing AdHoc sync");
|
||||
const account = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const userId = account?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No active user found");
|
||||
}
|
||||
|
||||
const cipherViewMap = await firstValueFrom(this.cipherService.cipherViews$(userId));
|
||||
this.logService.info("Performing AdHoc sync", Object.values(cipherViewMap ?? []));
|
||||
await this.sync(Object.values(cipherViewMap ?? []));
|
||||
}
|
||||
|
||||
/** Give metadata about all available credentials in the users vault */
|
||||
async sync(cipherViews: CipherView[]) {
|
||||
const status = await this.status();
|
||||
@@ -130,6 +163,11 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
this.logService.info("Syncing autofill credentials", {
|
||||
fido2Credentials,
|
||||
passwordCredentials,
|
||||
});
|
||||
|
||||
const syncResult = await ipc.autofill.runCommand<NativeAutofillSyncCommand>({
|
||||
namespace: "autofill",
|
||||
command: "sync",
|
||||
@@ -155,107 +193,152 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
get lastRegistrationRequest() {
|
||||
return this.registrationRequest;
|
||||
}
|
||||
|
||||
listenIpc() {
|
||||
ipc.autofill.listenPasskeyRegistration((clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyRegistration", clientId, sequenceNumber, request);
|
||||
this.logService.warning(
|
||||
"listenPasskeyRegistration2",
|
||||
this.convertRegistrationRequest(request),
|
||||
);
|
||||
ipc.autofill.listenPasskeyRegistration(async (clientId, sequenceNumber, request, callback) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyRegistration: MacOsNativeCredentialSync feature flag is disabled",
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.registrationRequest = request;
|
||||
|
||||
this.logService.debug("listenPasskeyRegistration", clientId, sequenceNumber, request);
|
||||
this.logService.debug("listenPasskeyRegistration2", this.convertRegistrationRequest(request));
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.makeCredential(
|
||||
|
||||
try {
|
||||
const response = await this.fido2AuthenticatorService.makeCredential(
|
||||
this.convertRegistrationRequest(request),
|
||||
{ windowXy: request.windowXy },
|
||||
{ windowXy: normalizePosition(request.windowXy) },
|
||||
controller,
|
||||
)
|
||||
.then((response) => {
|
||||
callback(null, this.convertRegistrationResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyRegistration error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
);
|
||||
|
||||
callback(null, this.convertRegistrationResponse(request, response));
|
||||
} catch (error) {
|
||||
this.logService.error("listenPasskeyRegistration error", error);
|
||||
callback(error, null);
|
||||
}
|
||||
});
|
||||
|
||||
ipc.autofill.listenPasskeyAssertionWithoutUserInterface(
|
||||
async (clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning(
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyAssertionWithoutUserInterface: MacOsNativeCredentialSync feature flag is disabled",
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.debug(
|
||||
"listenPasskeyAssertion without user interface",
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request,
|
||||
);
|
||||
|
||||
// For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
if (!activeUserId) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Active user not found");
|
||||
callback(new Error("Active user not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)),
|
||||
);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.getAssertion(
|
||||
this.convertAssertionRequest(request),
|
||||
{ windowXy: request.windowXy },
|
||||
|
||||
try {
|
||||
// For some reason the credentialId is passed as an empty array in the request, so we need to
|
||||
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
|
||||
if (request.recordIdentifier && request.credentialId.length === 0) {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getOptionalUserId),
|
||||
);
|
||||
if (!activeUserId) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Active user not found");
|
||||
callback(new Error("Active user not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId);
|
||||
if (!cipher) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Cipher not found");
|
||||
callback(new Error("Cipher not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
const decrypted = await this.cipherService.decrypt(cipher, activeUserId);
|
||||
|
||||
const fido2Credential = decrypted.login.fido2Credentials?.[0];
|
||||
if (!fido2Credential) {
|
||||
this.logService.error("listenPasskeyAssertion error", "Fido2Credential not found");
|
||||
callback(new Error("Fido2Credential not found"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
request.credentialId = Array.from(
|
||||
new Uint8Array(parseCredentialId(decrypted.login.fido2Credentials?.[0].credentialId)),
|
||||
);
|
||||
}
|
||||
|
||||
const response = await this.fido2AuthenticatorService.getAssertion(
|
||||
this.convertAssertionRequest(request, true),
|
||||
{ windowXy: normalizePosition(request.windowXy) },
|
||||
controller,
|
||||
)
|
||||
.then((response) => {
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
);
|
||||
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
} catch (error) {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ipc.autofill.listenPasskeyAssertion(async (clientId, sequenceNumber, request, callback) => {
|
||||
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
this.logService.debug(
|
||||
"listenPasskeyAssertion: MacOsNativeCredentialSync feature flag is disabled",
|
||||
);
|
||||
callback(new Error("MacOsNativeCredentialSync feature flag is disabled"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.debug("listenPasskeyAssertion", clientId, sequenceNumber, request);
|
||||
|
||||
const controller = new AbortController();
|
||||
void this.fido2AuthenticatorService
|
||||
.getAssertion(
|
||||
try {
|
||||
const response = await this.fido2AuthenticatorService.getAssertion(
|
||||
this.convertAssertionRequest(request),
|
||||
{ windowXy: request.windowXy },
|
||||
{ windowXy: normalizePosition(request.windowXy) },
|
||||
controller,
|
||||
)
|
||||
.then((response) => {
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
});
|
||||
);
|
||||
|
||||
callback(null, this.convertAssertionResponse(request, response));
|
||||
} catch (error) {
|
||||
this.logService.error("listenPasskeyAssertion error", error);
|
||||
callback(error, null);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for native status messages
|
||||
ipc.autofill.listenNativeStatus(async (clientId, sequenceNumber, status) => {
|
||||
if (!(await this.configService.getFeatureFlag(FeatureFlag.MacOsNativeCredentialSync))) {
|
||||
this.logService.debug(
|
||||
"listenNativeStatus: MacOsNativeCredentialSync feature flag is disabled",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Received native status", status.key, status.value);
|
||||
if (status.key === "request-sync") {
|
||||
// perform ad-hoc sync
|
||||
await this.adHocSync();
|
||||
}
|
||||
});
|
||||
|
||||
ipc.autofill.listenerReady();
|
||||
}
|
||||
|
||||
private convertRegistrationRequest(
|
||||
@@ -277,7 +360,10 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
alg,
|
||||
type: "public-key",
|
||||
})),
|
||||
excludeCredentialDescriptorList: [],
|
||||
excludeCredentialDescriptorList: request.excludedCredentials.map((credentialId) => ({
|
||||
id: new Uint8Array(credentialId),
|
||||
type: "public-key" as const,
|
||||
})),
|
||||
requireResidentKey: true,
|
||||
requireUserVerification:
|
||||
request.userVerification === "required" || request.userVerification === "preferred",
|
||||
@@ -309,18 +395,19 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
request:
|
||||
| autofill.PasskeyAssertionRequest
|
||||
| autofill.PasskeyAssertionWithoutUserInterfaceRequest,
|
||||
assumeUserPresence: boolean = false,
|
||||
): Fido2AuthenticatorGetAssertionParams {
|
||||
let allowedCredentials;
|
||||
if ("credentialId" in request) {
|
||||
allowedCredentials = [
|
||||
{
|
||||
id: new Uint8Array(request.credentialId),
|
||||
id: new Uint8Array(request.credentialId).buffer,
|
||||
type: "public-key" as const,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
allowedCredentials = request.allowedCredentials.map((credentialId) => ({
|
||||
id: new Uint8Array(credentialId),
|
||||
id: new Uint8Array(credentialId).buffer,
|
||||
type: "public-key" as const,
|
||||
}));
|
||||
}
|
||||
@@ -333,7 +420,7 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
requireUserVerification:
|
||||
request.userVerification === "required" || request.userVerification === "preferred",
|
||||
fallbackSupported: false,
|
||||
assumeUserPresence: true, // For desktop assertions, it's safe to assume UP has been checked by OS dialogues
|
||||
assumeUserPresence,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -358,3 +445,13 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePosition(position: { x: number; y: number }): { x: number; y: number } {
|
||||
// Add 100 pixels to the x-coordinate to offset the native OS dialog positioning.
|
||||
const xPositionOffset = 100;
|
||||
|
||||
return {
|
||||
x: Math.round(position.x + xPositionOffset),
|
||||
y: Math.round(position.y),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Account, UserId } from "@bitwarden/common/platform/models/domain/account";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
|
||||
|
||||
@@ -30,9 +31,10 @@ describe("DesktopAutotypeDefaultSettingPolicy", () => {
|
||||
beforeEach(() => {
|
||||
mockAccountSubject = new BehaviorSubject<Account | null>({
|
||||
id: mockUserId,
|
||||
email: "test@example.com",
|
||||
emailVerified: true,
|
||||
name: "Test User",
|
||||
...mockAccountInfoWith({
|
||||
email: "test@example.com",
|
||||
name: "Test User",
|
||||
}),
|
||||
});
|
||||
mockFeatureFlagSubject = new BehaviorSubject<boolean>(true);
|
||||
mockAuthStatusSubject = new BehaviorSubject<AuthenticationStatus>(
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { combineLatest, filter, firstValueFrom, map, Observable, of, switchMap } from "rxjs";
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import {
|
||||
combineLatest,
|
||||
concatMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
of,
|
||||
Subject,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -15,8 +28,10 @@ import {
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { AutotypeConfig } from "../models/autotype-config";
|
||||
import { AutotypeVaultData } from "../models/autotype-vault-data";
|
||||
|
||||
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
|
||||
@@ -44,16 +59,26 @@ export const AUTOTYPE_KEYBOARD_SHORTCUT = new KeyDefinition<string[]>(
|
||||
{ deserializer: (b) => b },
|
||||
);
|
||||
|
||||
export class DesktopAutotypeService {
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class DesktopAutotypeService implements OnDestroy {
|
||||
private readonly autotypeEnabledState = this.globalStateProvider.get(AUTOTYPE_ENABLED);
|
||||
private readonly autotypeKeyboardShortcut = this.globalStateProvider.get(
|
||||
AUTOTYPE_KEYBOARD_SHORTCUT,
|
||||
);
|
||||
|
||||
autotypeEnabledUserSetting$: Observable<boolean> = of(false);
|
||||
resolvedAutotypeEnabled$: Observable<boolean> = of(false);
|
||||
// if the user's account is Premium
|
||||
private readonly isPremiumAccount$: Observable<boolean>;
|
||||
|
||||
// The enabled/disabled state from the user settings menu
|
||||
autotypeEnabledUserSetting$: Observable<boolean>;
|
||||
|
||||
// The keyboard shortcut from the user settings menu
|
||||
autotypeKeyboardShortcut$: Observable<string[]> = of(defaultWindowsAutotypeKeyboardShortcut);
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
@@ -63,76 +88,110 @@ export class DesktopAutotypeService {
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private desktopAutotypePolicy: DesktopAutotypeDefaultSettingPolicy,
|
||||
private logService: LogService,
|
||||
) {
|
||||
this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$.pipe(
|
||||
map((enabled) => enabled ?? false),
|
||||
distinctUntilChanged(), // Only emit when the boolean result changes
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.isPremiumAccount$ = this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
),
|
||||
distinctUntilChanged(), // Only emit when the boolean result changes
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe(
|
||||
map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Currently Autotype is only supported for Windows
|
||||
if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) {
|
||||
return;
|
||||
}
|
||||
|
||||
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
|
||||
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
|
||||
const firstCipher = possibleCiphers?.at(0);
|
||||
const [error, vaultData] = getAutotypeVaultData(firstCipher);
|
||||
callback(error, vaultData);
|
||||
});
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$;
|
||||
this.autotypeKeyboardShortcut$ = this.autotypeKeyboardShortcut.state$.pipe(
|
||||
map((shortcut) => shortcut ?? defaultWindowsAutotypeKeyboardShortcut),
|
||||
);
|
||||
|
||||
// Currently Autotype is only supported for Windows
|
||||
if (this.platformUtilsService.getDevice() === DeviceType.WindowsDesktop) {
|
||||
// If `autotypeDefaultPolicy` is `true` for a user's organization, and the
|
||||
// user has never changed their local autotype setting (`autotypeEnabledState`),
|
||||
// we set their local setting to `true` (once the local user setting is changed
|
||||
// by this policy or the user themselves, the default policy should
|
||||
// never change the user setting again).
|
||||
combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.desktopAutotypePolicy.autotypeDefaultSetting$,
|
||||
])
|
||||
.pipe(
|
||||
map(async ([autotypeEnabledState, autotypeDefaultPolicy]) => {
|
||||
// If `autotypeDefaultPolicy` is `true` for a user's organization, and the
|
||||
// user has never changed their local autotype setting (`autotypeEnabledState`),
|
||||
// we set their local setting to `true` (once the local user setting is changed
|
||||
// by this policy or the user themselves, the default policy should
|
||||
// never change the user setting again).
|
||||
combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.desktopAutotypePolicy.autotypeDefaultSetting$,
|
||||
])
|
||||
.pipe(
|
||||
concatMap(async ([autotypeEnabledState, autotypeDefaultPolicy]) => {
|
||||
try {
|
||||
if (autotypeDefaultPolicy === true && autotypeEnabledState === null) {
|
||||
await this.setAutotypeEnabledState(true);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.subscribe();
|
||||
} catch {
|
||||
this.logService.error("Failed to set Autotype enabled state.");
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// autotypeEnabledUserSetting$ publicly represents the value the
|
||||
// user has set for autotyeEnabled in their local settings.
|
||||
this.autotypeEnabledUserSetting$ = this.autotypeEnabledState.state$;
|
||||
// listen for changes in keyboard shortcut settings
|
||||
this.autotypeKeyboardShortcut$
|
||||
.pipe(
|
||||
concatMap(async (keyboardShortcut) => {
|
||||
const config: AutotypeConfig = {
|
||||
keyboardShortcut,
|
||||
};
|
||||
ipc.autofill.configureAutotype(config);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// resolvedAutotypeEnabled$ represents the final determination if the Autotype
|
||||
// feature should be on or off.
|
||||
this.resolvedAutotypeEnabled$ = combineLatest([
|
||||
this.autotypeEnabledState.state$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map((activeAccount) => activeAccount?.id),
|
||||
switchMap((userId) => this.authService.authStatusFor$(userId)),
|
||||
),
|
||||
this.accountService.activeAccount$.pipe(
|
||||
filter((account): account is Account => !!account),
|
||||
switchMap((account) =>
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
map(
|
||||
([autotypeEnabled, windowsDesktopAutotypeFeatureFlag, authStatus, hasPremium]) =>
|
||||
autotypeEnabled &&
|
||||
windowsDesktopAutotypeFeatureFlag &&
|
||||
authStatus == AuthenticationStatus.Unlocked &&
|
||||
hasPremium,
|
||||
),
|
||||
);
|
||||
this.autotypeFeatureEnabled$
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
ipc.autofill.toggleAutotype(enabled);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
combineLatest([this.resolvedAutotypeEnabled$, this.autotypeKeyboardShortcut$]).subscribe(
|
||||
([resolvedAutotypeEnabled, autotypeKeyboardShortcut]) => {
|
||||
ipc.autofill.configureAutotype(resolvedAutotypeEnabled, autotypeKeyboardShortcut);
|
||||
},
|
||||
);
|
||||
}
|
||||
// Returns an observable that represents whether autotype is enabled for the current user.
|
||||
private get autotypeFeatureEnabled$(): Observable<boolean> {
|
||||
return combineLatest([
|
||||
// if the user has enabled the setting
|
||||
this.autotypeEnabledUserSetting$,
|
||||
// if the feature flag is set
|
||||
this.configService.getFeatureFlag$(FeatureFlag.WindowsDesktopAutotype),
|
||||
// if there is an active account with an unlocked vault
|
||||
this.authService.activeAccountStatus$,
|
||||
// if the active user's account is Premium
|
||||
this.isPremiumAccount$,
|
||||
]).pipe(
|
||||
map(
|
||||
([settingsEnabled, ffEnabled, authStatus, isPremiumAcct]) =>
|
||||
settingsEnabled &&
|
||||
ffEnabled &&
|
||||
authStatus === AuthenticationStatus.Unlocked &&
|
||||
isPremiumAcct,
|
||||
),
|
||||
distinctUntilChanged(), // Only emit when the boolean result changes
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
}
|
||||
|
||||
async setAutotypeEnabledState(enabled: boolean): Promise<void> {
|
||||
@@ -176,6 +235,11 @@ export class DesktopAutotypeService {
|
||||
|
||||
return possibleCiphers;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,9 +43,7 @@ export type NativeWindowObject = {
|
||||
windowXy?: { x: number; y: number };
|
||||
};
|
||||
|
||||
export class DesktopFido2UserInterfaceService
|
||||
implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject>
|
||||
{
|
||||
export class DesktopFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction<NativeWindowObject> {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
@@ -66,7 +64,7 @@ export class DesktopFido2UserInterfaceService
|
||||
nativeWindowObject: NativeWindowObject,
|
||||
abortController?: AbortController,
|
||||
): Promise<DesktopFido2UserInterfaceSession> {
|
||||
this.logService.warning("newSession", fallbackSupported, abortController, nativeWindowObject);
|
||||
this.logService.debug("newSession", fallbackSupported, abortController, nativeWindowObject);
|
||||
const session = new DesktopFido2UserInterfaceSession(
|
||||
this.authService,
|
||||
this.cipherService,
|
||||
@@ -94,9 +92,11 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
) {}
|
||||
|
||||
private confirmCredentialSubject = new Subject<boolean>();
|
||||
private createdCipher: Cipher;
|
||||
|
||||
private availableCipherIdsSubject = new BehaviorSubject<string[]>(null);
|
||||
private updatedCipher: CipherView;
|
||||
|
||||
private rpId = new BehaviorSubject<string>(null);
|
||||
private availableCipherIdsSubject = new BehaviorSubject<string[]>([""]);
|
||||
/**
|
||||
* Observable that emits available cipher IDs once they're confirmed by the UI
|
||||
*/
|
||||
@@ -114,7 +114,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
assumeUserPresence,
|
||||
masterPasswordRepromptRequired,
|
||||
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
this.logService.warning("pickCredential desktop function", {
|
||||
this.logService.debug("pickCredential desktop function", {
|
||||
cipherIds,
|
||||
userVerification,
|
||||
assumeUserPresence,
|
||||
@@ -123,6 +123,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
|
||||
try {
|
||||
// Check if we can return the credential without user interaction
|
||||
await this.accountService.setShowHeader(false);
|
||||
if (assumeUserPresence && cipherIds.length === 1 && !masterPasswordRepromptRequired) {
|
||||
this.logService.debug(
|
||||
"shortcut - Assuming user presence and returning cipherId",
|
||||
@@ -136,22 +137,27 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
// make the cipherIds available to the UI.
|
||||
this.availableCipherIdsSubject.next(cipherIds);
|
||||
|
||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
||||
await this.showUi("/fido2-assertion", this.windowObject.windowXy, false);
|
||||
|
||||
const chosenCipherResponse = await this.waitForUiChosenCipher();
|
||||
|
||||
this.logService.debug("Received chosen cipher", chosenCipherResponse);
|
||||
|
||||
return {
|
||||
cipherId: chosenCipherResponse.cipherId,
|
||||
userVerified: chosenCipherResponse.userVerified,
|
||||
cipherId: chosenCipherResponse?.cipherId,
|
||||
userVerified: chosenCipherResponse?.userVerified,
|
||||
};
|
||||
} finally {
|
||||
// Make sure to clean up so the app is never stuck in modal mode?
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.accountService.setShowHeader(true);
|
||||
}
|
||||
}
|
||||
|
||||
async getRpId(): Promise<string> {
|
||||
return firstValueFrom(this.rpId.pipe(filter((id) => id != null)));
|
||||
}
|
||||
|
||||
confirmChosenCipher(cipherId: string, userVerified: boolean = false): void {
|
||||
this.chosenCipherSubject.next({ cipherId, userVerified });
|
||||
this.chosenCipherSubject.complete();
|
||||
@@ -159,7 +165,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
|
||||
private async waitForUiChosenCipher(
|
||||
timeoutMs: number = 60000,
|
||||
): Promise<{ cipherId: string; userVerified: boolean } | undefined> {
|
||||
): Promise<{ cipherId?: string; userVerified: boolean } | undefined> {
|
||||
try {
|
||||
return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs)));
|
||||
} catch {
|
||||
@@ -174,7 +180,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
/**
|
||||
* Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS.
|
||||
*/
|
||||
notifyConfirmNewCredential(confirmed: boolean): void {
|
||||
notifyConfirmCreateCredential(confirmed: boolean, updatedCipher?: CipherView): void {
|
||||
if (updatedCipher) {
|
||||
this.updatedCipher = updatedCipher;
|
||||
}
|
||||
this.confirmCredentialSubject.next(confirmed);
|
||||
this.confirmCredentialSubject.complete();
|
||||
}
|
||||
@@ -195,60 +204,79 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
async confirmNewCredential({
|
||||
credentialName,
|
||||
userName,
|
||||
userHandle,
|
||||
userVerification,
|
||||
rpId,
|
||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
this.logService.warning(
|
||||
this.logService.debug(
|
||||
"confirmNewCredential",
|
||||
credentialName,
|
||||
userName,
|
||||
userHandle,
|
||||
userVerification,
|
||||
rpId,
|
||||
);
|
||||
this.rpId.next(rpId);
|
||||
|
||||
try {
|
||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
||||
await this.showUi("/fido2-creation", this.windowObject.windowXy, false);
|
||||
|
||||
// Wait for the UI to wrap up
|
||||
const confirmation = await this.waitForUiNewCredentialConfirmation();
|
||||
if (!confirmation) {
|
||||
return { cipherId: undefined, userVerified: false };
|
||||
}
|
||||
// Create the credential
|
||||
await this.createCredential({
|
||||
credentialName,
|
||||
userName,
|
||||
rpId,
|
||||
userHandle: "",
|
||||
userVerification,
|
||||
});
|
||||
|
||||
// wait for 10ms to help RXJS catch up(?)
|
||||
// We sometimes get a race condition from this.createCredential not updating cipherService in time
|
||||
//console.log("waiting 10ms..");
|
||||
//await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
//console.log("Just waited 10ms");
|
||||
|
||||
// Return the new cipher (this.createdCipher)
|
||||
return { cipherId: this.createdCipher.id, userVerified: userVerification };
|
||||
if (this.updatedCipher) {
|
||||
await this.updateCredential(this.updatedCipher);
|
||||
return { cipherId: this.updatedCipher.id, userVerified: userVerification };
|
||||
} else {
|
||||
// Create the cipher
|
||||
const createdCipher = await this.createCipher({
|
||||
credentialName,
|
||||
userName,
|
||||
rpId,
|
||||
userHandle,
|
||||
userVerification,
|
||||
});
|
||||
return { cipherId: createdCipher.id, userVerified: userVerification };
|
||||
}
|
||||
} finally {
|
||||
// Make sure to clean up so the app is never stuck in modal mode?
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.accountService.setShowHeader(true);
|
||||
}
|
||||
}
|
||||
|
||||
private async showUi(route: string, position?: { x: number; y: number }): Promise<void> {
|
||||
private async hideUi(): Promise<void> {
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
|
||||
private async showUi(
|
||||
route: string,
|
||||
position?: { x: number; y: number },
|
||||
showTrafficButtons: boolean = false,
|
||||
disableRedirect?: boolean,
|
||||
): Promise<void> {
|
||||
// Load the UI:
|
||||
await this.desktopSettingsService.setModalMode(true, position);
|
||||
await this.router.navigate(["/passkeys"]);
|
||||
await this.desktopSettingsService.setModalMode(true, showTrafficButtons, position);
|
||||
await this.accountService.setShowHeader(showTrafficButtons);
|
||||
await this.router.navigate([
|
||||
route,
|
||||
{
|
||||
"disable-redirect": disableRedirect || null,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called by the UI to create a new credential with user input etc.
|
||||
* Can be called by the UI to create a new cipher with user input etc.
|
||||
* @param param0
|
||||
*/
|
||||
async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise<Cipher> {
|
||||
async createCipher({ credentialName, userName, rpId }: NewCredentialParams): Promise<Cipher> {
|
||||
// Store the passkey on a new cipher to avoid replacing something important
|
||||
|
||||
const cipher = new CipherView();
|
||||
cipher.name = credentialName;
|
||||
|
||||
@@ -267,32 +295,81 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!activeUserId) {
|
||||
throw new Error("No active user ID found!");
|
||||
}
|
||||
|
||||
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||
|
||||
this.createdCipher = createdCipher;
|
||||
try {
|
||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||
|
||||
return createdCipher;
|
||||
return createdCipher;
|
||||
} catch {
|
||||
throw new Error("Unable to create cipher");
|
||||
}
|
||||
}
|
||||
|
||||
async updateCredential(cipher: CipherView): Promise<void> {
|
||||
this.logService.info("updateCredential");
|
||||
await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map(async (a) => {
|
||||
if (a) {
|
||||
const encCipher = await this.cipherService.encrypt(cipher, a.id);
|
||||
await this.cipherService.updateWithServer(encCipher);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
||||
this.logService.warning("informExcludedCredential", existingCipherIds);
|
||||
this.logService.debug("informExcludedCredential", existingCipherIds);
|
||||
|
||||
// make the cipherIds available to the UI.
|
||||
this.availableCipherIdsSubject.next(existingCipherIds);
|
||||
|
||||
await this.accountService.setShowHeader(false);
|
||||
await this.showUi("/fido2-excluded", this.windowObject.windowXy, false);
|
||||
}
|
||||
|
||||
async ensureUnlockedVault(): Promise<void> {
|
||||
this.logService.warning("ensureUnlockedVault");
|
||||
this.logService.debug("ensureUnlockedVault");
|
||||
|
||||
const status = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
if (status !== AuthenticationStatus.Unlocked) {
|
||||
throw new Error("Vault is not unlocked");
|
||||
await this.showUi("/lock", this.windowObject.windowXy, true, true);
|
||||
|
||||
let status2: AuthenticationStatus;
|
||||
try {
|
||||
status2 = await lastValueFrom(
|
||||
this.authService.activeAccountStatus$.pipe(
|
||||
filter((s) => s === AuthenticationStatus.Unlocked),
|
||||
take(1),
|
||||
timeout(1000 * 60 * 5), // 5 minutes
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
this.logService.warning("Error while waiting for vault to unlock", error);
|
||||
}
|
||||
|
||||
if (status2 === AuthenticationStatus.Unlocked) {
|
||||
await this.router.navigate(["/"]);
|
||||
}
|
||||
|
||||
if (status2 !== AuthenticationStatus.Unlocked) {
|
||||
await this.hideUi();
|
||||
throw new Error("Vault is not unlocked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async informCredentialNotFound(): Promise<void> {
|
||||
this.logService.warning("informCredentialNotFound");
|
||||
this.logService.debug("informCredentialNotFound");
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.logService.warning("close");
|
||||
this.logService.debug("close");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,6 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
private authorizedSshKeys: Record<string, Date> = {};
|
||||
|
||||
private isFeatureFlagEnabled = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@@ -91,6 +89,7 @@ export class SshAgentService implements OnDestroy {
|
||||
filter(({ enabled }) => enabled),
|
||||
map(({ message }) => message),
|
||||
withLatestFrom(this.authService.activeAccountStatus$, this.accountService.activeAccount$),
|
||||
filter(([, , account]) => account != null),
|
||||
// This switchMap handles unlocking the vault if it is locked:
|
||||
// - If the vault is locked, we will wait for it to be unlocked.
|
||||
// - If the vault is not unlocked within the timeout, we will abort the flow.
|
||||
@@ -127,7 +126,11 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
throw error;
|
||||
}),
|
||||
map(() => [message, account.id]),
|
||||
concatMap(async () => {
|
||||
// The active account may have switched with account switching during unlock
|
||||
const updatedAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
return [message, updatedAccount.id] as const;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,10 +203,6 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
this.accountService.activeAccount$.pipe(skip(1), takeUntil(this.destroy$)).subscribe({
|
||||
next: (account) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.authorizedSshKeys = {};
|
||||
this.logService.info("Active account changed, clearing SSH keys");
|
||||
ipc.platform.sshAgent
|
||||
@@ -211,20 +210,12 @@ export class SshAgentService implements OnDestroy {
|
||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||
},
|
||||
error: (e: unknown) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.error("Error in active account observable", e);
|
||||
ipc.platform.sshAgent
|
||||
.clearKeys()
|
||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||
},
|
||||
complete: () => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Active account observable completed, clearing SSH keys");
|
||||
this.authorizedSshKeys = {};
|
||||
ipc.platform.sshAgent
|
||||
@@ -239,10 +230,6 @@ export class SshAgentService implements OnDestroy {
|
||||
])
|
||||
.pipe(
|
||||
concatMap(async ([, enabled]) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await ipc.platform.sshAgent.clearKeys();
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
@@ -13,6 +15,10 @@ import { DesktopBiometricsService } from "./desktop.biometrics.service";
|
||||
*/
|
||||
@Injectable()
|
||||
export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
constructor(private tokenService: TokenService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async authenticateWithBiometrics(): Promise<boolean> {
|
||||
return await ipc.keyManagement.biometric.authenticateWithBiometrics();
|
||||
}
|
||||
@@ -31,6 +37,10 @@ export class RendererBiometricsService extends DesktopBiometricsService {
|
||||
}
|
||||
|
||||
async getBiometricsStatusForUser(id: UserId): Promise<BiometricsStatus> {
|
||||
if ((await firstValueFrom(this.tokenService.hasAccessToken$(id))) === false) {
|
||||
return BiometricsStatus.NotEnabledInConnectedDesktopApp;
|
||||
}
|
||||
|
||||
return await ipc.keyManagement.biometric.getBiometricsStatusForUser(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||
import { BiometricStateService, KdfConfigService } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
makeSymmetricCryptoKey,
|
||||
FakeAccountService,
|
||||
mockAccountServiceWith,
|
||||
FakeStateProvider,
|
||||
makeSymmetricCryptoKey,
|
||||
mockAccountServiceWith,
|
||||
} from "../../../../libs/common/spec";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { VAULT_TIMEOUT } from "../../../../libs/common/src/key-management/vault-timeout";
|
||||
|
||||
import { DesktopBiometricsService } from "./biometrics/desktop.biometrics.service";
|
||||
import { ElectronKeyService } from "./electron-key.service";
|
||||
@@ -40,11 +42,13 @@ describe("ElectronKeyService", () => {
|
||||
let accountService: FakeAccountService;
|
||||
let masterPasswordService: FakeMasterPasswordService;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
masterPasswordService = new FakeMasterPasswordService();
|
||||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
await stateProvider.setUserState(VAULT_TIMEOUT, 10, mockUserId);
|
||||
|
||||
keyService = new ElectronKeyService(
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
@@ -79,38 +83,17 @@ describe("ElectronKeyService", () => {
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("biometric unlock enabled", () => {
|
||||
beforeEach(() => {
|
||||
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||
});
|
||||
it("sets biometric key when biometric unlock enabled", async () => {
|
||||
biometricStateService.getBiometricUnlockEnabled.mockResolvedValue(true);
|
||||
|
||||
it("sets null biometric client key half and biometric unlock key when require password on start disabled", async () => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(false);
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
|
||||
describe("require password on start enabled", () => {
|
||||
beforeEach(() => {
|
||||
biometricStateService.getRequirePasswordOnStart.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("sets biometric key", async () => {
|
||||
await keyService.setUserKey(userKey, mockUserId);
|
||||
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
});
|
||||
});
|
||||
expect(biometricService.setBiometricProtectedUnlockKeyForUser).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
userKey,
|
||||
);
|
||||
expect(biometricStateService.setEncryptedClientKeyHalf).not.toHaveBeenCalled();
|
||||
expect(biometricStateService.getBiometricUnlockEnabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<div id="remove-password-page" *ngIf="!loading">
|
||||
<div class="content">
|
||||
<h1>{{ "removeMasterPassword" | i18n }}</h1>
|
||||
<p>{{ "removeMasterPasswordForOrganizationUserKeyConnector" | i18n }}</p>
|
||||
<p class="tw-mb-0">{{ "organizationName" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.name }}</p>
|
||||
<p class="tw-mb-0">{{ "keyConnectorDomain" | i18n }}:</p>
|
||||
<p class="tw-text-muted tw-mb-6">{{ organization.keyConnectorUrl }}</p>
|
||||
<div class="buttons">
|
||||
<button type="submit" class="btn primary block" [disabled]="action" (click)="convert()">
|
||||
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!continuing" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn secondary block" [disabled]="action" (click)="leave()">
|
||||
<b [hidden]="leaving">{{ "leaveOrganization" | i18n }}</b>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!leaving" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-remove-password",
|
||||
templateUrl: "remove-password.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { defer, from, map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
|
||||
|
||||
export class DesktopSessionTimeoutSettingsComponentService
|
||||
implements SessionTimeoutSettingsComponentService
|
||||
{
|
||||
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() =>
|
||||
from(ipc.platform.powermonitor.isLockMonitorAvailable()).pipe(
|
||||
map((isLockMonitorAvailable) => {
|
||||
const options: VaultTimeoutOption[] = [
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
|
||||
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
|
||||
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
|
||||
{ name: this.i18nService.t("oneHour"), value: 60 },
|
||||
{ name: this.i18nService.t("fourHours"), value: 240 },
|
||||
{ name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle },
|
||||
{ name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep },
|
||||
];
|
||||
|
||||
if (isLockMonitorAvailable) {
|
||||
options.push({
|
||||
name: this.i18nService.t("onLocked"),
|
||||
value: VaultTimeoutStringType.OnLocked,
|
||||
});
|
||||
}
|
||||
|
||||
options.push(
|
||||
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
|
||||
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
|
||||
);
|
||||
|
||||
return options;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(private readonly i18nService: I18nService) {}
|
||||
|
||||
onTimeoutSave(_: VaultTimeout): void {}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import {
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
|
||||
import { DesktopSessionTimeoutTypeService } from "./desktop-session-timeout-type.service";
|
||||
|
||||
describe("DesktopSessionTimeoutTypeService", () => {
|
||||
let service: DesktopSessionTimeoutTypeService;
|
||||
let mockIsLockMonitorAvailable: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockIsLockMonitorAvailable = jest.fn();
|
||||
|
||||
(global as any).ipc = {
|
||||
platform: {
|
||||
powermonitor: {
|
||||
isLockMonitorAvailable: mockIsLockMonitorAvailable,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
service = new DesktopSessionTimeoutTypeService();
|
||||
});
|
||||
|
||||
describe("isAvailable", () => {
|
||||
it("should return false for Immediately", async () => {
|
||||
const result = await service.isAvailable(VaultTimeoutNumberType.Immediately);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.Custom,
|
||||
])("should return true for always available type: %s", async (timeoutType) => {
|
||||
const result = await service.isAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it.each([VaultTimeoutNumberType.OnMinute, VaultTimeoutNumberType.EightHours])(
|
||||
"should return true for numeric timeout type: %s",
|
||||
async (timeoutType) => {
|
||||
const result = await service.isAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
describe("OnLocked availability", () => {
|
||||
it("should return true when lock monitor is available", async () => {
|
||||
mockIsLockMonitorAvailable.mockResolvedValue(true);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockIsLockMonitorAvailable).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return false when lock monitor is not available", async () => {
|
||||
mockIsLockMonitorAvailable.mockResolvedValue(false);
|
||||
|
||||
const result = await service.isAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockIsLockMonitorAvailable).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrPromoteToAvailable", () => {
|
||||
it.each([
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutStringType.OnRestart,
|
||||
VaultTimeoutStringType.OnLocked,
|
||||
VaultTimeoutStringType.Never,
|
||||
VaultTimeoutStringType.Custom,
|
||||
])("should return the original type when it is available: %s", async (timeoutType) => {
|
||||
jest.spyOn(service, "isAvailable").mockResolvedValue(true);
|
||||
|
||||
const result = await service.getOrPromoteToAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(timeoutType);
|
||||
expect(service.isAvailable).toHaveBeenCalledWith(timeoutType);
|
||||
});
|
||||
|
||||
it("should return OnMinute when Immediately is not available", async () => {
|
||||
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
|
||||
|
||||
const result = await service.getOrPromoteToAvailable(VaultTimeoutNumberType.Immediately);
|
||||
|
||||
expect(result).toBe(VaultTimeoutNumberType.OnMinute);
|
||||
expect(service.isAvailable).toHaveBeenCalledWith(VaultTimeoutNumberType.Immediately);
|
||||
});
|
||||
|
||||
it("should return OnSleep when OnLocked is not available", async () => {
|
||||
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
|
||||
|
||||
const result = await service.getOrPromoteToAvailable(VaultTimeoutStringType.OnLocked);
|
||||
|
||||
expect(result).toBe(VaultTimeoutStringType.OnSleep);
|
||||
expect(service.isAvailable).toHaveBeenCalledWith(VaultTimeoutStringType.OnLocked);
|
||||
});
|
||||
|
||||
it.each([
|
||||
VaultTimeoutStringType.OnIdle,
|
||||
VaultTimeoutStringType.OnSleep,
|
||||
VaultTimeoutNumberType.OnMinute,
|
||||
5,
|
||||
])("should return OnRestart when type is not available: %s", async (timeoutType) => {
|
||||
jest.spyOn(service, "isAvailable").mockResolvedValue(false);
|
||||
|
||||
const result = await service.getOrPromoteToAvailable(timeoutType);
|
||||
|
||||
expect(result).toBe(VaultTimeoutStringType.OnRestart);
|
||||
expect(service.isAvailable).toHaveBeenCalledWith(timeoutType);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SessionTimeoutTypeService } from "@bitwarden/common/key-management/session-timeout";
|
||||
import {
|
||||
isVaultTimeoutTypeNumeric,
|
||||
VaultTimeout,
|
||||
VaultTimeoutNumberType,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
|
||||
export class DesktopSessionTimeoutTypeService implements SessionTimeoutTypeService {
|
||||
async isAvailable(type: VaultTimeout): Promise<boolean> {
|
||||
switch (type) {
|
||||
case VaultTimeoutNumberType.Immediately:
|
||||
return false;
|
||||
case VaultTimeoutStringType.OnIdle:
|
||||
case VaultTimeoutStringType.OnSleep:
|
||||
case VaultTimeoutStringType.OnRestart:
|
||||
case VaultTimeoutStringType.Never:
|
||||
case VaultTimeoutStringType.Custom:
|
||||
return true;
|
||||
case VaultTimeoutStringType.OnLocked:
|
||||
return await ipc.platform.powermonitor.isLockMonitorAvailable();
|
||||
default:
|
||||
if (isVaultTimeoutTypeNumeric(type)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getOrPromoteToAvailable(type: VaultTimeout): Promise<VaultTimeout> {
|
||||
const available = await this.isAvailable(type);
|
||||
if (!available) {
|
||||
switch (type) {
|
||||
case VaultTimeoutNumberType.Immediately:
|
||||
return VaultTimeoutNumberType.OnMinute;
|
||||
case VaultTimeoutStringType.OnLocked:
|
||||
return VaultTimeoutStringType.OnSleep;
|
||||
default:
|
||||
return VaultTimeoutStringType.OnRestart;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
}
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "'n Onverwagte fout het voorgekom."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Iteminligting"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Volg Ons"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Sichroniseer Kluis"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Verander Hoofwagwoord"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Stuur Kluis Uit"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Lêerformaat"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Hoofwagwoord is verwyder"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Invoer data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Invoer fout"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "حدث خطأ غير متوقع."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "معلومات العنصر"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "تابعنا"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "مزامنة الخزانة"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "تغيير كلمة المرور الرئيسية"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "تصدير من"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "تصدير الخزانة"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "صيغة الملف"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "تمت إزالة كلمة المرور الرئيسية."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "الاسم البديل للنطاق"
|
||||
},
|
||||
"importData": {
|
||||
"message": "استيراد البيانات",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "خطأ في الاستيراد"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "تم حفظ الملف على الجهاز. إدارة من تنزيلات جهازك."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Qoşma əlavə et"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Elementlər köçürüldü"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Şifrələməni düzəlt"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "Bu fayl, köhnə bir şifrələmə üsulunu istifadə edir."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Qoşma güncəllənib"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maksimal fayl həcmi 500 MB-dır"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Gözlənilməz bir səhv baş verdi."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Gözlənilməz xəta"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Bu Bitwarden pəncərəsini bağlayıb yenidən sınayın."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Element məlumatları"
|
||||
},
|
||||
@@ -1094,22 +1112,22 @@
|
||||
"message": "Daha ətraflı"
|
||||
},
|
||||
"migrationsFailed": {
|
||||
"message": "An error occurred updating the encryption settings."
|
||||
"message": "Şifrələmə ayarlarını güncəlləyərkən bir xəta baş verdi."
|
||||
},
|
||||
"updateEncryptionSettingsTitle": {
|
||||
"message": "Update your encryption settings"
|
||||
"message": "Şifrələmə ayarlarınızı güncəlləyin"
|
||||
},
|
||||
"updateEncryptionSettingsDesc": {
|
||||
"message": "The new recommended encryption settings will improve your account security. Enter your master password to update now."
|
||||
"message": "Tövsiyə edilən yeni şifrələmə ayarları, hesabınızın təhlükəsizliyini artıracaq. İndi güncəlləmək üçün ana parolunuzu daxil edin."
|
||||
},
|
||||
"confirmIdentityToContinue": {
|
||||
"message": "Confirm your identity to continue"
|
||||
"message": "Davam etmək üçün kimliyinizi təsdiqləyin"
|
||||
},
|
||||
"enterYourMasterPassword": {
|
||||
"message": "Enter your master password"
|
||||
"message": "Ana parolunuzu daxil edin"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
"message": "Ayarları güncəllə"
|
||||
},
|
||||
"featureUnavailable": {
|
||||
"message": "Özəllik əlçatmazdır"
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Bizi izləyin"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Seyfi sinxronlaşdır"
|
||||
"syncNow": {
|
||||
"message": "İndi sinxr."
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Ana parolu dəyişdir"
|
||||
@@ -1509,7 +1527,7 @@
|
||||
"message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi."
|
||||
},
|
||||
"premiumSignUpStorageV2": {
|
||||
"message": "$SIZE$ encrypted storage for file attachments.",
|
||||
"message": "Fayl qoşmaları üçün $SIZE$ şifrələnmiş anbar sahəsi.",
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"content": "$1",
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Buradan xaricə köçür"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Seyfi xaricə köçür"
|
||||
"export": {
|
||||
"message": "Xaricə köçür"
|
||||
},
|
||||
"import": {
|
||||
"message": "Daxilə köçür"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Fayl formatı"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Ana parol silindi."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "Aşağıdakı təşkilatların üzvləri üçün artıq ana parol tələb olunmur. Lütfən aşağıdakı domeni təşkilatınızın inzibatçısı ilə təsdiqləyin."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Təşkilat adı"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Domen ləqəbi"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Veriləri daxilə köçür",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Daxilə köçürmə xətası"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Fayl cihazda saxlanıldı. Endirilənləri cihazınızdan idarə edin."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Vacib bildiriş"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "İki addımlı girişi qur"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden, 2025-ci ilin Fevral ayından etibarən yeni cihazlardan gələn girişləri doğrulamaq üçün hesabınızın e-poçtuna bir kod göndərəcək."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "Hesabınızı qorumaq üçün alternativ bir yol kimi iki addımlı girişi qura və ya e-poçtunuzu erişə biləcəyiniz bir e-poçtla dəyişdirə bilərsiniz."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Daha sonra xatırlat"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "$EMAIL$ e-poçtunuza güvənli şəkildə erişə bilirsiniz?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "Xeyr, bilmirəm"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Bəli, e-poçtuma güvənli şəkildə erişə bilirəm"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "İki addımlı girişi işə sal"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Hesabın e-poçtunu dəyişdir"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Keçid açarı ilə giriş edilsin?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Keçid açarı saxlanılsın?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Yeni giriş kimi saxla"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Keçid açarını yeni bir giriş olaraq saxla"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "Bu sayt üçün uyuşan giriş məlumatı yoxdur"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Keçid açarının üzərinə yazılsın?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Keçid açarı saxlanıla bilmir"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "Bu elementdə artıq bir keçid açarı var. Hazırkı keçid açarının üzərinə yazmaq istədiyinizə əminsiniz?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "Bu tətbiq üçün bir keçid açarı artıq mövcuddur."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "Bu tətbiq, təkrarları dəstəkləmir."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Bu pəncərəni bağla"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Ekranı çəkməyə icazə ver"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "Və daha çoxu!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Tam onlayn təhlükəsizlik"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Qabaqcıl onlayn təhlükəsizlik"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "\"Premium\"a yüksəlt"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Təşkilatınız, artıq Bitwarden-ə giriş etmək üçün ana parol istifadə etmir. Davam etmək üçün təşkilatı və domeni doğrulayın."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Giriş etməyə davam"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Davam etmə"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domen"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "Bu domen, hesabınızın şifrələmə açarlarını saxlayacaq, ona görə də, bu domenə güvəndiyinizə əmin olun. Əmin deyilsinizsə, adminizlə əlaqə saxlayın."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Giriş etmək üçün təşkilatınızı doğrulayın"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Təşkilat doğrulandı"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domen doğrulandı"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "Təşkilatınızı doğrulamasanız, təşkilata erişiminiz ləğv ediləcək."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "İndi tərk et"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Giriş etmək üçün domeninizi doğrulayın"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "Giriş prosesini davam etdirmək üçün bu domeni doğrulayın."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "Giriş prosesini davam etdirmək üçün bu təşkilatı və domeni doğrulayın."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Vaxt bitmə əməliyyatı"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Sessiya vaxt bitməsi"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "Bu ayar, təşkilatınız tərəfindən idarə olunur."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Təşkilatınız, maksimum seyf bitmə vaxtını $HOURS$ saat $MINUTES$ dəqiqə olaraq ayarladı.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Təşkilatınız, seansın ilkin bitmə vaxtını Sistem kilidi açılanda olaraq təyin etdi."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Təşkilatınız, seansın ilkin bitmə vaxtını Yenidən başladılanda olaraq təyin etdi."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maksimum bitmə vaxtı $HOURS$ saat $MINUTES$ dəqiqə dəyərini aşa bilməz",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "Yenidən başladılanda"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Vaxt bitmə əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu qurun"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Yüksəlt"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Tərk etmək istədiyinizə əminsiniz?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "Rədd cavabı versəniz, fərdi elementləriniz hesabınızda qalacaq, paylaşılan elementlərə və təşkilat özəlliklərinə erişimi itirəcəksiniz."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Erişimi təkrar qazanmaq üçün admininizlə əlaqə saxlayın."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Tərk et: $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "Seyfimi necə idarə edim?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Elementləri bura köçür: $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$, təhlükəsizlik və riayətlilik üçün bütün elementlərin təşkilata aid olmasını tələb edir. Elementlərinizin sahibliyini transfer etmək üçün qəbul edin.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Transferi qəbul et"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Rədd et və tərk et"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Bunu niyə görürəm?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Адбылася нечаканая памылка."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Звесткі аб элеменце"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Сачыце за намі"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Сінхранізаваць сховішча"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Змяніць асноўны пароль"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Экспартаваць сховішча"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Фармат файла"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Асноўны пароль выдалены."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Import error"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Добавяне на прикачен файл"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Елементите са прехвърлени"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Поправяне на шифроването"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "Този файл използва остарял метод на шифроване."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Прикаченият файл е актуализиран"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Максималният размер на файла е 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Неочаквана грешка."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Неочаквана грешка"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Затворете прозореца на Битуорден и опитайте отново."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Сведения за елемента"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Следвайте ни"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Синхронизиране"
|
||||
"syncNow": {
|
||||
"message": "Синхронизиране сега"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Промяна на главната парола"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Изнасяне от"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Изнасяне на трезора"
|
||||
"export": {
|
||||
"message": "Изнасяне"
|
||||
},
|
||||
"import": {
|
||||
"message": "Внасяне"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Формат на файла"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Главната парола е премахната."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "За членовете на следната организация вече не се изисква главна парола. Потвърдете домейна по-долу с администратора на организацията си."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Име на организацията"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Псевдонимен домейн"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Внасяне на данни",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Грешка при внасянето"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Файлът е запазен на устройството. Можете да го намерите в мястото за сваляния на устройството."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Важно съобщение"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Настройте двустепенно удостоверяване"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Битуорден ще изпрати код до е-пощата Ви, за потвърждаване на вписването от нови устройства. Това ще започне от февруари 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "Можете да настроите двустепенно удостоверяване, като различен метод на защита, или ако е необходимо да промените е-пощата си с такава, до която имате достъп."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Напомнете ми по-късно"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Имате ли сигурен достъп до е-пощата си – $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "Не, нямам"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Да, имам достъп до е-пощата си"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Включване на двустепенното удостоверяване"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Промяна на е-пощата"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Вписване със секретен ключ?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Да се запази на секретният ключ?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Запазване като нов елемент за вписване"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Запазване на секретния ключ като нов елемент за вписване"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "Няма записи за вписване отговарящи на този уеб сайт"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Да се замени ли секретният ключ?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Секретният ключ не може да бъде запазен"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "Този елемент вече съдържа секретен ключ. Наистина ли искате да замените текущия секретен ключ?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "За това приложение вече съществува секретен ключ."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "Това приложение не поддържа дубликати."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Затворете този прозорец"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Позволяване на заснемането на екрана"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "И още!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Пълна сигурност в Интернет"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Разширена сигурност в Интернет"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Надградете до Платения план"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Вашата организация вече не използва главни пароли за вписване в Битуорден. За да продължите, потвърдете организацията и домейна."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Продължаване с вписването"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Не продължавам"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Домейн"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "Този домейн ще съхранява ключовете за шифроване на акаунта Ви, така че се уверете, че му имате доверие. Ако имате съмнения, свържете се с администратора си."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Потвърдете организацията си, за да се впишете"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Организацията е потвърдена"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Домейнът е потвърден"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "Ако не потвърдите организацията, достъпът Ви до нея ще бъде преустановен."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Напускане сега"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Потвърдете домейна си, за да се впишете"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "За да продължите с вписването, потвърдете този домейн."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "За да продължите с вписването, потвърдете организацията и домейна."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Действие при изтичането на времето за достъп"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Изтичане на времето за сесията"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "Тази настройка се управлява от организацията Ви."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Организацията Ви е настроила максималното разрешено време за достъп на [%1$i] час(а) и [%2$i] минути.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Организацията Ви е настроила стандартното разрешено време за достъп да бъде до заключване на системата."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Организацията Ви е настроила стандартното разрешено време за достъп да бъде до рестартиране."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Максималното време на достъп не може да превишава $HOURS$ час(а) и $MINUTES$ минути",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "При рестартиране"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Задайте метод за отключване, за да може да промените действието при изтичане на времето за достъп"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Надграждане"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Наистина ли искате да напуснете?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "Ако откажете, Вашите собствени елементи ще останат в акаунта Ви, но ще загубите достъп до споделените елементи и функционалностите на организацията."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Свържете се с администратор, за да получите достъп отново."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Напускане на $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "Как да управлявам трезора си?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Прехвърляне на елементи към $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ изисква всички елементи да станат притежание на организацията, за по-добра сигурност и съвместимост. Изберете, че приемате, за да прехвърлите собствеността на елементите си към организацията.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Приемане на прехвърлянето"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Отказване и напускане"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Защо виждам това?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "একটি অপ্রত্যাশিত ত্রুটি ঘটেছে।"
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "বস্তু তথ্য"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Follow us"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "ভল্ট সিঙ্ক করুন"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "মূল পাসওয়ার্ড পরিবর্তন"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "ভল্ট রফতানি"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "ফাইলের ধরণ"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Master password removed"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Import error"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Neočekivana greška se dogodila."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Informacije o stavki"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Pratite nas"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Sinhronizujte trezor sada"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Promijenite glavnu lozinku"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Izvezi trezor"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Format datoteke"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Master password removed"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Uvoz podataka",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Greška pri uvozu"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Afig adjunt"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "La mida màxima del fitxer és de 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "S'ha produït un error inesperat."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Informació de l'element"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Seguiu-nos"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Sincronitza la caixa forta"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Canvia la contrasenya mestra"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Exporta des de"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Exporta caixa forta"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Format de fitxer"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "S'ha suprimit la contrasenya mestra."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "Ja no cal contrasenya mestra per als membres de la següent organització. Confirmeu el domini següent amb l'administrador de l'organització."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Nom de l'organització"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Domini d'àlies"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Importa dades",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Error d'importació"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Permet capturar la pantalla"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Přidat přílohu"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Převedené položky"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Opravit šifrování"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "Tento soubor používá zastaralou šifrovací metodu."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Příloha byla aktualizována"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximální velikost souboru je 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Vyskytla se neočekávaná chyba."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Neočekávaná chyba"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Zavřete toto okno Bitwardenu a zkuste to znovu."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Informace o položce"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Sledujte nás"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Synchronizovat trezor"
|
||||
"syncNow": {
|
||||
"message": "Synchronizovat nyní"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Změnit hlavní heslo"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Exportovat z"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Exportovat trezor"
|
||||
"export": {
|
||||
"message": "Exportovat"
|
||||
},
|
||||
"import": {
|
||||
"message": "Importovat"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Formát souboru"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Hlavní heslo bylo odebráno"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "Hlavní heslo již není vyžadováno pro členy následující organizace. Potvrďte níže uvedenou doménu u správce Vaší organizace."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Název organizace"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Doména aliasu"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Importovat data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Chyba importu"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Soubor byl uložen. Můžete jej nalézt ve stažené složce v zařízení."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Důležité upozornění"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Nastavit dvoufázové přihlášení"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden odešle kód na e-mail Vašeho účtu pro ověření přihlášení z nových zařízení počínaje únorem 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "Dvoufázové přihlášení můžete nastavit jako alternativní způsob ochrany Vašeho účtu nebo změnit svůj e-mail na ten, k němuž můžete přistupovat."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Připomenout později"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Máte spolehlivý přístup ke svému e-mailu $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "Ne, nemám"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Ano, ke svému e-mailu mám přístup"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Zapnout dvoufázové přihlášení"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Změnit e-mail účtu"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Přihlásit se pomocí přístupového klíče?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Uložit přístupový klíč?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Uložit jako nové přihlašovací údaje"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Uložit přístupový klíč jako nové přihlášení"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "Žádné odpovídající přihlašovací údaje pro tento web"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Přepsat přístupový klíč?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Nelze uložit přístupový klíč"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "Tato položka již obsahuje přístupový klíč. Jste si jisti, že chcete přepsat aktuální přístupový klíč?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "Přístupový klíč pro tuto aplikaci již existuje."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "Tato aplikace nepodporuje duplikáty."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Zavřít toto okno"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Povolit záznam obrazovky"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "A ještě více!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Dokončit online zabezpečení"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Pokročilé zabezpečení online"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Aktualizovat na Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Vaše organizace již k přihlášení do Bitwardenu nepoužívá hlavní hesla. Chcete-li pokračovat, ověřte organizaci a doménu."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Pokračovat s přihlášením"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Nepokračovat"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Doména"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "Tato doména uloží šifrovací klíče Vašeho účtu, takže se ujistěte, že jí věříte. Pokud si nejste jisti, kontaktujte Vašeho správce."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Ověřte svou organizaci pro přihlášení"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organizace byla ověřena"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Doména byla ověřena"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "Pokud neověříte svou organizaci, Váš přístup k organizaci bude zrušen."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Opustit hned"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Ověřte svou doménu pro přihlášení"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "Chcete-li pokračovat v přihlášení, ověřte tuto doménu."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "Chcete-li pokračovat v přihlášení, ověřte organizaci a doménu."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Akce vypršení časového limitu"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Časový limit relace"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "Tato nastavení je spravováno Vaší organizací."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Vaše organizace nastavila maximální časový limit relace na $HOURS$ hodin a $MINUTES$ minut.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Vaše organizace nastavila výchozí časový limit relace na Při uzamknutí systému."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Vaše organizace nastavila výchozí časový limit relace na Při restartu."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximální časový limit nesmí překročit $HOURS$ hodin a $MINUTES$ minut",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "Při restartu"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Nastavte metodu odemknutí, abyste změnili akci při vypršení časového limitu"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Aktualizovat"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Opravdu chcete odejít?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "Odmítnutím zůstanou Vaše osobní položky ve Vašem účtu, ale ztratíte přístup ke sdíleným položkám a funkcím organizace."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Obraťte se na svého správce, abyste znovu získali přístup."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Opustit $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "Jak mohu spravovat svůj trezor?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Přenést položky do $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ vyžaduje, aby byly všechny položky vlastněny organizací z důvodu bezpečnosti a shody. Klepnutím na tlačítko pro převod vlastnictví Vašich položek.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Přijmout převod"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Odmítnout a odejít"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Proč se mi toto zobrazuje?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "An unexpected error has occurred."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Item information"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Follow us"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Sync vault"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Change master password"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export from"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Export vault"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "File format"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Master password removed"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Import data",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Import error"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "File saved to device. Manage from your device downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Add attachment"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Maximum file size is 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "En uventet fejl opstod."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Emneinformation"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Følg os"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Synk boks"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Skift hovedadgangskode"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Eksportér fra"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Eksportér boks"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Filformat"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Hovedadgangskode fjernet."
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Aliasdomæne"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Dataimport",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Importfejl"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Fil gemt på enheden. Håndtér fra enhedens downloads."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
}
|
||||
},
|
||||
"noEditPermissions": {
|
||||
"message": "Keine Berechtigung zum Bearbeiten dieses Eintrags"
|
||||
"message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"message": "Willkommen zurück"
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Anhang hinzufügen"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Einträge wurden übertragen"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Verschlüsselung reparieren"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "Diese Datei verwendet eine veraltete Verschlüsselungsmethode."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Anhang aktualisiert"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Die maximale Dateigröße beträgt 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Ein unerwarteter Fehler ist aufgetreten."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unerwarteter Fehler"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Schließe dieses Bitwarden-Fenster und versuche es erneut."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Eintragsinformationen"
|
||||
},
|
||||
@@ -1094,22 +1112,22 @@
|
||||
"message": "Mehr erfahren"
|
||||
},
|
||||
"migrationsFailed": {
|
||||
"message": "An error occurred updating the encryption settings."
|
||||
"message": "Beim Aktualisieren der Verschlüsselungseinstellungen ist ein Fehler aufgetreten."
|
||||
},
|
||||
"updateEncryptionSettingsTitle": {
|
||||
"message": "Update your encryption settings"
|
||||
"message": "Aktualisiere deine Verschlüsselungseinstellungen"
|
||||
},
|
||||
"updateEncryptionSettingsDesc": {
|
||||
"message": "The new recommended encryption settings will improve your account security. Enter your master password to update now."
|
||||
"message": "Die neuen empfohlenen Verschlüsselungseinstellungen verbessern deine Kontosicherheit. Gib dein Master-Passwort ein, um sie zu aktualisieren."
|
||||
},
|
||||
"confirmIdentityToContinue": {
|
||||
"message": "Confirm your identity to continue"
|
||||
"message": "Bestätige deine Identität, um fortzufahren"
|
||||
},
|
||||
"enterYourMasterPassword": {
|
||||
"message": "Enter your master password"
|
||||
"message": "Gib dein Master-Passwort ein"
|
||||
},
|
||||
"updateSettings": {
|
||||
"message": "Update settings"
|
||||
"message": "Einstellungen aktualisieren"
|
||||
},
|
||||
"featureUnavailable": {
|
||||
"message": "Funktion nicht verfügbar"
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Folge uns"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Tresor synchronisieren"
|
||||
"syncNow": {
|
||||
"message": "Jetzt synchronisieren"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Master-Passwort ändern"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Export aus"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Tresor exportieren"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Dateiformat"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Master-Passwort entfernt"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "Für Mitglieder der folgenden Organisation ist kein Master-Passwort mehr erforderlich. Bitte bestätige die folgende Domain bei deinem Organisations-Administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Name der Organisation"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Alias-Domain"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Daten importieren",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Importfehler"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Datei auf Gerät gespeichert. Greife darauf über die Downloads deines Geräts zu."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Wichtiger Hinweis"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Zwei-Faktor-Authentifizierung einrichten"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Ab Februar 2025 wird Bitwarden einen Code an deine Konto-E-Mail-Adresse senden, um Anmeldungen von neuen Geräten zu verifizieren."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "Du kannst die Zwei-Faktor-Authentifizierung als eine alternative Methode einrichten, um dein Konto zu schützen, oder deine E-Mail-Adresse zu einer anderen ändern, auf die du zugreifen kannst."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Erinnere mich später"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Hast du zuverlässigen Zugriff auf deine E-Mail-Adresse $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "Nein, habe ich nicht"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Ja, ich kann zuverlässig auf meine E-Mails zugreifen"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Zwei-Faktor-Authentifizierung aktivieren"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "E-Mail-Adresse des Kontos ändern"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Mit Passkey anmelden?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Passkey speichern?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Als neue Zugangsdaten speichern"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Passkey als neue Zugangsdaten speichern"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "Keine passenden Zugangsdaten für diese Seite"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Passkey überschreiben?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Passkey konnte nicht gespeichert werden"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "Dieser Eintrag enthält bereits einen Passkey. Bist du sicher, dass du den aktuellen Passkey überschreiben möchtest?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "Für diese Anwendung existiert bereits ein Passkey."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "Diese Anwendung unterstützt keine mehrfachen Instanzen."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Dieses Fenster schließen"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Bildschirmaufnahme erlauben"
|
||||
},
|
||||
@@ -4212,7 +4295,7 @@
|
||||
"message": "Eintrag wurde archiviert"
|
||||
},
|
||||
"itemWasUnarchived": {
|
||||
"message": "Eintrag wurde wiederhergestellt"
|
||||
"message": "Eintrag wird nicht mehr archiviert"
|
||||
},
|
||||
"archiveItem": {
|
||||
"message": "Eintrag archivieren"
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "Und vieles mehr!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Kompletter Online-Sicherheitsplan"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Erweiterte Online-Sicherheit"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Auf Premium upgraden"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Deine Organisation verwendet keine Master-Passwörter mehr, um sich bei Bitwarden anzumelden. Verifiziere die Organisation und Domain, um fortzufahren."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Mit der Anmeldung fortfahren"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Nicht fortfahren"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "Diese Domain speichert die Verschlüsselungsschlüssel deines Kontos. Stelle daher sicher, dass du ihr vertraust. Wenn du dir nicht sicher bist, wende dich an deinen Administrator."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verifiziere deine Organisation, um dich anzumelden"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organisation verifiziert"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verifiziert"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "Wenn du deine Organisation nicht verifizierst, wird dein Zugriff auf die Organisation widerrufen."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Jetzt verlassen"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verifiziere deine Domain, um dich anzumelden"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "Verifiziere diese Domain, um mit der Anmeldung fortzufahren."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "Um mit der Anmeldung fortzufahren, verifiziere die Organisation und Domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout-Aktion"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Sitzungs-Timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "Diese Einstellung wird von deiner Organisation verwaltet."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Deine Organisation hat das maximale Sitzungs-Timeout auf $HOURS$ Stunde(n) und $MINUTES$ Minute(n) festgelegt.",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Deine Organisation hat das Standard-Sitzungs-Timeout auf \"Wenn System gesperrt\" gesetzt."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Deine Organisation hat das Standard-Sitzungs-Timeout auf \"Beim Neustart der App\" gesetzt."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Das maximale Timeout darf $HOURS$ Stunde(n) und $MINUTES$ Minute(n) nicht überschreiten",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "Beim Neustart der App"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Stell eine Entsperrmethode ein, um deine Timeout-Aktion zu ändern"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Bist du sicher, dass du gehen willst?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "Wenn du ablehnst, bleiben deine persönlichen Einträge in deinem Konto erhalten, aber du wirst den Zugriff auf geteilte Einträge und Organisationsfunktionen verlieren."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Kontaktiere deinen Administrator, um wieder Zugriff zu erhalten."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "$ORGANIZATION$ verlassen",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "Wie kann ich meinen Tresor verwalten?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Einträge zu $ORGANIZATION$ übertragen",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ erfordert zur Sicherheit und Compliance, dass alle Einträge der Organisation gehören. Klicke auf Akzeptieren, um den Besitz deiner Einträge zu übertragen.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Übertragung annehmen"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Ablehnen und verlassen"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Warum wird mir das angezeigt?"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,6 +708,18 @@
|
||||
"addAttachment": {
|
||||
"message": "Προσθήκη συνημμένου"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "Items transferred"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "Fix encryption"
|
||||
},
|
||||
"fixEncryptionTooltip": {
|
||||
"message": "This file is using an outdated encryption method."
|
||||
},
|
||||
"attachmentUpdated": {
|
||||
"message": "Attachment updated"
|
||||
},
|
||||
"maxFileSizeSansPunctuation": {
|
||||
"message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB"
|
||||
},
|
||||
@@ -908,6 +920,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Πληροφορίες αντικειμένου"
|
||||
},
|
||||
@@ -1180,8 +1198,8 @@
|
||||
"followUs": {
|
||||
"message": "Ακολουθήστε μας"
|
||||
},
|
||||
"syncVault": {
|
||||
"message": "Συγχρονισμός κρύπτης"
|
||||
"syncNow": {
|
||||
"message": "Sync now"
|
||||
},
|
||||
"changeMasterPass": {
|
||||
"message": "Αλλαγή κύριου κωδικού πρόσβασης"
|
||||
@@ -1757,8 +1775,11 @@
|
||||
"exportFrom": {
|
||||
"message": "Εξαγωγή από"
|
||||
},
|
||||
"exportVault": {
|
||||
"message": "Εξαγωγή κρύπτης"
|
||||
"export": {
|
||||
"message": "Export"
|
||||
},
|
||||
"import": {
|
||||
"message": "Import"
|
||||
},
|
||||
"fileFormat": {
|
||||
"message": "Τύπος αρχείου"
|
||||
@@ -2619,9 +2640,6 @@
|
||||
"removedMasterPassword": {
|
||||
"message": "Ο κύριος κωδικός αφαιρέθηκε"
|
||||
},
|
||||
"removeMasterPasswordForOrganizationUserKeyConnector": {
|
||||
"message": "A master password is no longer required for members of the following organization. Please confirm the domain below with your organization administrator."
|
||||
},
|
||||
"organizationName": {
|
||||
"message": "Organization name"
|
||||
},
|
||||
@@ -3477,10 +3495,6 @@
|
||||
"aliasDomain": {
|
||||
"message": "Ψευδώνυμο τομέα"
|
||||
},
|
||||
"importData": {
|
||||
"message": "Εισαγωγή δεδομένων",
|
||||
"description": "Used for the desktop menu item and the header of the import dialog"
|
||||
},
|
||||
"importError": {
|
||||
"message": "Σφάλμα κατά την εισαγωγή"
|
||||
},
|
||||
@@ -3886,6 +3900,75 @@
|
||||
"fileSavedToDevice": {
|
||||
"message": "Το αρχείο αποθηκεύτηκε στη συσκευή. Διαχείριση από τις λήψεις της συσκευής σας."
|
||||
},
|
||||
"importantNotice": {
|
||||
"message": "Important notice"
|
||||
},
|
||||
"setupTwoStepLogin": {
|
||||
"message": "Set up two-step login"
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage1": {
|
||||
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
|
||||
},
|
||||
"newDeviceVerificationNoticeContentPage2": {
|
||||
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneFormContent": {
|
||||
"message": "Do you have reliable access to your email, $EMAIL$?",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "your_name@email.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessNo": {
|
||||
"message": "No, I do not"
|
||||
},
|
||||
"newDeviceVerificationNoticePageOneEmailAccessYes": {
|
||||
"message": "Yes, I can reliably access my email"
|
||||
},
|
||||
"turnOnTwoStepLogin": {
|
||||
"message": "Turn on two-step login"
|
||||
},
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"savePasskeyNewLogin": {
|
||||
"message": "Save passkey as new login"
|
||||
},
|
||||
"noMatchingLoginsForSite": {
|
||||
"message": "No matching logins for this site"
|
||||
},
|
||||
"overwritePasskey": {
|
||||
"message": "Overwrite passkey?"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"alreadyContainsPasskey": {
|
||||
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
|
||||
},
|
||||
"passkeyAlreadyExists": {
|
||||
"message": "A passkey already exists for this application."
|
||||
},
|
||||
"applicationDoesNotSupportDuplicates": {
|
||||
"message": "This application does not support duplicates."
|
||||
},
|
||||
"closeThisWindow": {
|
||||
"message": "Close this window"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
@@ -4244,16 +4327,147 @@
|
||||
"andMoreFeatures": {
|
||||
"message": "And more!"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"advancedOnlineSecurity": {
|
||||
"message": "Advanced online security"
|
||||
},
|
||||
"upgradeToPremium": {
|
||||
"message": "Upgrade to Premium"
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
},
|
||||
"sessionTimeoutSettingsAction": {
|
||||
"message": "Timeout action"
|
||||
},
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetMaximumTimeoutToHoursMinutes": {
|
||||
"message": "Your organization has set the maximum session timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "8"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnLocked": {
|
||||
"message": "Your organization has set the default session timeout to On system lock."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicySetDefaultTimeoutToOnRestart": {
|
||||
"message": "Your organization has set the default session timeout to On restart."
|
||||
},
|
||||
"sessionTimeoutSettingsPolicyMaximumError": {
|
||||
"message": "Maximum timeout cannot exceed $HOURS$ hour(s) and $MINUTES$ minute(s)",
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
"content": "$1",
|
||||
"example": "5"
|
||||
},
|
||||
"minutes": {
|
||||
"content": "$2",
|
||||
"example": "5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionTimeoutOnRestart": {
|
||||
"message": "On restart"
|
||||
},
|
||||
"sessionTimeoutSettingsSetUnlockMethodToChangeTimeoutAction": {
|
||||
"message": "Set an unlock method to change your timeout action"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade"
|
||||
},
|
||||
"leaveConfirmationDialogTitle": {
|
||||
"message": "Are you sure you want to leave?"
|
||||
},
|
||||
"leaveConfirmationDialogContentOne": {
|
||||
"message": "By declining, your personal items will stay in your account, but you'll lose access to shared items and organization features."
|
||||
},
|
||||
"leaveConfirmationDialogContentTwo": {
|
||||
"message": "Contact your admin to regain access."
|
||||
},
|
||||
"leaveConfirmationDialogConfirmButton": {
|
||||
"message": "Leave $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"howToManageMyVault": {
|
||||
"message": "How do I manage my vault?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "Transfer items to $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "$ORGANIZATION$ is requiring all items to be owned by the organization for security and compliance. Click accept to transfer ownership of your items.",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
"example": "My Org Name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "Accept transfer"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "Decline and leave"
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user