├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── assets ├── android-screenshot.png └── apple-screenshot.png ├── build.rs ├── examples └── hello_world.rs └── src ├── error.rs ├── lib.rs ├── sys.rs ├── sys ├── android.rs ├── android │ ├── AuthenticationCallback.java │ └── callback.rs ├── apple.rs ├── linux.rs ├── unsupported.rs ├── windows.rs └── windows │ └── fallback.rs └── text.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store 4 | .idea/ 5 | .vscode/ 6 | .cargo/ 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "android-build" 22 | version = "0.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "a133d38cebf328adaea4bc1891d9568e14a394b50e4f4ba5f63dc14e8beaaee9" 25 | dependencies = [ 26 | "windows-sys 0.52.0", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.80" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 34 | 35 | [[package]] 36 | name = "autocfg" 37 | version = "1.1.0" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 40 | 41 | [[package]] 42 | name = "backtrace" 43 | version = "0.3.69" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 46 | dependencies = [ 47 | "addr2line", 48 | "cc", 49 | "cfg-if", 50 | "libc", 51 | "miniz_oxide", 52 | "object", 53 | "rustc-demangle", 54 | ] 55 | 56 | [[package]] 57 | name = "bitflags" 58 | version = "1.3.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 61 | 62 | [[package]] 63 | name = "bitflags" 64 | version = "2.5.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 67 | 68 | [[package]] 69 | name = "block2" 70 | version = "0.5.1" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" 73 | dependencies = [ 74 | "objc2", 75 | ] 76 | 77 | [[package]] 78 | name = "bytes" 79 | version = "1.5.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 82 | 83 | [[package]] 84 | name = "cc" 85 | version = "1.0.83" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 88 | dependencies = [ 89 | "libc", 90 | ] 91 | 92 | [[package]] 93 | name = "cesu8" 94 | version = "1.1.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 97 | 98 | [[package]] 99 | name = "cfg-expr" 100 | version = "0.15.7" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" 103 | dependencies = [ 104 | "smallvec", 105 | "target-lexicon", 106 | ] 107 | 108 | [[package]] 109 | name = "cfg-if" 110 | version = "1.0.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 113 | 114 | [[package]] 115 | name = "combine" 116 | version = "4.6.6" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 119 | dependencies = [ 120 | "bytes", 121 | "memchr", 122 | ] 123 | 124 | [[package]] 125 | name = "equivalent" 126 | version = "1.0.1" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 129 | 130 | [[package]] 131 | name = "futures-channel" 132 | version = "0.3.30" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 135 | dependencies = [ 136 | "futures-core", 137 | ] 138 | 139 | [[package]] 140 | name = "futures-core" 141 | version = "0.3.30" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 144 | 145 | [[package]] 146 | name = "futures-executor" 147 | version = "0.3.30" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 150 | dependencies = [ 151 | "futures-core", 152 | "futures-task", 153 | "futures-util", 154 | ] 155 | 156 | [[package]] 157 | name = "futures-io" 158 | version = "0.3.30" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 161 | 162 | [[package]] 163 | name = "futures-macro" 164 | version = "0.3.30" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 167 | dependencies = [ 168 | "proc-macro2", 169 | "quote", 170 | "syn 2.0.49", 171 | ] 172 | 173 | [[package]] 174 | name = "futures-task" 175 | version = "0.3.30" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 178 | 179 | [[package]] 180 | name = "futures-util" 181 | version = "0.3.30" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 184 | dependencies = [ 185 | "futures-core", 186 | "futures-macro", 187 | "futures-task", 188 | "pin-project-lite", 189 | "pin-utils", 190 | "slab", 191 | ] 192 | 193 | [[package]] 194 | name = "getrandom" 195 | version = "0.2.14" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 198 | dependencies = [ 199 | "cfg-if", 200 | "libc", 201 | "wasi", 202 | ] 203 | 204 | [[package]] 205 | name = "gimli" 206 | version = "0.28.1" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 209 | 210 | [[package]] 211 | name = "gio" 212 | version = "0.17.0" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "1981edf8679d2f2c8ec3120015867f45aa0a1c2d5e3e129ca2f7dda174d3d2a9" 215 | dependencies = [ 216 | "bitflags 1.3.2", 217 | "futures-channel", 218 | "futures-core", 219 | "futures-io", 220 | "futures-util", 221 | "gio-sys", 222 | "glib", 223 | "libc", 224 | "once_cell", 225 | "pin-project-lite", 226 | "smallvec", 227 | "thiserror", 228 | ] 229 | 230 | [[package]] 231 | name = "gio-sys" 232 | version = "0.17.10" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" 235 | dependencies = [ 236 | "glib-sys", 237 | "gobject-sys", 238 | "libc", 239 | "system-deps", 240 | "winapi", 241 | ] 242 | 243 | [[package]] 244 | name = "glib" 245 | version = "0.17.10" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" 248 | dependencies = [ 249 | "bitflags 1.3.2", 250 | "futures-channel", 251 | "futures-core", 252 | "futures-executor", 253 | "futures-task", 254 | "futures-util", 255 | "gio-sys", 256 | "glib-macros", 257 | "glib-sys", 258 | "gobject-sys", 259 | "libc", 260 | "memchr", 261 | "once_cell", 262 | "smallvec", 263 | "thiserror", 264 | ] 265 | 266 | [[package]] 267 | name = "glib-macros" 268 | version = "0.17.10" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" 271 | dependencies = [ 272 | "anyhow", 273 | "heck", 274 | "proc-macro-crate", 275 | "proc-macro-error", 276 | "proc-macro2", 277 | "quote", 278 | "syn 1.0.109", 279 | ] 280 | 281 | [[package]] 282 | name = "glib-sys" 283 | version = "0.17.10" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" 286 | dependencies = [ 287 | "libc", 288 | "system-deps", 289 | ] 290 | 291 | [[package]] 292 | name = "gobject-sys" 293 | version = "0.17.10" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" 296 | dependencies = [ 297 | "glib-sys", 298 | "libc", 299 | "system-deps", 300 | ] 301 | 302 | [[package]] 303 | name = "hashbrown" 304 | version = "0.14.3" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 307 | 308 | [[package]] 309 | name = "heck" 310 | version = "0.4.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 313 | 314 | [[package]] 315 | name = "indexmap" 316 | version = "2.2.5" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" 319 | dependencies = [ 320 | "equivalent", 321 | "hashbrown", 322 | ] 323 | 324 | [[package]] 325 | name = "jni" 326 | version = "0.21.1" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 329 | dependencies = [ 330 | "cesu8", 331 | "cfg-if", 332 | "combine", 333 | "jni-sys", 334 | "log", 335 | "thiserror", 336 | "walkdir", 337 | "windows-sys 0.45.0", 338 | ] 339 | 340 | [[package]] 341 | name = "jni-sys" 342 | version = "0.3.0" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 345 | 346 | [[package]] 347 | name = "libc" 348 | version = "0.2.152" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 351 | 352 | [[package]] 353 | name = "log" 354 | version = "0.4.21" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 357 | 358 | [[package]] 359 | name = "memchr" 360 | version = "2.7.1" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 363 | 364 | [[package]] 365 | name = "miniz_oxide" 366 | version = "0.7.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 369 | dependencies = [ 370 | "adler", 371 | ] 372 | 373 | [[package]] 374 | name = "ndk-context" 375 | version = "0.1.1" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 378 | 379 | [[package]] 380 | name = "objc-sys" 381 | version = "0.3.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" 384 | 385 | [[package]] 386 | name = "objc2" 387 | version = "0.5.2" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" 390 | dependencies = [ 391 | "objc-sys", 392 | "objc2-encode", 393 | ] 394 | 395 | [[package]] 396 | name = "objc2-encode" 397 | version = "4.0.3" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" 400 | 401 | [[package]] 402 | name = "objc2-foundation" 403 | version = "0.2.2" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" 406 | dependencies = [ 407 | "bitflags 2.5.0", 408 | "block2", 409 | "libc", 410 | "objc2", 411 | ] 412 | 413 | [[package]] 414 | name = "objc2-local-authentication" 415 | version = "0.2.2" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "430605e43490dc3837b7d50d8daedacb9f7926da3935a8cd09651a6a9d071b71" 418 | dependencies = [ 419 | "block2", 420 | "objc2", 421 | "objc2-foundation", 422 | ] 423 | 424 | [[package]] 425 | name = "object" 426 | version = "0.32.2" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 429 | dependencies = [ 430 | "memchr", 431 | ] 432 | 433 | [[package]] 434 | name = "once_cell" 435 | version = "1.19.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 438 | 439 | [[package]] 440 | name = "pin-project-lite" 441 | version = "0.2.13" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 444 | 445 | [[package]] 446 | name = "pin-utils" 447 | version = "0.1.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 450 | 451 | [[package]] 452 | name = "pkg-config" 453 | version = "0.3.30" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 456 | 457 | [[package]] 458 | name = "polkit" 459 | version = "0.17.0" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "f7866121c1e115212fd6e4eca8f84e03af65eda1a3d57babf849a946c791559c" 462 | dependencies = [ 463 | "bitflags 1.3.2", 464 | "gio", 465 | "glib", 466 | "polkit-sys", 467 | ] 468 | 469 | [[package]] 470 | name = "polkit-sys" 471 | version = "0.17.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "9277a6580d2cd5b54f9dc428d4ee720e46ca6ba2e7a8b44c26282dbef6ecedf2" 474 | dependencies = [ 475 | "gio-sys", 476 | "glib-sys", 477 | "gobject-sys", 478 | "libc", 479 | "system-deps", 480 | ] 481 | 482 | [[package]] 483 | name = "ppv-lite86" 484 | version = "0.2.17" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 487 | 488 | [[package]] 489 | name = "proc-macro-crate" 490 | version = "1.3.1" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 493 | dependencies = [ 494 | "once_cell", 495 | "toml_edit 0.19.15", 496 | ] 497 | 498 | [[package]] 499 | name = "proc-macro-error" 500 | version = "1.0.4" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 503 | dependencies = [ 504 | "proc-macro-error-attr", 505 | "proc-macro2", 506 | "quote", 507 | "syn 1.0.109", 508 | "version_check", 509 | ] 510 | 511 | [[package]] 512 | name = "proc-macro-error-attr" 513 | version = "1.0.4" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "version_check", 520 | ] 521 | 522 | [[package]] 523 | name = "proc-macro2" 524 | version = "1.0.78" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 527 | dependencies = [ 528 | "unicode-ident", 529 | ] 530 | 531 | [[package]] 532 | name = "quote" 533 | version = "1.0.35" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 536 | dependencies = [ 537 | "proc-macro2", 538 | ] 539 | 540 | [[package]] 541 | name = "rand" 542 | version = "0.8.5" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 545 | dependencies = [ 546 | "libc", 547 | "rand_chacha", 548 | "rand_core", 549 | ] 550 | 551 | [[package]] 552 | name = "rand_chacha" 553 | version = "0.3.1" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 556 | dependencies = [ 557 | "ppv-lite86", 558 | "rand_core", 559 | ] 560 | 561 | [[package]] 562 | name = "rand_core" 563 | version = "0.6.4" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 566 | dependencies = [ 567 | "getrandom", 568 | ] 569 | 570 | [[package]] 571 | name = "retry" 572 | version = "2.0.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4" 575 | dependencies = [ 576 | "rand", 577 | ] 578 | 579 | [[package]] 580 | name = "robius-android-env" 581 | version = "0.2.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "087fcb3061ccc432658a605cb868edd44e0efb08e7a159b486f02804a7616bef" 584 | dependencies = [ 585 | "jni", 586 | "ndk-context", 587 | ] 588 | 589 | [[package]] 590 | name = "robius-authentication" 591 | version = "0.1.1" 592 | dependencies = [ 593 | "android-build", 594 | "block2", 595 | "cfg-if", 596 | "gio", 597 | "jni", 598 | "log", 599 | "objc2", 600 | "objc2-foundation", 601 | "objc2-local-authentication", 602 | "polkit", 603 | "retry", 604 | "robius-android-env", 605 | "tokio", 606 | "windows", 607 | "windows-core", 608 | ] 609 | 610 | [[package]] 611 | name = "rustc-demangle" 612 | version = "0.1.23" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 615 | 616 | [[package]] 617 | name = "same-file" 618 | version = "1.0.6" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 621 | dependencies = [ 622 | "winapi-util", 623 | ] 624 | 625 | [[package]] 626 | name = "serde" 627 | version = "1.0.196" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 630 | dependencies = [ 631 | "serde_derive", 632 | ] 633 | 634 | [[package]] 635 | name = "serde_derive" 636 | version = "1.0.196" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 639 | dependencies = [ 640 | "proc-macro2", 641 | "quote", 642 | "syn 2.0.49", 643 | ] 644 | 645 | [[package]] 646 | name = "serde_spanned" 647 | version = "0.6.5" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 650 | dependencies = [ 651 | "serde", 652 | ] 653 | 654 | [[package]] 655 | name = "slab" 656 | version = "0.4.9" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 659 | dependencies = [ 660 | "autocfg", 661 | ] 662 | 663 | [[package]] 664 | name = "smallvec" 665 | version = "1.13.1" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 668 | 669 | [[package]] 670 | name = "syn" 671 | version = "1.0.109" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 674 | dependencies = [ 675 | "proc-macro2", 676 | "quote", 677 | "unicode-ident", 678 | ] 679 | 680 | [[package]] 681 | name = "syn" 682 | version = "2.0.49" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" 685 | dependencies = [ 686 | "proc-macro2", 687 | "quote", 688 | "unicode-ident", 689 | ] 690 | 691 | [[package]] 692 | name = "system-deps" 693 | version = "6.2.0" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" 696 | dependencies = [ 697 | "cfg-expr", 698 | "heck", 699 | "pkg-config", 700 | "toml", 701 | "version-compare", 702 | ] 703 | 704 | [[package]] 705 | name = "target-lexicon" 706 | version = "0.12.14" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" 709 | 710 | [[package]] 711 | name = "thiserror" 712 | version = "1.0.57" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 715 | dependencies = [ 716 | "thiserror-impl", 717 | ] 718 | 719 | [[package]] 720 | name = "thiserror-impl" 721 | version = "1.0.57" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 724 | dependencies = [ 725 | "proc-macro2", 726 | "quote", 727 | "syn 2.0.49", 728 | ] 729 | 730 | [[package]] 731 | name = "tokio" 732 | version = "1.35.1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" 735 | dependencies = [ 736 | "backtrace", 737 | "pin-project-lite", 738 | ] 739 | 740 | [[package]] 741 | name = "toml" 742 | version = "0.8.10" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 745 | dependencies = [ 746 | "serde", 747 | "serde_spanned", 748 | "toml_datetime", 749 | "toml_edit 0.22.6", 750 | ] 751 | 752 | [[package]] 753 | name = "toml_datetime" 754 | version = "0.6.5" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 757 | dependencies = [ 758 | "serde", 759 | ] 760 | 761 | [[package]] 762 | name = "toml_edit" 763 | version = "0.19.15" 764 | source = "registry+https://github.com/rust-lang/crates.io-index" 765 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 766 | dependencies = [ 767 | "indexmap", 768 | "toml_datetime", 769 | "winnow 0.5.40", 770 | ] 771 | 772 | [[package]] 773 | name = "toml_edit" 774 | version = "0.22.6" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" 777 | dependencies = [ 778 | "indexmap", 779 | "serde", 780 | "serde_spanned", 781 | "toml_datetime", 782 | "winnow 0.6.5", 783 | ] 784 | 785 | [[package]] 786 | name = "unicode-ident" 787 | version = "1.0.12" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 790 | 791 | [[package]] 792 | name = "version-compare" 793 | version = "0.1.1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" 796 | 797 | [[package]] 798 | name = "version_check" 799 | version = "0.9.4" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 802 | 803 | [[package]] 804 | name = "walkdir" 805 | version = "2.4.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 808 | dependencies = [ 809 | "same-file", 810 | "winapi-util", 811 | ] 812 | 813 | [[package]] 814 | name = "wasi" 815 | version = "0.11.0+wasi-snapshot-preview1" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 818 | 819 | [[package]] 820 | name = "winapi" 821 | version = "0.3.9" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 824 | dependencies = [ 825 | "winapi-i686-pc-windows-gnu", 826 | "winapi-x86_64-pc-windows-gnu", 827 | ] 828 | 829 | [[package]] 830 | name = "winapi-i686-pc-windows-gnu" 831 | version = "0.4.0" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 834 | 835 | [[package]] 836 | name = "winapi-util" 837 | version = "0.1.6" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 840 | dependencies = [ 841 | "winapi", 842 | ] 843 | 844 | [[package]] 845 | name = "winapi-x86_64-pc-windows-gnu" 846 | version = "0.4.0" 847 | source = "registry+https://github.com/rust-lang/crates.io-index" 848 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 849 | 850 | [[package]] 851 | name = "windows" 852 | version = "0.56.0" 853 | source = "registry+https://github.com/rust-lang/crates.io-index" 854 | checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" 855 | dependencies = [ 856 | "windows-core", 857 | "windows-targets 0.52.5", 858 | ] 859 | 860 | [[package]] 861 | name = "windows-core" 862 | version = "0.56.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" 865 | dependencies = [ 866 | "windows-implement", 867 | "windows-interface", 868 | "windows-result", 869 | "windows-targets 0.52.5", 870 | ] 871 | 872 | [[package]] 873 | name = "windows-implement" 874 | version = "0.56.0" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" 877 | dependencies = [ 878 | "proc-macro2", 879 | "quote", 880 | "syn 2.0.49", 881 | ] 882 | 883 | [[package]] 884 | name = "windows-interface" 885 | version = "0.56.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" 888 | dependencies = [ 889 | "proc-macro2", 890 | "quote", 891 | "syn 2.0.49", 892 | ] 893 | 894 | [[package]] 895 | name = "windows-result" 896 | version = "0.1.1" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b" 899 | dependencies = [ 900 | "windows-targets 0.52.5", 901 | ] 902 | 903 | [[package]] 904 | name = "windows-sys" 905 | version = "0.45.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 908 | dependencies = [ 909 | "windows-targets 0.42.2", 910 | ] 911 | 912 | [[package]] 913 | name = "windows-sys" 914 | version = "0.52.0" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 917 | dependencies = [ 918 | "windows-targets 0.52.5", 919 | ] 920 | 921 | [[package]] 922 | name = "windows-targets" 923 | version = "0.42.2" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 926 | dependencies = [ 927 | "windows_aarch64_gnullvm 0.42.2", 928 | "windows_aarch64_msvc 0.42.2", 929 | "windows_i686_gnu 0.42.2", 930 | "windows_i686_msvc 0.42.2", 931 | "windows_x86_64_gnu 0.42.2", 932 | "windows_x86_64_gnullvm 0.42.2", 933 | "windows_x86_64_msvc 0.42.2", 934 | ] 935 | 936 | [[package]] 937 | name = "windows-targets" 938 | version = "0.52.5" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 941 | dependencies = [ 942 | "windows_aarch64_gnullvm 0.52.5", 943 | "windows_aarch64_msvc 0.52.5", 944 | "windows_i686_gnu 0.52.5", 945 | "windows_i686_gnullvm", 946 | "windows_i686_msvc 0.52.5", 947 | "windows_x86_64_gnu 0.52.5", 948 | "windows_x86_64_gnullvm 0.52.5", 949 | "windows_x86_64_msvc 0.52.5", 950 | ] 951 | 952 | [[package]] 953 | name = "windows_aarch64_gnullvm" 954 | version = "0.42.2" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 957 | 958 | [[package]] 959 | name = "windows_aarch64_gnullvm" 960 | version = "0.52.5" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 963 | 964 | [[package]] 965 | name = "windows_aarch64_msvc" 966 | version = "0.42.2" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 969 | 970 | [[package]] 971 | name = "windows_aarch64_msvc" 972 | version = "0.52.5" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 975 | 976 | [[package]] 977 | name = "windows_i686_gnu" 978 | version = "0.42.2" 979 | source = "registry+https://github.com/rust-lang/crates.io-index" 980 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 981 | 982 | [[package]] 983 | name = "windows_i686_gnu" 984 | version = "0.52.5" 985 | source = "registry+https://github.com/rust-lang/crates.io-index" 986 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 987 | 988 | [[package]] 989 | name = "windows_i686_gnullvm" 990 | version = "0.52.5" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 993 | 994 | [[package]] 995 | name = "windows_i686_msvc" 996 | version = "0.42.2" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 999 | 1000 | [[package]] 1001 | name = "windows_i686_msvc" 1002 | version = "0.52.5" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1005 | 1006 | [[package]] 1007 | name = "windows_x86_64_gnu" 1008 | version = "0.42.2" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1011 | 1012 | [[package]] 1013 | name = "windows_x86_64_gnu" 1014 | version = "0.52.5" 1015 | source = "registry+https://github.com/rust-lang/crates.io-index" 1016 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1017 | 1018 | [[package]] 1019 | name = "windows_x86_64_gnullvm" 1020 | version = "0.42.2" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1023 | 1024 | [[package]] 1025 | name = "windows_x86_64_gnullvm" 1026 | version = "0.52.5" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1029 | 1030 | [[package]] 1031 | name = "windows_x86_64_msvc" 1032 | version = "0.42.2" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1035 | 1036 | [[package]] 1037 | name = "windows_x86_64_msvc" 1038 | version = "0.52.5" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1041 | 1042 | [[package]] 1043 | name = "winnow" 1044 | version = "0.5.40" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 1047 | dependencies = [ 1048 | "memchr", 1049 | ] 1050 | 1051 | [[package]] 1052 | name = "winnow" 1053 | version = "0.6.5" 1054 | source = "registry+https://github.com/rust-lang/crates.io-index" 1055 | checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" 1056 | dependencies = [ 1057 | "memchr", 1058 | ] 1059 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "robius-authentication" 3 | version = "0.1.1" 4 | edition = "2021" 5 | authors = [ 6 | "Klim Tsoutsman ", 7 | "Kevin Boos ", 8 | "Project Robius Maintainers", 9 | ] 10 | description = "Rust abstractions for multi-platform native authentication: biometrics, fingerprint, password, TouchID, FaceID, Windows Hello, etc." 11 | documentation = "https://docs.rs/robius-authentication" 12 | homepage = "https://robius.rs/" 13 | keywords = ["robius", "authentication", "biometric", "password", "fingerprint"] 14 | categories = ["os", "hardware-support", "api-bindings"] 15 | license = "MIT" 16 | readme = "README.md" 17 | repository = "https://github.com/project-robius/robius-authentication" 18 | 19 | [build-dependencies] 20 | android-build = "0.1.0" 21 | 22 | [dependencies] 23 | cfg-if = "1.0.0" 24 | 25 | [target.'cfg(target_os = "android")'.dependencies] 26 | jni = "0.21.1" 27 | log = "0.4.21" 28 | robius-android-env = "0.2.0" 29 | 30 | [target.'cfg(target_vendor = "apple")'.dependencies.block2] 31 | version = "0.5.1" 32 | 33 | [target.'cfg(any(target_vendor = "apple", target_os = "android"))'.dependencies.tokio] 34 | version = "1.35.1" 35 | default-features = false 36 | features = ["sync"] 37 | optional = true 38 | 39 | [target.'cfg(target_vendor = "apple")'.dependencies.objc2-local-authentication] 40 | version = "0.2.2" 41 | features = ["block2", "LAContext", "LAError"] 42 | 43 | [target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation] 44 | version = "0.2.2" 45 | default-features = false 46 | features = ["NSError", "NSString"] 47 | 48 | [target.'cfg(target_vendor = "apple")'.dependencies.objc2] 49 | version = "0.5.2" 50 | default-features = false 51 | 52 | [target.'cfg(target_os = "linux")'.dependencies.polkit] 53 | version = "=0.17.0" 54 | 55 | [target.'cfg(target_os = "linux")'.dependencies.gio] 56 | version = "=0.17.0" 57 | 58 | [target.'cfg(target_os = "windows")'.dependencies.retry] 59 | version = "2.0.0" 60 | 61 | [target.'cfg(target_os = "windows")'.dependencies.windows] 62 | version = "0.56.0" 63 | features = [ 64 | # For UWP-based authentication. 65 | "Foundation", 66 | "Security_Credentials_UI", 67 | # WinRT 68 | "Win32_UI_WindowsAndMessaging", 69 | "Win32_System_WinRT", 70 | # Fallback 71 | "Win32_Foundation", 72 | "Win32_Graphics_Gdi", 73 | "Win32_NetworkManagement_NetManagement", 74 | "Win32_Security_Authentication_Identity", 75 | "Win32_Security_Credentials", 76 | "Win32_UI_Input_KeyboardAndMouse", 77 | ] 78 | 79 | [target.'cfg(target_os = "windows")'.dependencies.windows-core] 80 | version = "0.56.0" 81 | default-features = false 82 | 83 | [features] 84 | default = [] 85 | ## Enable this feature to expose non-blocking asynchronous authentication APIs. 86 | async = ["dep:tokio"] 87 | ## Note: there is a UWP feature still in the code, 88 | ## but enabling it causes the app to freeze on Windows 11 Pro. 89 | ## Everything still works correctly without the UWP feature. 90 | # uwp = [] 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `robius-authentication` 2 | 3 | [![Latest Version](https://img.shields.io/crates/v/robius-authentication.svg)](https://crates.io/crates/robius_authentication) 4 | [![Docs](https://docs.rs/robius-authentication/badge.svg)](https://docs.rs/robius-authentication/latest/robius_authentication/) 5 | [![Project Robius Matrix Chat](https://img.shields.io/matrix/robius-general%3Amatrix.org?server_fqdn=matrix.org&style=flat&logo=matrix&label=Project%20Robius%20Matrix%20Chat&color=B7410E)](https://matrix.to/#/#robius:matrix.org) 6 | 7 | Rust abstractions for multi-platform native authentication. 8 | 9 | This crate supports: 10 | * Apple: TouchID, FaceID, and regular username/password on both macOS and iOS. 11 | * Android: See below for additional steps. 12 | * Requires the `USE_BIOMETRIC` permission in your app's manifest. 13 | * Windows: Windows Hello (face recognition, fingerprint, PIN), 14 | plus winrt-based fallback for username/password. 15 | * Linux: [`polkit`]-based authentication using the desktop environment's prompt. 16 | * **Note: Linux support is currently incomplete.** 17 | 18 | ## Usage on Android 19 | 20 | For authentication to work, the following must be added to your app's 21 | `AndroidManifest.xml`: 22 | ```xml 23 | 24 | ``` 25 | 26 | ## Example 27 | 28 | ```no_run 29 | use robius_authentication::{ 30 | AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText, 31 | }; 32 | 33 | let policy: Policy = PolicyBuilder::new() 34 | .biometrics(Some(BiometricStrength::Strong)) 35 | .password(true) 36 | .watch(true) 37 | .build() 38 | .unwrap(); 39 | 40 | let text = Text { 41 | android: AndroidText { 42 | title: "Title", 43 | subtitle: None, 44 | description: None, 45 | }, 46 | apple: "authenticate", 47 | windows: WindowsText::new("Title", "Description"), 48 | }; 49 | 50 | let auth_result = Context::new(()).blocking_authenticate(text, &policy); 51 | ... 52 | ``` 53 | 54 | For more details about the prompt text, see the `Text` struct, 55 | which allows you to customize the prompt for each platform. 56 | 57 | [`polkit`]: https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html 58 | -------------------------------------------------------------------------------- /assets/android-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-robius/robius-authentication/a2786e5ba852a590383fdddfa76d8efc61e675d5/assets/android-screenshot.png -------------------------------------------------------------------------------- /assets/apple-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-robius/robius-authentication/a2786e5ba852a590383fdddfa76d8efc61e675d5/assets/apple-screenshot.png -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::PathBuf}; 2 | 3 | const JAVA_FILE_RELATIVE_PATH: &str = "src/sys/android/AuthenticationCallback.java"; 4 | 5 | fn main() { 6 | let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 7 | 8 | if target_os == "android" { 9 | println!("cargo:rerun-if-changed={JAVA_FILE_RELATIVE_PATH}"); 10 | 11 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 12 | let java_file = 13 | PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join(JAVA_FILE_RELATIVE_PATH); 14 | 15 | let android_jar_path = 16 | android_build::android_jar(None).expect("Failed to find android.jar"); 17 | 18 | // Compile the .java file into a .class file. 19 | assert!( 20 | android_build::JavaBuild::new() 21 | .class_path(android_jar_path.clone()) 22 | .classes_out_dir(out_dir.clone()) 23 | .file(java_file) 24 | .compile() 25 | .expect("failed to acquire exit status for javac invocation") 26 | .success(), 27 | "javac invocation failed" 28 | ); 29 | 30 | let class_file = out_dir 31 | .join("robius") 32 | .join("authentication") 33 | .join("AuthenticationCallback.class"); 34 | 35 | let d8_jar_path = android_build::android_d8_jar(None).expect("Failed to find d8.jar"); 36 | 37 | assert!( 38 | android_build::JavaRun::new() 39 | .class_path(d8_jar_path) 40 | .main_class("com.android.tools.r8.D8") 41 | .arg("--classpath") 42 | .arg(android_jar_path) 43 | .arg("--output") 44 | .arg(&out_dir) 45 | .arg(&class_file) 46 | .run() 47 | .expect("failed to acquire exit status for java d8.jar invocation") 48 | .success(), 49 | "java d8.jar invocation failed" 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | #![feature(const_option)] 2 | 3 | use robius_authentication::{ 4 | AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText, 5 | }; 6 | 7 | const POLICY: Policy = PolicyBuilder::new() 8 | .biometrics(Some(BiometricStrength::Strong)) 9 | .password(true) 10 | .watch(true) 11 | .build() 12 | .unwrap(); 13 | 14 | const TEXT: Text = Text { 15 | android: AndroidText { 16 | title: "Title", 17 | subtitle: None, 18 | description: None, 19 | }, 20 | apple: "authenticate", 21 | windows: WindowsText::new("Title", "Description").unwrap(), 22 | }; 23 | 24 | fn main() { 25 | let context = Context::new(()); 26 | 27 | if context.blocking_authenticate(TEXT, &POLICY).is_ok() { 28 | println!("Authorized"); 29 | } else { 30 | println!("Unauthorized"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /// The result of an authentication operation. 2 | pub type Result = std::result::Result; 3 | 4 | /// An error produced during authentication. 5 | #[derive(Debug)] 6 | pub enum Error { 7 | // TODO: Reexport jni::errors::Error 8 | // TODO: Remove target cfg 9 | #[cfg(target_os = "android")] 10 | Java(jni::errors::Error), 11 | 12 | // Common errors 13 | /// The user failed to provide valid credentials. 14 | Authentication, 15 | /// Authentication failed because there were too many failed attempts. 16 | #[doc(alias = "lockout")] 17 | Exhausted, 18 | /// The requested authentication method was unavailable. 19 | Unavailable, 20 | /// The user canceled authentication. 21 | UserCanceled, 22 | 23 | // Apple-specific errors 24 | /// The app canceled authentication. 25 | /// 26 | /// This error can occur on: 27 | /// - [Apple] 28 | /// 29 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorappcancel 30 | AppCanceled, 31 | /// The system canceled authentication. 32 | /// 33 | /// This error can occur on: 34 | /// - [Apple] 35 | /// 36 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorsystemcancel 37 | SystemCanceled, 38 | /// The device supports biometry only using a removable accessory, but the 39 | /// paired accessory isn’t connected. 40 | /// 41 | /// This error can occur on: 42 | /// - [Apple] 43 | /// 44 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorbiometrydisconnected 45 | BiometryDisconnected, 46 | /// The device supports biometry only using a removable accessory, but no 47 | /// accessory is paired. 48 | /// 49 | /// This error can occur on: 50 | /// - [Apple] 51 | /// 52 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorbiometrynotpaired 53 | NotPaired, 54 | /// The user has no enrolled biometric identities. 55 | /// 56 | /// This error can occur on: 57 | /// - [Apple] 58 | /// 59 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorbiometrynotenrolled 60 | NotEnrolled, 61 | /// Displaying the required authentication user interface is forbidden. 62 | /// 63 | /// This error can occur on: 64 | /// - [Apple] 65 | /// 66 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrornotinteractive 67 | NotInteractive, 68 | /// An attempt to authenticate with Apple Watch failed. 69 | /// 70 | /// This error can occur on: 71 | /// - [Apple] 72 | /// 73 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorwatchnotavailable 74 | WatchNotAvailable, 75 | /// This error can occur on: 76 | /// - [Apple] 77 | /// 78 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorinvaliddimensions 79 | InvalidDimensions, 80 | /// A passcode isn’t set on the device. 81 | /// 82 | /// This error can occur on: 83 | /// - [Apple] 84 | /// 85 | /// [Apple]: https://developer.apple.com/documentation/localauthentication/laerror/laerrorpasscodenotset 86 | PasscodeNotSet, 87 | 88 | // Android-specific errors 89 | UpdateRequired, 90 | Timeout, 91 | 92 | // Windows-specific errors 93 | /// The biometric verifier device is performing an operation and is 94 | /// unavailable. 95 | /// 96 | /// This error can occur on: 97 | /// - [Windows] 98 | /// 99 | /// [Windows]: https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverificationresult 100 | Busy, 101 | /// Group policy has disabled the biometric verifier device. 102 | /// 103 | /// This error can occur on: 104 | /// - [Windows] 105 | /// 106 | /// [Windows]: https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverificationresult 107 | DisabledByPolicy, 108 | /// A biometric verifier device is not configured for this user. 109 | /// 110 | /// This error can occur on: 111 | /// - [Windows] 112 | /// 113 | /// [Windows]: https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.ui.userconsentverificationresult 114 | NotConfigured, 115 | 116 | /// An unknown error occurred. 117 | Unknown, 118 | } 119 | 120 | #[cfg(target_os = "android")] 121 | impl From for Error { 122 | fn from(value: jni::errors::Error) -> Self { 123 | Self::Java(value) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Abstractions for multi-platform native authentication. 2 | //! 3 | //! This crate supports: 4 | //! - Apple: TouchID, FaceID, and regular username/password on macOS and iOS. 5 | //! - Android: See below for additional steps. 6 | //! - Requires the `USE_BIOMETRIC` permission in your app's manifest. 7 | //! - Windows: Windows Hello (face recognition, fingerprint, PIN), plus 8 | //! winrt-based fallback for username/password. 9 | //! - Linux: [`polkit`]-based authentication using the desktop environment's 10 | //! prompt. 11 | //! - **Note: Linux support is currently incomplete.** 12 | //! 13 | //! # Example 14 | //! 15 | //! ```no_run 16 | //! use robius_authentication::{ 17 | //! AndroidText, BiometricStrength, Context, Policy, PolicyBuilder, Text, WindowsText, 18 | //! }; 19 | //! 20 | //! let policy: Policy = PolicyBuilder::new() 21 | //! .biometrics(Some(BiometricStrength::Strong)) 22 | //! .password(true) 23 | //! .watch(true) 24 | //! .build() 25 | //! .unwrap(); 26 | //! 27 | //! let text = Text { 28 | //! android: AndroidText { 29 | //! title: "Title", 30 | //! subtitle: None, 31 | //! description: None, 32 | //! }, 33 | //! apple: "authenticate", 34 | //! windows: WindowsText::new_truncated("Title", "Description"), 35 | //! }; 36 | //! 37 | //! Context::new(()) 38 | //! .blocking_authenticate(text, &policy) 39 | //! .expect("authentication failed"); 40 | //! ``` 41 | //! 42 | //! The `Policy` and `Text` structs can also be constructed at compile-time to 43 | //! avoid run-time unwraps: 44 | //! ``` 45 | //! #![feature(const_option)] 46 | //! 47 | //! use robius_authentication::{ 48 | //! AndroidText, BiometricStrength, Policy, PolicyBuilder, Text, WindowsText, 49 | //! }; 50 | //! 51 | //! const POLICY: Policy = PolicyBuilder::new() 52 | //! .biometrics(Some(BiometricStrength::Strong)) 53 | //! .password(true) 54 | //! .watch(true) 55 | //! .build() 56 | //! .unwrap(); 57 | //! 58 | //! const TEXT: Text = Text { 59 | //! android: AndroidText { 60 | //! title: "Title", 61 | //! subtitle: None, 62 | //! description: None, 63 | //! }, 64 | //! apple: "authenticate", 65 | //! windows: WindowsText::new("Title", "Description").unwrap(), 66 | //! }; 67 | //! ``` 68 | //! 69 | //! For more details about the prompt text see the [`Text`] struct 70 | //! which allows you to customize the prompt for each platform. 71 | //! 72 | //! ## Usage on Android 73 | //! 74 | //! For authentication to work, the following must be added to your app's 75 | //! `AndroidManifest.xml`: 76 | //! ```xml 77 | //! 78 | //! ``` 79 | //! 80 | //! [`polkit`]: https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html 81 | 82 | mod error; 83 | mod sys; 84 | mod text; 85 | 86 | pub use crate::{ 87 | error::{Error, Result}, 88 | text::{AndroidText, Text, WindowsText}, 89 | }; 90 | 91 | /// A "raw" context that can be used to create a [`Context`]. 92 | /// 93 | /// Currently, all platforms define this as the void type `()`. 94 | pub type RawContext = sys::RawContext; 95 | 96 | /// Holds platform-specific contextual state required to display an authentication prompt. 97 | #[derive(Debug)] 98 | pub struct Context { 99 | inner: sys::Context, 100 | } 101 | 102 | impl Context { 103 | /// Creates a new context from the given "raw" context. 104 | #[inline] 105 | pub fn new(raw: RawContext) -> Self { 106 | Self { 107 | inner: sys::Context::new(raw), 108 | } 109 | } 110 | 111 | /// Authenticates using the provided policy and message. 112 | /// 113 | /// Returns whether the authentication was successful. 114 | #[inline] 115 | #[cfg(feature = "async")] 116 | pub async fn authenticate( 117 | &self, 118 | message: Text<'_, '_, '_, '_, '_, '_>, 119 | policy: &Policy, 120 | ) -> Result<()> { 121 | self.inner.authenticate(message, &policy.inner).await 122 | } 123 | 124 | /// Authenticates using the provided policy and message. 125 | /// 126 | /// Returns whether the authentication was successful. 127 | #[inline] 128 | pub fn blocking_authenticate(&self, message: Text, policy: &Policy) -> Result<()> { 129 | self.inner.blocking_authenticate(message, &policy.inner) 130 | } 131 | } 132 | 133 | /// A biometric strength class. 134 | /// 135 | /// This only has an effect on Android. On other targets, any biometric strength 136 | /// setting will enable all biometric authentication devices. See the [Android 137 | /// documentation][android-docs] for more details. 138 | /// 139 | /// [android-docs]: https://source.android.com/docs/security/features/biometric 140 | #[derive(Debug)] 141 | pub enum BiometricStrength { 142 | Strong, 143 | Weak, 144 | } 145 | 146 | /// A builder for conveniently defining a policy. 147 | #[derive(Debug)] 148 | pub struct PolicyBuilder { 149 | inner: sys::PolicyBuilder, 150 | } 151 | 152 | impl Default for PolicyBuilder { 153 | #[inline] 154 | fn default() -> Self { 155 | Self::new() 156 | } 157 | } 158 | 159 | impl PolicyBuilder { 160 | /// Returns a new policy with sane defaults. 161 | #[inline] 162 | pub const fn new() -> Self { 163 | Self { 164 | inner: sys::PolicyBuilder::new(), 165 | } 166 | } 167 | 168 | /// Configures biometric authentication with the given strength. 169 | /// 170 | /// The strength only has an effect on Android, see [`BiometricStrength`] 171 | /// for more details. 172 | #[inline] 173 | #[must_use] 174 | pub const fn biometrics(self, strength: Option) -> Self { 175 | Self { 176 | inner: self.inner.biometrics(strength), 177 | } 178 | } 179 | 180 | /// Sets whether the policy supports passwords. 181 | #[inline] 182 | #[must_use] 183 | pub const fn password(self, password: bool) -> Self { 184 | Self { 185 | inner: self.inner.password(password), 186 | } 187 | } 188 | 189 | /// Sets whether the policy supports watch proximity authentication. 190 | /// 191 | /// This only has an effect on iOS and macOS. 192 | #[inline] 193 | #[must_use] 194 | pub const fn watch(self, watch: bool) -> Self { 195 | Self { 196 | inner: self.inner.watch(watch), 197 | } 198 | } 199 | 200 | /// Sets whether the policy requires the watch to be on the user's wrist. 201 | /// 202 | /// This only has an effect on watchOS. 203 | #[inline] 204 | #[must_use] 205 | pub const fn wrist_detection(self, wrist_detection: bool) -> Self { 206 | Self { 207 | inner: self.inner.wrist_detection(wrist_detection), 208 | } 209 | } 210 | 211 | /// Constructs the policy. 212 | /// 213 | /// Returns `None` if the specified configuration is not valid for the 214 | /// current target. 215 | #[inline] 216 | #[must_use] 217 | pub const fn build(self) -> Option { 218 | Some(Policy { 219 | // TODO: feature(const_try) 220 | inner: match self.inner.build() { 221 | Some(inner) => inner, 222 | None => return None, 223 | }, 224 | }) 225 | } 226 | } 227 | 228 | /// An authentication policy. 229 | #[derive(Debug)] 230 | pub struct Policy { 231 | inner: sys::Policy, 232 | } 233 | -------------------------------------------------------------------------------- /src/sys.rs: -------------------------------------------------------------------------------- 1 | cfg_if::cfg_if! { 2 | if #[cfg(target_os = "android")] { 3 | mod android; 4 | pub(crate) use android::*; 5 | } else if #[cfg(target_vendor = "apple")] { 6 | mod apple; 7 | pub(crate) use apple::*; 8 | } else if #[cfg(target_os = "linux")] { 9 | mod linux; 10 | pub(crate) use linux::*; 11 | } else if #[cfg(target_os = "windows")] { 12 | mod windows; 13 | pub(crate) use windows::*; 14 | } else { 15 | mod unsupported; 16 | pub(crate) use unsupported::*; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sys/android.rs: -------------------------------------------------------------------------------- 1 | mod callback; 2 | 3 | use callback::{Receiver, Sender}; 4 | use jni::{ 5 | objects::{GlobalRef, JObject, JValueGen}, 6 | JNIEnv, 7 | }; 8 | 9 | use crate::{BiometricStrength, Error, Result, Text}; 10 | 11 | pub(crate) type RawContext = (); 12 | 13 | // Actual contextual info is handled by the `robius-android-env` crate, so we 14 | // don't have to store any state here. 15 | #[derive(Debug)] 16 | pub(crate) struct Context; 17 | 18 | impl Context { 19 | pub(crate) fn new(_: RawContext) -> Self { 20 | Self 21 | } 22 | 23 | #[cfg(feature = "async")] 24 | pub(crate) async fn authenticate( 25 | &self, 26 | text: Text<'_, '_, '_, '_, '_, '_>, 27 | policy: &Policy, 28 | ) -> Result<()> { 29 | if let Ok(inner) = self.authenticate_inner(text, policy)?.await { 30 | inner 31 | } else { 32 | Err(Error::Unknown) 33 | } 34 | } 35 | 36 | pub(crate) fn blocking_authenticate(&self, text: Text, policy: &Policy) -> Result<()> { 37 | #[cfg(feature = "async")] 38 | let result = self.authenticate_inner(text, policy)?.blocking_recv(); 39 | #[cfg(not(feature = "async"))] 40 | let result = self.authenticate_inner(text, policy)?.recv(); 41 | 42 | if let Ok(inner) = result { 43 | inner 44 | } else { 45 | Err(Error::Unknown) 46 | } 47 | } 48 | 49 | fn authenticate_inner(&self, text: Text, policy: &Policy) -> Result { 50 | robius_android_env::with_activity(|env, context| { 51 | let (tx, rx) = callback::channel(); 52 | 53 | let callback_class = callback::get_callback_class(env)?; 54 | 55 | let callback_instance = 56 | construct_callback(env, callback_class, Box::into_raw(Box::new(tx)))?; 57 | let cancellation_signal = construct_cancellation_signal(env)?; 58 | let executor = get_executor(env, context)?; 59 | 60 | let biometric_prompt = construct_biometric_prompt(env, context, policy, &text)?; 61 | 62 | env.call_method( 63 | biometric_prompt, 64 | "authenticate", 65 | "(Landroid/os/CancellationSignal;Ljava/util/concurrent/Executor;Landroid/hardware/\ 66 | biometrics/BiometricPrompt$AuthenticationCallback;)V", 67 | &[ 68 | JValueGen::Object(&cancellation_signal), 69 | JValueGen::Object(&executor), 70 | JValueGen::Object(&callback_instance), 71 | ], 72 | )?; 73 | 74 | Ok(rx) 75 | }) 76 | .map_err(|e| Error::Java(e))? 77 | } 78 | } 79 | 80 | #[derive(Debug)] 81 | pub(crate) struct Policy { 82 | #[allow(dead_code)] 83 | strength: BiometricStrength, 84 | password: bool, 85 | } 86 | 87 | #[derive(Debug)] 88 | pub(crate) struct PolicyBuilder { 89 | biometrics: Option, 90 | password: bool, 91 | } 92 | 93 | impl PolicyBuilder { 94 | pub(crate) const fn new() -> Self { 95 | Self { 96 | biometrics: Some(BiometricStrength::Strong), 97 | password: true, 98 | } 99 | } 100 | 101 | pub(crate) const fn biometrics(self, biometrics: Option) -> Self { 102 | Self { biometrics, ..self } 103 | } 104 | 105 | pub(crate) const fn password(self, password: bool) -> Self { 106 | Self { password, ..self } 107 | } 108 | 109 | pub(crate) const fn watch(self, _: bool) -> Self { 110 | self 111 | } 112 | 113 | pub(crate) const fn wrist_detection(self, _: bool) -> Self { 114 | self 115 | } 116 | 117 | pub(crate) const fn build(self) -> Option { 118 | if let Some(strength) = self.biometrics { 119 | return Some(Policy { 120 | strength, 121 | password: self.password, 122 | }); 123 | } 124 | None 125 | } 126 | } 127 | 128 | fn construct_callback<'a>( 129 | env: &mut JNIEnv<'a>, 130 | class: &GlobalRef, 131 | channel_ptr: *mut Sender, 132 | ) -> Result> { 133 | env.new_object(class, "(J)V", &[JValueGen::Long(channel_ptr as i64)]) 134 | .map_err(|e| e.into()) 135 | } 136 | 137 | fn construct_cancellation_signal<'a>(env: &mut JNIEnv<'a>) -> Result> { 138 | env.new_object("android/os/CancellationSignal", "()V", &[]) 139 | .map_err(|e| e.into()) 140 | } 141 | 142 | fn get_executor<'a, 'o, O>(env: &mut JNIEnv<'a>, context: O) -> Result> 143 | where 144 | O: AsRef>, 145 | { 146 | env.call_method( 147 | context, 148 | "getMainExecutor", 149 | "()Ljava/util/concurrent/Executor;", 150 | &[], 151 | )? 152 | .l() 153 | .map_err(|e| e.into()) 154 | } 155 | 156 | fn construct_biometric_prompt<'a>( 157 | env: &mut JNIEnv<'a>, 158 | context: &JObject<'_>, 159 | policy: &Policy, 160 | text: &Text, 161 | ) -> Result> { 162 | let context = env.new_global_ref(context).unwrap(); 163 | 164 | let builder = env.new_object( 165 | "android/hardware/biometrics/BiometricPrompt$Builder", 166 | "(Landroid/content/Context;)V", 167 | &[JValueGen::Object(context.as_ref())], 168 | )?; 169 | 170 | env.call_method( 171 | &builder, 172 | "setTitle", 173 | "(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;", 174 | &[JValueGen::Object( 175 | &env.new_string(text.android.title)?.into(), 176 | )], 177 | )?; 178 | 179 | if let Some(subtitle) = text.android.subtitle { 180 | env.call_method( 181 | &builder, 182 | "setSubtitle", 183 | "(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;", 184 | &[JValueGen::Object(&env.new_string(subtitle)?.into())], 185 | )?; 186 | } 187 | if let Some(description) = text.android.description { 188 | env.call_method( 189 | &builder, 190 | "setDescription", 191 | "(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;", 192 | &[JValueGen::Object(&env.new_string(description)?.into())], 193 | )?; 194 | } 195 | const STRONG: i32 = 0xf; 196 | const WEAK: i32 = 0xff; 197 | const CREDENTIAL: i32 = 0x8000; 198 | 199 | env.call_method( 200 | &builder, 201 | "setAllowedAuthenticators", 202 | "(I)Landroid/hardware/biometrics/BiometricPrompt$Builder;", 203 | &[JValueGen::Int( 204 | match policy.strength { 205 | BiometricStrength::Strong => STRONG, 206 | BiometricStrength::Weak => WEAK, 207 | } | if policy.password { CREDENTIAL } else { 0 }, 208 | )], 209 | )?; 210 | 211 | env.call_method( 212 | builder, 213 | "build", 214 | "()Landroid/hardware/biometrics/BiometricPrompt;", 215 | &[], 216 | )? 217 | .l() 218 | .map_err(|e| e.into()) 219 | } 220 | -------------------------------------------------------------------------------- /src/sys/android/AuthenticationCallback.java: -------------------------------------------------------------------------------- 1 | /* This file is compiled by build.rs. */ 2 | 3 | package robius.authentication; 4 | 5 | import android.hardware.biometrics.BiometricPrompt; 6 | 7 | public class AuthenticationCallback extends BiometricPrompt.AuthenticationCallback { 8 | private long pointer; 9 | 10 | /* TODO: There are neater ways of doing this */ 11 | private native void rustCallback(long pointer, int errorCode, int helpCode); 12 | 13 | public AuthenticationCallback(long pointer) { 14 | this.pointer = pointer; 15 | } 16 | 17 | public void onAuthenticationError(int errorCode, CharSequence errString) { 18 | rustCallback(pointer, errorCode, 0); 19 | } 20 | 21 | /* This is called when the user presents an incorrect authenticator (e.g. fingerprint or password). However, the 22 | * prompt remains displayed and so we don't really care, until the prompt dissapears, in which case 23 | * `onAuthenticationError`, `onAuthenticationHelp`, or `onAuthenticationSucceeded` is called. */ 24 | public void onAuthenticationFailed() {} 25 | 26 | public void onAuthenticationHelp(int helpCode, CharSequence helpString) { 27 | rustCallback(pointer, 0, helpCode); 28 | } 29 | 30 | public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) { 31 | rustCallback(pointer, 0, 0); 32 | } 33 | } -------------------------------------------------------------------------------- /src/sys/android/callback.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "async"))] 2 | use std::sync::mpsc as channel_impl; 3 | use std::sync::OnceLock; 4 | 5 | use jni::{ 6 | objects::{GlobalRef, JClass, JObject, JValueGen}, 7 | sys::{jint, jlong}, 8 | JNIEnv, NativeMethod, 9 | }; 10 | #[cfg(feature = "async")] 11 | use tokio::sync::oneshot as channel_impl; 12 | 13 | use crate::{Error, Result}; 14 | 15 | const AUTHENTICATION_CALLBACK_BYTECODE: &[u8] = 16 | include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex")); 17 | 18 | type ChannelData = Result<()>; 19 | 20 | pub(super) type Receiver = channel_impl::Receiver; 21 | pub(super) type Sender = channel_impl::Sender; 22 | 23 | pub(super) fn channel() -> (Sender, Receiver) { 24 | channel_impl::channel() 25 | } 26 | 27 | // NOTE: This must be kept in sync with the signature of `rust_callback`. 28 | const RUST_CALLBACK_SIGNATURE: &str = "(JII)V"; 29 | 30 | // NOTE: The signature of this function must be kept in sync with 31 | // `RUST_CALLBACK_SIGNATURE`. 32 | unsafe extern "C" fn rust_callback<'a>( 33 | _: JNIEnv<'a>, 34 | _: JObject<'a>, 35 | channel_ptr: jlong, 36 | error_code: jint, 37 | help_code: jint, 38 | ) { 39 | let channel = unsafe { Box::from_raw(channel_ptr as *mut Sender) }; 40 | 41 | if error_code != 0 { 42 | let _ = channel.send(Err(match error_code { 43 | BIOMETRIC_ERROR_CANCELED => Error::SystemCanceled, 44 | // TODO: Differentiate between not present and unavailable? 45 | BIOMETRIC_ERROR_HW_NOT_PRESENT => Error::Unavailable, 46 | BIOMETRIC_ERROR_HW_UNAVAILABLE => Error::Unavailable, 47 | BIOMETRIC_ERROR_LOCKOUT => Error::Exhausted, 48 | // TODO: Differentiate between lockout and lockout permanent? 49 | BIOMETRIC_ERROR_LOCKOUT_PERMANENT => Error::Exhausted, 50 | BIOMETRIC_ERROR_NO_BIOMETRICS => Error::Unavailable, 51 | BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL => Error::Unavailable, 52 | BIOMETRIC_ERROR_NO_SPACE => Error::Unknown, 53 | BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED => Error::UpdateRequired, 54 | BIOMETRIC_ERROR_TIMEOUT => Error::Timeout, 55 | BIOMETRIC_ERROR_UNABLE_TO_PROCESS => Error::Unknown, 56 | BIOMETRIC_ERROR_USER_CANCELED => Error::UserCanceled, 57 | BIOMETRIC_ERROR_VENDOR => Error::Unknown, 58 | BIOMETRIC_NO_AUTHENTICATION => Error::Unavailable, 59 | _ => { 60 | log::warn!("received unknown biometric error code: {error_code:#0x}"); 61 | Error::Unknown 62 | } 63 | })); 64 | } else if help_code != 0 { 65 | // TODO 66 | let _ = channel.send(Err(Error::Unknown)); 67 | } else { 68 | let _ = channel.send(Ok(())); 69 | } 70 | } 71 | 72 | static CALLBACK_CLASS: OnceLock = OnceLock::new(); 73 | 74 | pub(super) fn get_callback_class(env: &mut JNIEnv<'_>) -> Result<&'static GlobalRef> { 75 | // TODO: This can be optimised when the `once_cell_try` feature is stabilised. 76 | 77 | if let Some(class) = CALLBACK_CLASS.get() { 78 | return Ok(class); 79 | } 80 | let callback_class = load_callback_class(env)?; 81 | register_rust_callback(env, &callback_class)?; 82 | let global = env.new_global_ref(callback_class)?; 83 | 84 | Ok(CALLBACK_CLASS.get_or_init(|| global)) 85 | } 86 | 87 | fn register_rust_callback<'a>(env: &mut JNIEnv<'a>, callback_class: &JClass<'a>) -> Result<()> { 88 | env.register_native_methods( 89 | callback_class, 90 | &[NativeMethod { 91 | name: "rustCallback".into(), 92 | sig: RUST_CALLBACK_SIGNATURE.into(), 93 | fn_ptr: rust_callback as *mut _, 94 | }], 95 | ) 96 | .map_err(|e| e.into()) 97 | } 98 | 99 | fn load_callback_class<'a>(env: &mut JNIEnv<'a>) -> Result> { 100 | const LOADER_CLASS: &str = "dalvik/system/InMemoryDexClassLoader"; 101 | 102 | let byte_buffer = unsafe { 103 | env.new_direct_byte_buffer( 104 | AUTHENTICATION_CALLBACK_BYTECODE.as_ptr() as *mut u8, 105 | AUTHENTICATION_CALLBACK_BYTECODE.len(), 106 | ) 107 | }?; 108 | 109 | let dex_class_loader = env.new_object( 110 | LOADER_CLASS, 111 | "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V", 112 | &[ 113 | JValueGen::Object(&JObject::from(byte_buffer)), 114 | JValueGen::Object(&JObject::null()), 115 | ], 116 | )?; 117 | 118 | Ok(env 119 | .call_method( 120 | &dex_class_loader, 121 | "loadClass", 122 | "(Ljava/lang/String;)Ljava/lang/Class;", 123 | &[JValueGen::Object(&JObject::from( 124 | env.new_string("robius/authentication/AuthenticationCallback") 125 | .unwrap(), 126 | ))], 127 | )? 128 | .l()? 129 | .into()) 130 | } 131 | 132 | // https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt#BIOMETRIC_ERROR_CANCELED 133 | const BIOMETRIC_ERROR_CANCELED: i32 = 5; 134 | const BIOMETRIC_ERROR_HW_NOT_PRESENT: i32 = 0xc; 135 | const BIOMETRIC_ERROR_HW_UNAVAILABLE: i32 = 1; 136 | const BIOMETRIC_ERROR_LOCKOUT: i32 = 7; 137 | const BIOMETRIC_ERROR_LOCKOUT_PERMANENT: i32 = 9; 138 | const BIOMETRIC_ERROR_NO_BIOMETRICS: i32 = 0xb; 139 | const BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL: i32 = 0xe; 140 | const BIOMETRIC_ERROR_NO_SPACE: i32 = 4; 141 | const BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED: i32 = 0xf; 142 | const BIOMETRIC_ERROR_TIMEOUT: i32 = 3; 143 | const BIOMETRIC_ERROR_UNABLE_TO_PROCESS: i32 = 2; 144 | const BIOMETRIC_ERROR_USER_CANCELED: i32 = 0xa; 145 | const BIOMETRIC_ERROR_VENDOR: i32 = 8; 146 | // NOTE: I don't think onAuthenticationError is ever actually called with this 147 | // value. 148 | const BIOMETRIC_NO_AUTHENTICATION: i32 = -1; 149 | -------------------------------------------------------------------------------- /src/sys/apple.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | #[cfg(not(feature = "async"))] 3 | use std::sync::mpsc as channel_impl; 4 | 5 | use block2::RcBlock; 6 | use objc2::rc::Retained; 7 | use objc2_foundation::{NSError, NSString}; 8 | use objc2_local_authentication::{LAContext, LAError, LAPolicy}; 9 | #[cfg(feature = "async")] 10 | use tokio::sync::oneshot as channel_impl; 11 | 12 | use crate::{BiometricStrength, Error, Result, Text}; 13 | 14 | pub(crate) type RawContext = (); 15 | 16 | #[derive(Debug)] 17 | pub(crate) struct Context { 18 | inner: Retained, 19 | } 20 | 21 | impl Context { 22 | pub(crate) fn new(_: RawContext) -> Self { 23 | Self { 24 | inner: unsafe { LAContext::new() }, 25 | } 26 | } 27 | 28 | #[cfg(feature = "async")] 29 | pub(crate) async fn authenticate( 30 | &self, 31 | text: Text<'_, '_, '_, '_, '_, '_>, 32 | policy: &Policy, 33 | ) -> Result<()> { 34 | // The callback should always execute and hence a message will always be sent. 35 | self.authenticate_inner(text, policy).await.unwrap() 36 | } 37 | 38 | pub(crate) fn blocking_authenticate(&self, text: Text, policy: &Policy) -> Result<()> { 39 | // The callback should always execute, hence a message will always be sent, and 40 | // hence it is ok to unwrap. 41 | #[cfg(feature = "async")] 42 | { 43 | self.authenticate_inner(text, policy) 44 | .blocking_recv() 45 | .expect("failed to receive message from authentication callback") 46 | } 47 | #[cfg(not(feature = "async"))] 48 | { 49 | self.authenticate_inner(text, policy) 50 | .recv() 51 | .expect("failed to receive message from authentication callback") 52 | } 53 | } 54 | 55 | fn authenticate_inner( 56 | &self, 57 | text: Text<'_, '_, '_, '_, '_, '_>, 58 | policy: &Policy, 59 | ) -> channel_impl::Receiver> { 60 | let (tx, rx) = channel_impl::channel(); 61 | let unsafe_tx = MaybeUninit::new(tx); 62 | let message = text.apple; 63 | 64 | let block = RcBlock::new(move |is_success, error: *mut NSError| { 65 | // SAFETY: The callback is only executed once. 66 | let tx = unsafe { unsafe_tx.assume_init_read() }; 67 | let _ = if bool::from(is_success) { 68 | tx.send(Ok(())) 69 | } else { 70 | let code = unsafe { &*error }.code(); 71 | #[allow(non_upper_case_globals)] 72 | let error = match LAError(code) { 73 | LAError::AppCancel => Error::AppCanceled, 74 | LAError::AuthenticationFailed => Error::Authentication, 75 | LAError::BiometryDisconnected => Error::BiometryDisconnected, 76 | LAError::BiometryLockout => Error::Exhausted, 77 | // NOTE: This is triggered when access to biometrics is denied. 78 | LAError::BiometryNotAvailable => Error::Unavailable, 79 | LAError::BiometryNotEnrolled => Error::NotEnrolled, 80 | LAError::BiometryNotPaired => Error::NotPaired, 81 | // This error shouldn't occur, because we never invalidate the context. 82 | LAError::InvalidContext => Error::Unknown, 83 | LAError::InvalidDimensions => Error::InvalidDimensions, 84 | LAError::NotInteractive => Error::NotInteractive, 85 | LAError::PasscodeNotSet => Error::PasscodeNotSet, 86 | LAError::SystemCancel => Error::SystemCanceled, 87 | LAError::UserCancel => Error::UserCanceled, 88 | // TODO 89 | LAError::UserFallback => Error::Unknown, 90 | LAError::WatchNotAvailable => Error::WatchNotAvailable, 91 | _ => Error::Unknown, 92 | }; 93 | tx.send(Err(error)) 94 | }; 95 | }) 96 | .copy(); 97 | 98 | unsafe { 99 | self.inner.evaluatePolicy_localizedReason_reply( 100 | policy.inner, 101 | &NSString::from_str(message), 102 | &block, 103 | ) 104 | }; 105 | 106 | rx 107 | } 108 | } 109 | 110 | #[derive(Debug)] 111 | pub(crate) struct Policy { 112 | inner: LAPolicy, 113 | } 114 | 115 | #[derive(Debug)] 116 | pub(crate) struct PolicyBuilder { 117 | _biometrics: bool, 118 | _password: bool, 119 | _watch: bool, 120 | _wrist_detection: bool, 121 | } 122 | 123 | impl PolicyBuilder { 124 | pub(crate) const fn new() -> Self { 125 | Self { 126 | _biometrics: true, 127 | _password: true, 128 | _watch: true, 129 | _wrist_detection: true, 130 | } 131 | } 132 | 133 | pub(crate) const fn biometrics(self, strength: Option) -> Self { 134 | Self { 135 | _biometrics: strength.is_some(), 136 | ..self 137 | } 138 | } 139 | 140 | pub(crate) const fn password(self, password: bool) -> Self { 141 | Self { 142 | _password: password, 143 | ..self 144 | } 145 | } 146 | 147 | pub(crate) const fn watch(self, watch: bool) -> Self { 148 | Self { 149 | _watch: watch, 150 | ..self 151 | } 152 | } 153 | 154 | pub(crate) const fn wrist_detection(self, wrist_detection: bool) -> Self { 155 | Self { 156 | _wrist_detection: wrist_detection, 157 | ..self 158 | } 159 | } 160 | 161 | pub(crate) const fn build(self) -> Option { 162 | // TODO: Test watchos 163 | 164 | #[cfg(target_os = "watchos")] 165 | let policy = match self { 166 | Self { 167 | _password: true, 168 | _wrist_detection: true, 169 | .. 170 | } => LAPolicy::DeviceOwnerAuthenticationWithWristDetection, 171 | Self { 172 | _password: true, 173 | _wrist_detection: false, 174 | .. 175 | } => LAPolicy::DeviceOwnerAuthentication, 176 | _ => return None, 177 | }; 178 | 179 | #[cfg(not(target_os = "watchos"))] 180 | let policy = match self { 181 | Self { 182 | _biometrics: true, 183 | _password: true, 184 | _watch: true, 185 | .. 186 | } => LAPolicy::DeviceOwnerAuthentication, 187 | Self { 188 | _biometrics: true, 189 | _password: false, 190 | _watch: true, 191 | .. 192 | } => LAPolicy::DeviceOwnerAuthenticationWithBiometricsOrWatch, 193 | Self { 194 | _biometrics: true, 195 | _password: false, 196 | _watch: false, 197 | .. 198 | } => LAPolicy::DeviceOwnerAuthenticationWithBiometrics, 199 | Self { 200 | _biometrics: false, 201 | _password: false, 202 | _watch: true, 203 | .. 204 | } => LAPolicy::DeviceOwnerAuthenticationWithWatch, 205 | _ => return None, 206 | }; 207 | Some(Policy { inner: policy }) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/sys/linux.rs: -------------------------------------------------------------------------------- 1 | //! This module is not public yet because it is a work in progress. 2 | 3 | use polkit::{Authority, CheckAuthorizationFlags, Details, UnixProcess}; 4 | 5 | use crate::{BiometricStrength, Result}; 6 | 7 | #[derive(Debug)] 8 | pub struct Policy; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct PolicyBuilder; 12 | 13 | impl PolicyBuilder { 14 | pub(crate) const fn new() -> Self { 15 | Self 16 | } 17 | 18 | pub(crate) const fn biometrics(self, _: Option) -> Self { 19 | Self 20 | } 21 | 22 | pub(crate) const fn password(self, _: bool) -> Self { 23 | Self 24 | } 25 | 26 | pub(crate) const fn watch(self, _: bool) -> Self { 27 | Self 28 | } 29 | 30 | pub(crate) const fn wrist_detection(self, _: bool) -> Self { 31 | Self 32 | } 33 | 34 | pub(crate) const fn build(self) -> Option { 35 | Some(Policy) 36 | } 37 | } 38 | 39 | pub(crate) async fn authenticate(_message: &str, _: &Policy) -> Result<()> { 40 | unimplemented!() 41 | } 42 | 43 | pub(crate) fn blocking_authenticate(_message: &str, _: &Policy) -> Result<()> { 44 | // TODO: None? 45 | let authority = Authority::sync(Option::<&gio::Cancellable>::None).unwrap(); 46 | 47 | let current_user = "klim"; 48 | 49 | let details = Details::new(); 50 | details.insert("user", Some(current_user)); 51 | // TODO: user.gecos 52 | details.insert("user.display", Some(current_user)); 53 | // TODO: program 54 | // TODO: command_line 55 | details.insert("polkit.message", Some("Testing robius authentication")); 56 | // TODO: polkit.gettext_domain 57 | 58 | // for action in authority 59 | // .enumerate_actions_sync(Option::<&gio::Cancellable>::None) 60 | // .unwrap() 61 | // { 62 | // println!("-- action --"); 63 | // println!("id: {}", action.action_id()); 64 | // println!("description: {}", action.description()); 65 | // println!( 66 | // "allow_gui: {:?}", 67 | // action.annotation("org.freedesktop.policykit.exec.allow_gui") 68 | // ); 69 | // } 70 | 71 | let subject = UnixProcess::new(std::process::id() as i32); 72 | authority 73 | .check_authorization_sync( 74 | &subject, 75 | "org.hello-world.authenticate", 76 | Some(&details), 77 | // None, 78 | CheckAuthorizationFlags::ALLOW_USER_INTERACTION, 79 | Option::<&gio::Cancellable>::None, 80 | ) 81 | .unwrap(); 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /src/sys/unsupported.rs: -------------------------------------------------------------------------------- 1 | use crate::{BiometricStrength, Error, Result, Text}; 2 | 3 | pub(crate) type RawContext = (); 4 | 5 | #[derive(Debug)] 6 | pub(crate) struct Context; 7 | 8 | impl Context { 9 | pub(crate) fn new(_: RawContext) -> Self { 10 | Self 11 | } 12 | 13 | #[cfg(feature = "async")] 14 | pub(crate) async fn authenticate( 15 | &self, 16 | _: Text<'_, '_, '_, '_, '_, '_>, 17 | _: &Policy, 18 | ) -> Result<()> { 19 | Err(Error::Unknown) 20 | } 21 | 22 | pub(crate) fn blocking_authenticate(&self, _: Text, _: &Policy) -> Result<()> { 23 | Err(Error::Unknown) 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub(crate) struct Policy; 29 | 30 | #[derive(Debug)] 31 | pub(crate) struct PolicyBuilder; 32 | 33 | impl PolicyBuilder { 34 | pub(crate) const fn new() -> Self { 35 | Self 36 | } 37 | 38 | pub(crate) const fn biometrics(self, _: Option) -> Self { 39 | Self 40 | } 41 | 42 | pub(crate) const fn password(self, _: bool) -> Self { 43 | Self 44 | } 45 | 46 | pub(crate) const fn watch(self, _: bool) -> Self { 47 | Self 48 | } 49 | 50 | pub(crate) const fn wrist_detection(self, _: bool) -> Self { 51 | Self 52 | } 53 | 54 | pub(crate) const fn build(self) -> Option { 55 | None 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/sys/windows.rs: -------------------------------------------------------------------------------- 1 | // For the `uwp` feature gates. 2 | #![allow(unexpected_cfgs)] 3 | 4 | mod fallback; 5 | 6 | use windows::{ 7 | core::HSTRING, 8 | Foundation::IAsyncOperation, 9 | Security::Credentials::UI::{ 10 | UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability, 11 | }, 12 | }; 13 | 14 | use crate::{text::WindowsText, BiometricStrength, Error, Result, Text}; 15 | 16 | pub(crate) type RawContext = (); 17 | 18 | #[derive(Debug)] 19 | pub(crate) struct Context; 20 | 21 | impl Context { 22 | pub(crate) fn new(_: RawContext) -> Self { 23 | Self 24 | } 25 | 26 | #[cfg(feature = "async")] 27 | pub(crate) async fn authenticate( 28 | &self, 29 | message: Text<'_, '_, '_, '_, '_, '_>, 30 | _: &Policy, 31 | ) -> Result<()> { 32 | // NOTE: If we don't check availability, `request_verification` will hang. 33 | let available = 34 | check_availability()?.await == Ok(UserConsentVerifierAvailability::Available); 35 | 36 | if available { 37 | convert(request_verification(message.windows)?.await?) 38 | } else { 39 | fallback::authenticate(message.windows) 40 | } 41 | } 42 | 43 | pub(crate) fn blocking_authenticate(&self, message: Text, _: &Policy) -> Result<()> { 44 | // NOTE: If we don't check availability, `request_verification` will hang. 45 | let available = 46 | check_availability()?.get() == Ok(UserConsentVerifierAvailability::Available); 47 | 48 | if available { 49 | let verification = request_verification(message.windows)?; 50 | convert(verification.get()?) 51 | } else { 52 | fallback::authenticate(message.windows) 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub(crate) struct Policy; 59 | 60 | #[derive(Debug)] 61 | pub(crate) struct PolicyBuilder { 62 | valid: bool, 63 | } 64 | 65 | impl PolicyBuilder { 66 | pub(crate) const fn new() -> Self { 67 | Self { valid: true } 68 | } 69 | 70 | pub(crate) const fn biometrics(self, biometrics: Option) -> Self { 71 | if biometrics.is_none() { 72 | Self { valid: false } 73 | } else { 74 | self 75 | } 76 | } 77 | 78 | pub(crate) const fn password(self, password: bool) -> Self { 79 | if password { 80 | self 81 | } else { 82 | Self { valid: false } 83 | } 84 | } 85 | 86 | pub(crate) const fn watch(self, _: bool) -> Self { 87 | self 88 | } 89 | 90 | pub(crate) const fn wrist_detection(self, _: bool) -> Self { 91 | self 92 | } 93 | 94 | pub(crate) const fn build(self) -> Option { 95 | if self.valid { 96 | Some(Policy) 97 | } else { 98 | None 99 | } 100 | } 101 | } 102 | 103 | fn check_availability() -> Result> { 104 | UserConsentVerifier::CheckAvailabilityAsync().map_err(|e| e.into()) 105 | } 106 | 107 | #[cfg(feature = "uwp")] 108 | fn request_verification( 109 | text: WindowsText, 110 | ) -> Result> { 111 | let caption = caption(text.description); 112 | 113 | UserConsentVerifier::RequestVerificationAsync(&HSTRING::from_wide(&caption[..])?) 114 | .map_err(|e| e.into()) 115 | } 116 | 117 | #[cfg(not(feature = "uwp"))] 118 | fn request_verification( 119 | text: WindowsText, 120 | ) -> Result> { 121 | use windows::{ 122 | core::{factory, s}, 123 | Win32::{ 124 | Foundation::HWND, 125 | System::WinRT::IUserConsentVerifierInterop, 126 | UI::{ 127 | Input::KeyboardAndMouse::{ 128 | keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, 129 | KEYEVENTF_KEYUP, VK_MENU, 130 | }, 131 | WindowsAndMessaging::{FindWindowA, GetDesktopWindow, SetForegroundWindow}, 132 | }, 133 | }, 134 | }; 135 | 136 | // Taken from Bitwarden: 137 | // https://github.com/bitwarden/clients/blob/fb7273beb894b33db8b62f853b3d056656342856/apps/desktop/desktop_native/src/biometric/windows.rs#L192 138 | fn focus_security_prompt() -> Result<()> { 139 | unsafe fn try_find_and_set_focus( 140 | class_name: windows::core::PCSTR, 141 | ) -> retry::OperationResult<(), ()> { 142 | let hwnd = unsafe { FindWindowA(class_name, None) }; 143 | if hwnd.0 != 0 { 144 | set_focus(hwnd); 145 | return retry::OperationResult::Ok(()); 146 | } 147 | retry::OperationResult::Retry(()) 148 | } 149 | 150 | let class_name = s!("Credential Dialog Xaml Host"); 151 | retry::retry_with_index(retry::delay::Fixed::from_millis(500), |current_try| { 152 | if current_try > 3 { 153 | return retry::OperationResult::Err(()); 154 | } 155 | 156 | unsafe { try_find_and_set_focus(class_name) } 157 | }) 158 | .map_err(|_| Error::Unknown) 159 | } 160 | 161 | // Taken from Bitwarden: 162 | // https://github.com/bitwarden/clients/blob/fb7273beb894b33db8b62f853b3d056656342856/apps/desktop/desktop_native/src/biometric/windows.rs#L215 163 | fn set_focus(window: HWND) { 164 | let mut pressed = false; 165 | 166 | unsafe { 167 | // Simulate holding down Alt key to bypass windows limitations 168 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate#return-value 169 | // The most significant bit indicates if the key is currently being pressed. 170 | // This means the value will be negative if the key is pressed. 171 | if GetAsyncKeyState(VK_MENU.0 as i32) >= 0 { 172 | pressed = true; 173 | keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_EXTENDEDKEY, 0); 174 | } 175 | let _ = SetForegroundWindow(window); 176 | SetFocus(window); 177 | if pressed { 178 | keybd_event( 179 | VK_MENU.0 as u8, 180 | 0, 181 | KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 182 | 0, 183 | ); 184 | } 185 | } 186 | } 187 | 188 | let window = unsafe { GetDesktopWindow() }; 189 | let caption = caption(text.description); 190 | 191 | let factory = factory::()?; 192 | 193 | let op = unsafe { 194 | IUserConsentVerifierInterop::RequestVerificationForWindowAsync( 195 | &factory, 196 | window, 197 | &HSTRING::from_wide(&caption[..])?, 198 | ) 199 | }?; 200 | 201 | focus_security_prompt()?; 202 | 203 | Ok(op) 204 | } 205 | 206 | fn caption(message: &str) -> Vec { 207 | let mut caption = Vec::with_capacity(message.len()); 208 | 209 | for c in message.encode_utf16() { 210 | caption.push(c); 211 | } 212 | caption.push(0); 213 | 214 | caption 215 | } 216 | 217 | fn convert(result: UserConsentVerificationResult) -> Result<()> { 218 | match result { 219 | UserConsentVerificationResult::Verified => Ok(()), 220 | UserConsentVerificationResult::DeviceNotPresent => Err(Error::Unavailable), 221 | UserConsentVerificationResult::NotConfiguredForUser => Err(Error::Unavailable), 222 | UserConsentVerificationResult::DisabledByPolicy => Err(Error::Unavailable), 223 | UserConsentVerificationResult::DeviceBusy => Err(Error::Busy), 224 | UserConsentVerificationResult::RetriesExhausted => Err(Error::Exhausted), 225 | UserConsentVerificationResult::Canceled => Err(Error::UserCanceled), 226 | _ => Err(Error::Unknown), 227 | } 228 | } 229 | 230 | impl From for Error { 231 | fn from(_value: windows::core::Error) -> Self { 232 | Self::Unknown 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/sys/windows/fallback.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use windows::{ 4 | core::{PCWSTR, PSTR}, 5 | Win32::{ 6 | Foundation::{ERROR_CANCELLED, HANDLE, HWND, NO_ERROR, WIN32_ERROR}, 7 | Graphics::Gdi::HBITMAP, 8 | NetworkManagement::NetManagement::UNLEN, 9 | Security::{ 10 | Authentication::Identity::{ 11 | GetUserNameExW, LsaConnectUntrusted, LsaLookupAuthenticationPackage, 12 | NameSamCompatible, LSA_STRING, MSV1_0_PACKAGE_NAME, 13 | }, 14 | Credentials::{ 15 | CredUIParseUserNameW, CredUIPromptForWindowsCredentialsW, 16 | CredUnPackAuthenticationBufferW, CREDUIWIN_FLAGS, CREDUI_INFOW, 17 | CREDUI_MAX_DOMAIN_TARGET_LENGTH, CRED_PACK_PROTECTED_CREDENTIALS, 18 | }, 19 | LogonUserW, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, 20 | }, 21 | }, 22 | }; 23 | use windows_core::PWSTR; 24 | 25 | use crate::{text::WindowsText, Error, Result}; 26 | 27 | // Add one to include null byte. 28 | const MAX_USERNAME_LENGTH: usize = UNLEN as usize + 1; 29 | const MAX_DOMAIN_LENGTH: usize = CREDUI_MAX_DOMAIN_TARGET_LENGTH as usize + 1; 30 | const MAX_PASSWORD_LENGTH: usize = 256 + 1; 31 | 32 | type Username = [u16; MAX_USERNAME_LENGTH]; 33 | type Domain = [u16; MAX_DOMAIN_LENGTH]; 34 | type Password = [u16; MAX_PASSWORD_LENGTH]; 35 | 36 | pub(super) fn authenticate(text: WindowsText) -> Result<()> { 37 | let (auth_buf, auth_buf_size) = ui_prompt(text)?; 38 | let ((username, username_size), password) = 39 | unpack_authentication_buffer(auth_buf, auth_buf_size)?; 40 | 41 | let (current_user, current_user_size) = current_user()?; 42 | if username[..username_size] != current_user[..current_user_size] { 43 | return Err(Error::Authentication); 44 | } 45 | 46 | let (account_name, domain) = parse_username(username)?; 47 | logon_user(account_name, domain, password) 48 | } 49 | 50 | fn ui_prompt(text: WindowsText) -> Result<(*mut c_void, u32)> { 51 | // _message and _caption can only be dropped after we've used ui. 52 | let (_message, _caption, ui) = ui(text); 53 | 54 | let handle = handle()?; 55 | let mut auth_package = auth_package(handle)?; 56 | 57 | let mut auth_buf = std::ptr::null_mut(); 58 | let mut auth_buf_size = 0u32; 59 | 60 | let err = unsafe { 61 | CredUIPromptForWindowsCredentialsW( 62 | Some(&ui as *const _), 63 | 0, 64 | &mut auth_package as *mut _, 65 | None, 66 | 0, 67 | &mut auth_buf as *mut _, 68 | &mut auth_buf_size as *mut _, 69 | None, 70 | CREDUIWIN_FLAGS(0x200), 71 | ) 72 | }; 73 | 74 | match WIN32_ERROR(err) { 75 | NO_ERROR => Ok((auth_buf, auth_buf_size)), 76 | ERROR_CANCELLED => Err(Error::UserCanceled), 77 | _ => Err(Error::Unknown), 78 | } 79 | } 80 | 81 | fn ui(text: WindowsText) -> (Vec, Vec, CREDUI_INFOW) { 82 | let mut message = Vec::with_capacity(text.description.len() + 1); 83 | message.extend(text.description.encode_utf16()); 84 | message.push(0); 85 | 86 | let mut caption = Vec::with_capacity(text.title.len() + 1); 87 | caption.extend(text.title.encode_utf16()); 88 | caption.push(0); 89 | 90 | let ui = CREDUI_INFOW { 91 | cbSize: core::mem::size_of::() as u32, 92 | hwndParent: HWND(0), 93 | pszMessageText: PCWSTR(message.as_ptr()), 94 | pszCaptionText: PCWSTR(caption.as_ptr()), 95 | hbmBanner: HBITMAP(0), 96 | }; 97 | 98 | (message, caption, ui) 99 | } 100 | 101 | fn handle() -> Result { 102 | let mut handle = HANDLE(0); 103 | 104 | let result = unsafe { LsaConnectUntrusted(&mut handle as *mut _) }; 105 | 106 | if result.is_ok() { 107 | Ok(handle) 108 | } else { 109 | Err(Error::Unknown) 110 | } 111 | } 112 | 113 | fn auth_package(handle: HANDLE) -> Result { 114 | let mut auth_package = 0u32; 115 | 116 | let auth_package_bytes = unsafe { MSV1_0_PACKAGE_NAME.as_bytes() }; 117 | let auth_package_len = auth_package_bytes.len(); 118 | let auth_package_max_len = auth_package_len + 1; 119 | 120 | let mut auth_package_name = vec![0; auth_package_max_len]; 121 | auth_package_name[..auth_package_len].copy_from_slice(auth_package_bytes); 122 | auth_package_name[auth_package_len] = 0; 123 | 124 | let str = LSA_STRING { 125 | Length: auth_package_len as u16, 126 | MaximumLength: auth_package_max_len as u16, 127 | Buffer: PSTR(auth_package_name.as_ptr() as *mut _), 128 | }; 129 | 130 | let is_ok = unsafe { 131 | LsaLookupAuthenticationPackage(handle, &str as *const _, &mut auth_package as *mut _) 132 | } 133 | .is_ok(); 134 | 135 | if is_ok { 136 | Ok(auth_package) 137 | } else { 138 | Err(Error::Unknown) 139 | } 140 | } 141 | 142 | fn unpack_authentication_buffer( 143 | out_auth_buffer: *mut c_void, 144 | out_cred_size: u32, 145 | ) -> Result<((Username, usize), Password)> { 146 | // Length is wrong? This username includes domain. 147 | let mut username = [0u16; MAX_USERNAME_LENGTH]; 148 | let mut username_size = username.len() as u32; 149 | 150 | let mut password = [0u16; MAX_PASSWORD_LENGTH]; 151 | let mut password_size = password.len() as u32; 152 | 153 | unsafe { 154 | CredUnPackAuthenticationBufferW( 155 | CRED_PACK_PROTECTED_CREDENTIALS, 156 | out_auth_buffer, 157 | out_cred_size, 158 | PWSTR(username.as_mut_ptr()), 159 | &mut username_size as *mut _, 160 | PWSTR(std::ptr::null_mut()), 161 | None, 162 | PWSTR(password.as_mut_ptr()), 163 | &mut password_size as *mut _, 164 | ) 165 | } 166 | .map_err(|_| Error::Unknown)?; 167 | 168 | Ok(((username, username_size as usize), password)) 169 | } 170 | 171 | fn current_user() -> Result<(Username, usize)> { 172 | let mut username = [0; MAX_USERNAME_LENGTH]; 173 | let mut username_size = username.len() as u32; 174 | 175 | let is_ok = unsafe { 176 | GetUserNameExW( 177 | NameSamCompatible, 178 | PWSTR(&mut username as *mut _), 179 | &mut username_size as *mut _, 180 | ) 181 | } 182 | .as_bool(); 183 | 184 | if is_ok { 185 | // The size returned by GetUserNameExW doesn't include the null byte for some 186 | // reason :) 187 | Ok((username, username_size as usize + 1)) 188 | } else { 189 | Err(Error::Unknown) 190 | } 191 | } 192 | 193 | fn parse_username( 194 | mut username: Username, 195 | ) -> Result<([u16; MAX_USERNAME_LENGTH], [u16; MAX_DOMAIN_LENGTH])> { 196 | let mut account_name = [0; MAX_USERNAME_LENGTH]; 197 | let mut domain = [0; MAX_DOMAIN_LENGTH]; 198 | 199 | let err = unsafe { 200 | CredUIParseUserNameW( 201 | PCWSTR(username.as_mut_ptr()), 202 | &mut account_name, 203 | &mut domain, 204 | ) 205 | }; 206 | 207 | if err == 0 { 208 | Ok((account_name, domain)) 209 | } else { 210 | Err(Error::Unknown) 211 | } 212 | } 213 | 214 | fn logon_user( 215 | mut account_name: Username, 216 | mut domain: Domain, 217 | mut password: Password, 218 | ) -> Result<()> { 219 | let mut _handle = HANDLE(0); 220 | unsafe { 221 | LogonUserW( 222 | PWSTR(account_name.as_mut_ptr()), 223 | PWSTR(domain.as_mut_ptr()), 224 | PWSTR(password.as_mut_ptr()), 225 | LOGON32_LOGON_INTERACTIVE, 226 | LOGON32_PROVIDER_DEFAULT, 227 | // If we pass in a null pointer here, Windows silently succeeds regardless of the 228 | // password provided ... thanks Windows. 229 | &mut _handle as *mut _, 230 | ) 231 | } 232 | .map_err(|_| Error::Authentication) 233 | } 234 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | /// The text contents displayed by an authentication prompt. 2 | pub struct Text<'a, 'b, 'c, 'd, 'e, 'f> { 3 | /// The text of the authentication prompt on Android. 4 | pub android: AndroidText<'a, 'b, 'c>, 5 | /// The description of the authentication prompt on Apple devices. 6 | /// 7 | /// Appears as "$(binary_name) is trying to $(description)". 8 | pub apple: &'d str, 9 | /// The description of the authentication prompt on Windows. 10 | pub windows: WindowsText<'e, 'f>, 11 | } 12 | 13 | /// The text of the authentication prompt on Android. 14 | pub struct AndroidText<'a, 'b, 'c> { 15 | pub title: &'a str, 16 | pub subtitle: Option<&'b str>, 17 | pub description: Option<&'c str>, 18 | } 19 | 20 | /// The text of the authentication prompt on Windows, 21 | /// including a title ("caption") and description ("message"). 22 | pub struct WindowsText<'a, 'b> { 23 | #[allow(dead_code)] 24 | pub(crate) title: &'a str, 25 | #[allow(dead_code)] 26 | pub(crate) description: &'b str, 27 | } 28 | 29 | impl<'a, 'b> WindowsText<'a, 'b> { 30 | /// Creates a new `WindowsText` instance. 31 | /// 32 | /// Returns `None` if `title` exceeds 128 bytes in length 33 | /// or if `description` exceeds 1024 bytes in length. 34 | #[cfg(target_os = "windows")] 35 | pub const fn new(title: &'a str, description: &'b str) -> Option { 36 | use windows::Win32::Security::Credentials::{ 37 | CREDUI_MAX_CAPTION_LENGTH, CREDUI_MAX_MESSAGE_LENGTH, 38 | }; 39 | 40 | if title.len() <= CREDUI_MAX_CAPTION_LENGTH as usize 41 | && description.len() <= CREDUI_MAX_MESSAGE_LENGTH as usize 42 | { 43 | Some(Self { title, description }) 44 | } else { 45 | None 46 | } 47 | } 48 | 49 | /// Creates a new `WindowsText` instance. 50 | /// 51 | /// Returns `None` if `title` exceeds 128 bytes in length 52 | /// or if `description` exceeds 1024 bytes in length. 53 | #[cfg(not(target_os = "windows"))] 54 | pub const fn new(title: &'a str, description: &'b str) -> Option { 55 | Some(Self { title, description }) 56 | } 57 | 58 | /// Creates a new `WindowsText` instance. 59 | /// 60 | /// The `title` ("caption") will be truncated to 128 bytes in length, 61 | /// and the `description` ("message") will be truncated to 1024 bytes in length. 62 | #[cfg(target_os = "windows")] 63 | pub fn new_truncated(title: &'a str, description: &'b str) -> Self { 64 | use windows::Win32::Security::Credentials::{ 65 | CREDUI_MAX_CAPTION_LENGTH, CREDUI_MAX_MESSAGE_LENGTH, 66 | }; 67 | 68 | let title_max_len = std::cmp::min(CREDUI_MAX_CAPTION_LENGTH as usize, title.len()); 69 | let description_max_len = std::cmp::min(CREDUI_MAX_MESSAGE_LENGTH as usize, description.len()); 70 | Self { 71 | title: &title[..title_max_len], 72 | description: &description[..description_max_len], 73 | } 74 | } 75 | 76 | /// Creates a new `WindowsText` instance. 77 | /// 78 | /// The `title` ("caption") will be truncated to 128 bytes in length, 79 | /// and the `description` ("message") will be truncated to 1024 bytes in length. 80 | #[cfg(not(target_os = "windows"))] 81 | pub fn new_truncated(title: &'a str, description: &'b str) -> Self { 82 | Self { title, description } 83 | } 84 | } 85 | --------------------------------------------------------------------------------