Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
+813
@@ -0,0 +1,813 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fluxer_webauthn"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"libfido2-sys",
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-authentication-services",
|
||||
"objc2-foundation",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libfido2-sys"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c06f689efa98c9256934c671255adb58678c382f1ef14296ecb22d5de56902f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen",
|
||||
"cfg-if",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
"regex",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "napi"
|
||||
version = "3.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ctor",
|
||||
"futures",
|
||||
"napi-build",
|
||||
"napi-sys",
|
||||
"nohash-hasher",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-build"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive"
|
||||
version = "3.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"ctor",
|
||||
"napi-derive-backend",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-derive-backend"
|
||||
version = "5.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "napi-sys"
|
||||
version = "3.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400"
|
||||
dependencies = [
|
||||
"libloading 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nohash-hasher"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-app-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-text",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-authentication-services"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6d6f7dab884a28adaec1012eb3889257a49cc145724e35f93ece2d209f8b25"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-cloud-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-data"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-graphics"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-image"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-video"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-io-surface",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-surface"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-security"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,79 @@
|
||||
[package]
|
||||
name = "fluxer_webauthn"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
napi = {version = "3.9.1", default-features = false, features = ["dyn-symbols", "napi8"]}
|
||||
napi-derive = "3.5.6"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
libfido2-sys = "0.5.1"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = {version = "0.62.2", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Networking_WindowsWebServices",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
]}
|
||||
windows-core = "0.62.2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-foundation = {version = "0.3", features = [
|
||||
"NSArray",
|
||||
"NSData",
|
||||
"NSDictionary",
|
||||
"NSError",
|
||||
"NSObject",
|
||||
"NSString",
|
||||
"NSThread",
|
||||
"NSValue",
|
||||
]}
|
||||
objc2-app-kit = {version = "0.3", features = [
|
||||
"NSApplication",
|
||||
"NSResponder",
|
||||
"NSWindow",
|
||||
]}
|
||||
objc2-authentication-services = {version = "0.3.2", features = [
|
||||
"ASAuthorization",
|
||||
"ASAuthorizationController",
|
||||
"ASAuthorizationCredential",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialAssertion",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialAssertionRequest",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialDescriptor",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialProvider",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialRegistration",
|
||||
"ASAuthorizationPlatformPublicKeyCredentialRegistrationRequest",
|
||||
"ASAuthorizationProvider",
|
||||
"ASAuthorizationPublicKeyCredentialAssertion",
|
||||
"ASAuthorizationPublicKeyCredentialAssertionRequest",
|
||||
"ASAuthorizationPublicKeyCredentialConstants",
|
||||
"ASAuthorizationPublicKeyCredentialDescriptor",
|
||||
"ASAuthorizationPublicKeyCredentialParameters",
|
||||
"ASAuthorizationPublicKeyCredentialRegistration",
|
||||
"ASAuthorizationPublicKeyCredentialRegistrationRequest",
|
||||
"ASAuthorizationRequest",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialAssertion",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialAssertionRequest",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialDescriptor",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialProvider",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialRegistration",
|
||||
"ASAuthorizationSecurityKeyPublicKeyCredentialRegistrationRequest",
|
||||
"ASCOSEConstants",
|
||||
"ASFoundation",
|
||||
"ASPublicKeyCredential",
|
||||
]}
|
||||
dispatch2 = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "2.3.2"
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
fn main() {
|
||||
napi_build::setup();
|
||||
|
||||
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
|
||||
|
||||
match target_os.as_str() {
|
||||
"macos" => {}
|
||||
"windows" => {
|
||||
println!("cargo:rustc-link-arg-cdylib=/DELAYLOAD:webauthn.dll");
|
||||
println!("cargo:rustc-link-arg-cdylib=delayimp.lib");
|
||||
}
|
||||
"linux" => {
|
||||
println!("cargo:rustc-link-arg-cdylib=-Wl,--disable-new-dtags");
|
||||
println!("cargo:rustc-link-arg-cdylib=-Wl,-rpath,$ORIGIN");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {Buffer} from 'node:buffer';
|
||||
|
||||
export type AuthenticatorAttachment = 'platform' | 'cross-platform' | string;
|
||||
export type PublicKeyCredentialType = 'public-key';
|
||||
export type UserVerificationRequirement = 'discouraged' | 'preferred' | 'required';
|
||||
export type ResidentKeyRequirement = 'discouraged' | 'preferred' | 'required';
|
||||
export type AttestationConveyancePreference = 'direct' | 'enterprise' | 'indirect' | 'none';
|
||||
|
||||
export interface PublicKeyCredentialDescriptor {
|
||||
id: Buffer;
|
||||
type: PublicKeyCredentialType;
|
||||
transports?: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
export interface PublicKeyCredentialCreationOptions {
|
||||
attestation?: AttestationConveyancePreference;
|
||||
authenticatorSelection?: {
|
||||
authenticatorAttachment?: AuthenticatorAttachment;
|
||||
requireResidentKey?: boolean;
|
||||
residentKey?: ResidentKeyRequirement;
|
||||
userVerification?: UserVerificationRequirement;
|
||||
};
|
||||
challenge: Buffer;
|
||||
excludeCredentials?: ReadonlyArray<PublicKeyCredentialDescriptor>;
|
||||
extensions?: unknown;
|
||||
pubKeyCredParams: ReadonlyArray<{
|
||||
alg: number;
|
||||
type: PublicKeyCredentialType;
|
||||
}>;
|
||||
rp: {
|
||||
id?: string;
|
||||
name: string;
|
||||
};
|
||||
timeout?: number;
|
||||
origin: string;
|
||||
user: {
|
||||
displayName: string;
|
||||
id: Buffer;
|
||||
name: string;
|
||||
};
|
||||
windowHandle?: Buffer;
|
||||
pin?: string;
|
||||
}
|
||||
|
||||
export interface PublicKeyCredentialRequestOptions {
|
||||
allowCredentials?: ReadonlyArray<PublicKeyCredentialDescriptor>;
|
||||
challenge: Buffer;
|
||||
extensions?: unknown;
|
||||
origin: string;
|
||||
rpId?: string;
|
||||
timeout?: number;
|
||||
userVerification?: UserVerificationRequirement;
|
||||
windowHandle?: Buffer;
|
||||
pin?: string;
|
||||
}
|
||||
|
||||
export interface PublicKeyCredential {
|
||||
authenticatorAttachment?: AuthenticatorAttachment;
|
||||
id: string;
|
||||
rawId: Buffer;
|
||||
response: Buffer;
|
||||
type: PublicKeyCredentialType;
|
||||
}
|
||||
|
||||
export interface WebAuthnBackendInfo {
|
||||
apiVersion: number;
|
||||
backend: 'macos-authenticationservices' | 'windows-webauthn' | 'linux-libfido2' | 'unavailable';
|
||||
ceremoniesImplemented: boolean;
|
||||
nativeLoaded: boolean;
|
||||
platformAuthenticatorAvailable: boolean;
|
||||
platformBrokerAvailable: boolean;
|
||||
reason: string;
|
||||
supported: boolean;
|
||||
target: string;
|
||||
}
|
||||
|
||||
export declare function create(options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential>;
|
||||
|
||||
export declare function get(options: PublicKeyCredentialRequestOptions): Promise<PublicKeyCredential>;
|
||||
|
||||
export declare function getBackendInfo(): WebAuthnBackendInfo;
|
||||
|
||||
export declare function isSupported(): Promise<boolean>;
|
||||
|
||||
export declare const loadError: Error | null;
|
||||
@@ -0,0 +1,322 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const {existsSync} = require('node:fs');
|
||||
const {createHash} = require('node:crypto');
|
||||
const {join, sep} = require('node:path');
|
||||
const {createNativeLoadError, loadNativeBinding} = require('./loader-diagnostics.cjs');
|
||||
const MODULE_NAME = '@fluxer/webauthn';
|
||||
const SKIP_NATIVE_PROBE_ENV = 'FLUXER_WEBAUTHN_SKIP_NATIVE_PROBE';
|
||||
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY = 0;
|
||||
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM = 1;
|
||||
const WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM = 2;
|
||||
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED = 1;
|
||||
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED = 2;
|
||||
const WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED = 3;
|
||||
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE = 1;
|
||||
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT = 2;
|
||||
const WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT = 3;
|
||||
const WEBAUTHN_ENTERPRISE_ATTESTATION_NONE = 0;
|
||||
const WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED = 1;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_USB = 0x00000001;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_NFC = 0x00000002;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_BLE = 0x00000004;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_INTERNAL = 0x00000010;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_HYBRID = 0x00000020;
|
||||
const WEBAUTHN_CTAP_TRANSPORT_SMART_CARD = 0x00000040;
|
||||
|
||||
function resolveNativeRoot() {
|
||||
const asarSegment = `${sep}app.asar${sep}`;
|
||||
if (!__dirname.includes(asarSegment)) return __dirname;
|
||||
const unpackedDir = __dirname.replace(asarSegment, `${sep}app.asar.unpacked${sep}`);
|
||||
return existsSync(unpackedDir) ? unpackedDir : __dirname;
|
||||
}
|
||||
|
||||
function nativeFileName(platform = process.platform, arch = process.arch) {
|
||||
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) return `webauthn.darwin-${arch}.node`;
|
||||
if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) return `webauthn.win32-${arch}-msvc.node`;
|
||||
if (platform === 'linux' && (arch === 'x64' || arch === 'arm64')) return `webauthn.linux-${arch}-gnu.node`;
|
||||
return null;
|
||||
}
|
||||
|
||||
let binding = null;
|
||||
let loadError = null;
|
||||
|
||||
const fileName = nativeFileName();
|
||||
|
||||
if (fileName) {
|
||||
try {
|
||||
const nativeRoot = resolveNativeRoot();
|
||||
const nativePath = join(nativeRoot, fileName);
|
||||
const loaded = loadNativeBinding({
|
||||
moduleName: MODULE_NAME,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir: __dirname,
|
||||
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
||||
});
|
||||
binding = loaded.binding;
|
||||
loadError = loaded.loadError;
|
||||
if (loadError) throw loadError;
|
||||
} catch (error) {
|
||||
loadError = createNativeLoadError({
|
||||
moduleName: MODULE_NAME,
|
||||
nativeRoot: resolveNativeRoot(),
|
||||
packageDir: __dirname,
|
||||
reason: 'native loader threw before binding load completed',
|
||||
cause: error,
|
||||
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
||||
});
|
||||
throw loadError;
|
||||
}
|
||||
} else {
|
||||
loadError = createNativeLoadError({
|
||||
moduleName: MODULE_NAME,
|
||||
nativeRoot: resolveNativeRoot(),
|
||||
packageDir: __dirname,
|
||||
reason: `no native binary mapping for ${process.platform}/${process.arch}`,
|
||||
skipNativeProbeEnv: SKIP_NATIVE_PROBE_ENV,
|
||||
});
|
||||
throw loadError;
|
||||
}
|
||||
|
||||
function base64Url(buffer) {
|
||||
return Buffer.from(buffer).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function normalizedOrigin(origin) {
|
||||
if (typeof origin !== 'string' || origin.length === 0) {
|
||||
throw new TypeError('@fluxer/webauthn requires an origin string');
|
||||
}
|
||||
return new URL(origin).origin;
|
||||
}
|
||||
|
||||
function relyingPartyId(options, origin) {
|
||||
if (typeof options.rpId === 'string' && options.rpId.length > 0) return options.rpId;
|
||||
if (options.rp && typeof options.rp.id === 'string' && options.rp.id.length > 0) return options.rp.id;
|
||||
return new URL(origin).hostname;
|
||||
}
|
||||
|
||||
function clientDataJSON(type, challenge, origin) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
type,
|
||||
challenge: base64Url(challenge),
|
||||
origin,
|
||||
crossOrigin: false,
|
||||
}),
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
|
||||
function clientDataHash(data) {
|
||||
return createHash('sha256').update(data).digest();
|
||||
}
|
||||
|
||||
function authenticatorAttachment(value) {
|
||||
if (value === 'platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM;
|
||||
if (value === 'cross-platform') return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
|
||||
return WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY;
|
||||
}
|
||||
|
||||
function userVerification(value) {
|
||||
if (value === 'required') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_REQUIRED;
|
||||
if (value === 'discouraged') return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
|
||||
return WEBAUTHN_USER_VERIFICATION_REQUIREMENT_PREFERRED;
|
||||
}
|
||||
|
||||
function attestation(value) {
|
||||
if (value === 'direct') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT;
|
||||
if (value === 'indirect') return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT;
|
||||
return WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_NONE;
|
||||
}
|
||||
|
||||
function enterpriseAttestation(value) {
|
||||
return value === 'enterprise'
|
||||
? WEBAUTHN_ENTERPRISE_ATTESTATION_VENDOR_FACILITATED
|
||||
: WEBAUTHN_ENTERPRISE_ATTESTATION_NONE;
|
||||
}
|
||||
|
||||
function transportBits(transports) {
|
||||
if (!Array.isArray(transports)) return 0;
|
||||
let bits = 0;
|
||||
for (const transport of transports) {
|
||||
if (transport === 'usb') bits |= WEBAUTHN_CTAP_TRANSPORT_USB;
|
||||
else if (transport === 'nfc') bits |= WEBAUTHN_CTAP_TRANSPORT_NFC;
|
||||
else if (transport === 'ble') bits |= WEBAUTHN_CTAP_TRANSPORT_BLE;
|
||||
else if (transport === 'internal') bits |= WEBAUTHN_CTAP_TRANSPORT_INTERNAL;
|
||||
else if (transport === 'hybrid') bits |= WEBAUTHN_CTAP_TRANSPORT_HYBRID;
|
||||
else if (transport === 'smart-card') bits |= WEBAUTHN_CTAP_TRANSPORT_SMART_CARD;
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
function credentialDescriptors(descriptors) {
|
||||
if (!Array.isArray(descriptors)) return [];
|
||||
return descriptors.map((descriptor) => ({
|
||||
id: Buffer.from(descriptor.id),
|
||||
transports: transportBits(descriptor.transports),
|
||||
}));
|
||||
}
|
||||
|
||||
function windowHandleBuffer(value) {
|
||||
if (Buffer.isBuffer(value)) return value;
|
||||
if (value instanceof Uint8Array) return Buffer.from(value);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function pinString(value) {
|
||||
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function residentKeyFlags(selection) {
|
||||
const residentKey = selection?.residentKey;
|
||||
const requireResidentKey = Boolean(selection?.requireResidentKey) || residentKey === 'required';
|
||||
return {
|
||||
requireResidentKey,
|
||||
preferResidentKey: requireResidentKey || residentKey === 'preferred',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCreateOptions(options) {
|
||||
if (!options || typeof options !== 'object') throw new TypeError('registration options must be an object');
|
||||
const origin = normalizedOrigin(options.origin);
|
||||
const selection = options.authenticatorSelection || {};
|
||||
const residentKeys = residentKeyFlags(selection);
|
||||
return {
|
||||
rpId: relyingPartyId(options, origin),
|
||||
rpName: options.rp && typeof options.rp.name === 'string' ? options.rp.name : relyingPartyId(options, origin),
|
||||
challenge: Buffer.from(options.challenge),
|
||||
userId: Buffer.from(options.user.id),
|
||||
userName: options.user.name,
|
||||
userDisplayName: options.user.displayName,
|
||||
clientDataJSON: clientDataJSON('webauthn.create', options.challenge, origin),
|
||||
pubKeyCredParams: options.pubKeyCredParams,
|
||||
excludeCredentials: credentialDescriptors(options.excludeCredentials),
|
||||
timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
|
||||
authenticatorAttachment: authenticatorAttachment(selection.authenticatorAttachment),
|
||||
userVerification: userVerification(selection.userVerification),
|
||||
attestation: attestation(options.attestation),
|
||||
enterpriseAttestation: enterpriseAttestation(options.attestation),
|
||||
windowHandle: windowHandleBuffer(options.windowHandle),
|
||||
pin: pinString(options.pin),
|
||||
...residentKeys,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGetOptions(options) {
|
||||
if (!options || typeof options !== 'object') throw new TypeError('assertion options must be an object');
|
||||
const origin = normalizedOrigin(options.origin);
|
||||
return {
|
||||
rpId: relyingPartyId(options, origin),
|
||||
challenge: Buffer.from(options.challenge),
|
||||
clientDataJSON: clientDataJSON('webauthn.get', options.challenge, origin),
|
||||
allowCredentials: credentialDescriptors(options.allowCredentials),
|
||||
timeout: Number.isFinite(options.timeout) ? Math.max(0, Math.trunc(options.timeout)) : 0,
|
||||
authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT_ANY,
|
||||
userVerification: userVerification(options.userVerification),
|
||||
windowHandle: windowHandleBuffer(options.windowHandle),
|
||||
pin: pinString(options.pin),
|
||||
};
|
||||
}
|
||||
|
||||
function unavailableError() {
|
||||
return new Error(
|
||||
`@fluxer/webauthn native backend unavailable on ${process.platform}/${process.arch}: ${
|
||||
loadError instanceof Error ? loadError.message : 'unknown load error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
function requireBinding() {
|
||||
if (!binding) throw unavailableError();
|
||||
return binding;
|
||||
}
|
||||
|
||||
async function isSupported() {
|
||||
if (!binding || typeof binding.isSupported !== 'function') return false;
|
||||
return Boolean(await binding.isSupported());
|
||||
}
|
||||
|
||||
function rawIdCredential(rawId, response, authenticatorAttachment) {
|
||||
const id = base64Url(rawId);
|
||||
return {
|
||||
id,
|
||||
rawId,
|
||||
response: Buffer.from(JSON.stringify(response), 'utf8'),
|
||||
authenticatorAttachment,
|
||||
type: 'public-key',
|
||||
};
|
||||
}
|
||||
|
||||
async function create(options) {
|
||||
const native = requireBinding();
|
||||
if (typeof native.create !== 'function') {
|
||||
throw new Error(
|
||||
`@fluxer/webauthn native backend did not export registration on ${process.platform}/${process.arch}`,
|
||||
);
|
||||
}
|
||||
const normalized = normalizeCreateOptions(options);
|
||||
normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
|
||||
const result = await native.create(normalized);
|
||||
return rawIdCredential(
|
||||
result.rawId,
|
||||
{
|
||||
clientDataJSON: base64Url(result.clientDataJSON),
|
||||
attestationObject: base64Url(result.attestationObject),
|
||||
},
|
||||
result.authenticatorAttachment,
|
||||
);
|
||||
}
|
||||
|
||||
async function get(options) {
|
||||
const native = requireBinding();
|
||||
if (typeof native.get !== 'function') {
|
||||
throw new Error(
|
||||
`@fluxer/webauthn native backend did not export authentication on ${process.platform}/${process.arch}`,
|
||||
);
|
||||
}
|
||||
const normalized = normalizeGetOptions(options);
|
||||
normalized.clientDataHash = clientDataHash(normalized.clientDataJSON);
|
||||
const result = await native.get(normalized);
|
||||
const response = {
|
||||
clientDataJSON: base64Url(result.clientDataJSON),
|
||||
authenticatorData: base64Url(result.authenticatorData),
|
||||
signature: base64Url(result.signature),
|
||||
};
|
||||
if (result.userHandle) response.userHandle = base64Url(result.userHandle);
|
||||
return rawIdCredential(result.rawId, response, result.authenticatorAttachment);
|
||||
}
|
||||
|
||||
function getBackendInfo() {
|
||||
if (!binding || typeof binding.getBackendInfo !== 'function') {
|
||||
return {
|
||||
target: `${process.platform}/${process.arch}`,
|
||||
backend: 'unavailable',
|
||||
nativeLoaded: false,
|
||||
ceremoniesImplemented: false,
|
||||
platformBrokerAvailable: false,
|
||||
platformAuthenticatorAvailable: false,
|
||||
supported: false,
|
||||
apiVersion: 0,
|
||||
reason: loadError instanceof Error ? loadError.message : 'native backend did not load',
|
||||
};
|
||||
}
|
||||
return binding.getBackendInfo();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
get,
|
||||
getBackendInfo,
|
||||
isSupported,
|
||||
loadError,
|
||||
_private: {
|
||||
base64Url,
|
||||
clientDataJSON,
|
||||
nativeFileName,
|
||||
normalizeCreateOptions,
|
||||
normalizeGetOptions,
|
||||
resolveNativeRoot,
|
||||
transportBits,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,524 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
const {existsSync, readdirSync, readFileSync, statSync} = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const {basename} = require('node:path');
|
||||
const {spawnSync} = require('node:child_process');
|
||||
const NATIVE_LOAD_ERROR_MARKER = Symbol.for('fluxer.nativeLoadError');
|
||||
const MAX_TEXT_LENGTH = 6000;
|
||||
const MAX_DIRECTORY_ENTRIES = 80;
|
||||
|
||||
function trimText(value, limit = MAX_TEXT_LENGTH) {
|
||||
const text = Buffer.isBuffer(value) ? value.toString('utf8') : String(value ?? '');
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return null;
|
||||
return trimmed.length > limit ? `${trimmed.slice(0, limit)}\n...<truncated>` : trimmed;
|
||||
}
|
||||
|
||||
function errorDiagnostic(error) {
|
||||
if (!error) return null;
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
name: error.name || 'Error',
|
||||
message: error.message,
|
||||
code: error.code || null,
|
||||
stack: trimText(error.stack || error.message),
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: typeof error,
|
||||
message: trimText(String(error)),
|
||||
code: null,
|
||||
stack: null,
|
||||
};
|
||||
}
|
||||
|
||||
function formatErrorDiagnostic(diagnostic) {
|
||||
if (!diagnostic) return null;
|
||||
const lines = [];
|
||||
if (diagnostic.code) lines.push(`code=${diagnostic.code}`);
|
||||
if (diagnostic.stack) lines.push(diagnostic.stack);
|
||||
else if (diagnostic.message) lines.push(diagnostic.message);
|
||||
return trimText(lines.join('\n'));
|
||||
}
|
||||
|
||||
function fileDiagnostic(filePath) {
|
||||
if (!filePath) return {path: null, exists: false, error: 'not resolved'};
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
return {
|
||||
path: filePath,
|
||||
exists: true,
|
||||
size: stat.size,
|
||||
mode: `0${(stat.mode & 0o777).toString(8)}`,
|
||||
mtime: stat.mtime.toISOString(),
|
||||
isFile: stat.isFile(),
|
||||
isDirectory: stat.isDirectory(),
|
||||
};
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error);
|
||||
return {path: filePath, exists: false, error: reason};
|
||||
}
|
||||
}
|
||||
|
||||
function formatFileDiagnostic(diagnostic) {
|
||||
if (!diagnostic) return 'not resolved';
|
||||
if (!diagnostic.exists) return `exists=false, statError=${diagnostic.error || '<unknown>'}`;
|
||||
return [
|
||||
`exists=true`,
|
||||
`size=${diagnostic.size}`,
|
||||
`mode=${diagnostic.mode}`,
|
||||
`mtime=${diagnostic.mtime}`,
|
||||
`isFile=${diagnostic.isFile}`,
|
||||
].join(', ');
|
||||
}
|
||||
|
||||
function directoryDiagnostic(dirPath) {
|
||||
if (!dirPath) return {path: null, ok: false, error: 'not resolved', entries: [], total: 0, omitted: 0};
|
||||
try {
|
||||
const entries = readdirSync(dirPath, {withFileTypes: true}).map((entry) => ({
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
|
||||
}));
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const visible = entries.slice(0, MAX_DIRECTORY_ENTRIES);
|
||||
return {
|
||||
path: dirPath,
|
||||
ok: true,
|
||||
entries: visible,
|
||||
total: entries.length,
|
||||
omitted: Math.max(0, entries.length - visible.length),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
path: dirPath,
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
entries: [],
|
||||
total: 0,
|
||||
omitted: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatDirectoryDiagnostic(diagnostic) {
|
||||
if (!diagnostic) return '<unavailable>';
|
||||
if (!diagnostic.ok) return `directory listing failed: ${diagnostic.error || '<unknown>'}`;
|
||||
const entries = diagnostic.entries.map((entry) => `${entry.name}${entry.type === 'directory' ? '/' : ''}`);
|
||||
const suffix = diagnostic.omitted > 0 ? [`...<${diagnostic.omitted} more entries>`] : [];
|
||||
return [...entries, ...suffix].join('\n') || '<empty>';
|
||||
}
|
||||
|
||||
function selectedEnvironmentNames(skipNativeProbeEnv) {
|
||||
const names = [
|
||||
'ELECTRON_RUN_AS_NODE',
|
||||
'FLUXER_NATIVE_MODULE_PREFLIGHT_CHILD',
|
||||
'LD_LIBRARY_PATH',
|
||||
'DYLD_LIBRARY_PATH',
|
||||
'DISPLAY',
|
||||
'WAYLAND_DISPLAY',
|
||||
'XDG_CURRENT_DESKTOP',
|
||||
'XDG_SESSION_TYPE',
|
||||
'DBUS_SESSION_BUS_ADDRESS',
|
||||
'PULSE_SERVER',
|
||||
'PIPEWIRE_REMOTE',
|
||||
'PATH',
|
||||
];
|
||||
if (skipNativeProbeEnv) names.push(skipNativeProbeEnv);
|
||||
return names;
|
||||
}
|
||||
|
||||
function environmentDiagnostics(skipNativeProbeEnv) {
|
||||
return Object.fromEntries(
|
||||
selectedEnvironmentNames(skipNativeProbeEnv).map((name) => [name, process.env[name] ?? null]),
|
||||
);
|
||||
}
|
||||
|
||||
function formatEnvironment(diagnostic) {
|
||||
return Object.entries(diagnostic)
|
||||
.map(([name, value]) => `${name}=${value ?? '<unset>'}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function runtimeDiagnostics() {
|
||||
const versions = process.versions || {};
|
||||
let reportHeader = null;
|
||||
if (process.report && typeof process.report.getReport === 'function') {
|
||||
try {
|
||||
reportHeader = process.report.getReport().header || null;
|
||||
} catch {
|
||||
reportHeader = null;
|
||||
}
|
||||
}
|
||||
const glibcRuntime = versions.glibcVersionRuntime || reportHeader?.glibcVersionRuntime || '<unknown>';
|
||||
const glibcCompiler = versions.glibcVersionCompiler || reportHeader?.glibcVersionCompiler || '<unknown>';
|
||||
return {
|
||||
node: versions.node || null,
|
||||
electron: versions.electron || null,
|
||||
modules: versions.modules || null,
|
||||
napi: versions.napi || null,
|
||||
v8: versions.v8 || null,
|
||||
uv: versions.uv || null,
|
||||
openssl: versions.openssl || null,
|
||||
glibcRuntime,
|
||||
glibcCompiler,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
osType: os.type(),
|
||||
osRelease: os.release(),
|
||||
osVersion: typeof os.version === 'function' ? os.version() : null,
|
||||
execPath: process.execPath,
|
||||
resourcesPath: process.resourcesPath || null,
|
||||
cwd: process.cwd(),
|
||||
};
|
||||
}
|
||||
|
||||
function formatRuntimeDiagnostics(diagnostic) {
|
||||
return [
|
||||
`node=${diagnostic.node || '<unknown>'}`,
|
||||
`electron=${diagnostic.electron || '<none>'}`,
|
||||
`modules=${diagnostic.modules || '<unknown>'}`,
|
||||
`napi=${diagnostic.napi || '<unknown>'}`,
|
||||
`v8=${diagnostic.v8 || '<unknown>'}`,
|
||||
`uv=${diagnostic.uv || '<unknown>'}`,
|
||||
`openssl=${diagnostic.openssl || '<unknown>'}`,
|
||||
`glibcRuntime=${diagnostic.glibcRuntime || '<unknown>'}`,
|
||||
`glibcCompiler=${diagnostic.glibcCompiler || '<unknown>'}`,
|
||||
`process=${diagnostic.platform}/${diagnostic.arch}`,
|
||||
`os=${diagnostic.osType} ${diagnostic.osRelease} ${diagnostic.osVersion || '<unknown>'}`,
|
||||
`execPath=${diagnostic.execPath}`,
|
||||
`resourcesPath=${diagnostic.resourcesPath || '<unknown>'}`,
|
||||
`cwd=${diagnostic.cwd}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const REDISTRIBUTABLE_RUNTIME_PATTERNS = [
|
||||
/^vcruntime\d+(?:_\d+)?\.dll$/i,
|
||||
/^msvcp\d+(?:_\d+)?\.dll$/i,
|
||||
/^msvcr\d+(?:_\d+)?\.dll$/i,
|
||||
/^concrt\d+\.dll$/i,
|
||||
/^vcamp\d+\.dll$/i,
|
||||
/^vcomp\d+\.dll$/i,
|
||||
];
|
||||
|
||||
function readPeImports(filePath) {
|
||||
let buffer;
|
||||
try {
|
||||
buffer = readFileSync(filePath);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (buffer.length < 0x40) return null;
|
||||
const peOffset = buffer.readUInt32LE(0x3c);
|
||||
if (peOffset <= 0 || peOffset + 24 >= buffer.length) return null;
|
||||
if (buffer.readUInt32LE(peOffset) !== 0x4550) return null;
|
||||
const coffOffset = peOffset + 4;
|
||||
const numberOfSections = buffer.readUInt16LE(coffOffset + 2);
|
||||
const sizeOfOptionalHeader = buffer.readUInt16LE(coffOffset + 16);
|
||||
const optionalHeaderOffset = coffOffset + 20;
|
||||
if (optionalHeaderOffset + sizeOfOptionalHeader > buffer.length) return null;
|
||||
const magic = buffer.readUInt16LE(optionalHeaderOffset);
|
||||
if (magic !== 0x10b && magic !== 0x20b) return null;
|
||||
const dataDirectoriesOffset = optionalHeaderOffset + (magic === 0x20b ? 112 : 96);
|
||||
const importEntryOffset = dataDirectoriesOffset + 8;
|
||||
if (importEntryOffset + 8 > buffer.length) return null;
|
||||
const importRva = buffer.readUInt32LE(importEntryOffset);
|
||||
if (importRva === 0) return [];
|
||||
const sections = [];
|
||||
const sectionTableOffset = optionalHeaderOffset + sizeOfOptionalHeader;
|
||||
for (let i = 0; i < numberOfSections; i++) {
|
||||
const base = sectionTableOffset + i * 40;
|
||||
if (base + 40 > buffer.length) return null;
|
||||
sections.push({
|
||||
virtualSize: buffer.readUInt32LE(base + 8),
|
||||
virtualAddress: buffer.readUInt32LE(base + 12),
|
||||
rawSize: buffer.readUInt32LE(base + 16),
|
||||
rawPointer: buffer.readUInt32LE(base + 20),
|
||||
});
|
||||
}
|
||||
const rvaToOffset = (rva) => {
|
||||
for (const s of sections) {
|
||||
const span = Math.max(s.virtualSize, s.rawSize);
|
||||
if (rva >= s.virtualAddress && rva < s.virtualAddress + span) {
|
||||
return rva - s.virtualAddress + s.rawPointer;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const readCString = (offset) => {
|
||||
let end = offset;
|
||||
while (end < buffer.length && buffer[end] !== 0) end++;
|
||||
return buffer.toString('ascii', offset, end);
|
||||
};
|
||||
const importTableOffset = rvaToOffset(importRva);
|
||||
if (importTableOffset < 0) return [];
|
||||
const imports = new Set();
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
const base = importTableOffset + i * 20;
|
||||
if (base + 20 > buffer.length) break;
|
||||
const lookupRva = buffer.readUInt32LE(base);
|
||||
const nameRva = buffer.readUInt32LE(base + 12);
|
||||
const iatRva = buffer.readUInt32LE(base + 16);
|
||||
if (lookupRva === 0 && nameRva === 0 && iatRva === 0) break;
|
||||
const nameOffset = rvaToOffset(nameRva);
|
||||
if (nameOffset < 0) continue;
|
||||
const name = readCString(nameOffset);
|
||||
if (name) imports.add(name);
|
||||
}
|
||||
return Array.from(imports);
|
||||
}
|
||||
|
||||
function windowsImportProbe(nativePath) {
|
||||
const imports = readPeImports(nativePath);
|
||||
if (imports === null) return null;
|
||||
const sortedImports = [...imports].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||||
const redistributable = sortedImports.filter((dll) =>
|
||||
REDISTRIBUTABLE_RUNTIME_PATTERNS.some((pattern) => pattern.test(dll)),
|
||||
);
|
||||
return {
|
||||
command: ['pe-imports', nativePath],
|
||||
status: 0,
|
||||
signal: null,
|
||||
error: null,
|
||||
stdout: sortedImports.join('\n') || null,
|
||||
stderr: null,
|
||||
missing: [],
|
||||
redistributable,
|
||||
};
|
||||
}
|
||||
|
||||
function dependencyProbe(nativePath) {
|
||||
if (!nativePath || !existsSync(nativePath)) return null;
|
||||
if (process.platform === 'win32') return windowsImportProbe(nativePath);
|
||||
const command =
|
||||
process.platform === 'linux'
|
||||
? ['ldd', nativePath]
|
||||
: process.platform === 'darwin'
|
||||
? ['otool', '-L', nativePath]
|
||||
: null;
|
||||
if (!command) return null;
|
||||
const [bin, ...args] = command;
|
||||
const result = spawnSync(bin, args, {
|
||||
encoding: 'utf8',
|
||||
timeout: 4000,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
const stdout = trimText(result.stdout);
|
||||
const stderr = trimText(result.stderr);
|
||||
const missing =
|
||||
process.platform === 'linux' && stdout
|
||||
? stdout
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.includes('not found'))
|
||||
: [];
|
||||
return {
|
||||
command,
|
||||
status: result.status,
|
||||
signal: result.signal || null,
|
||||
error: result.error ? result.error.message : null,
|
||||
stdout,
|
||||
stderr,
|
||||
missing,
|
||||
redistributable: [],
|
||||
};
|
||||
}
|
||||
|
||||
function formatDependencyProbe(diagnostic) {
|
||||
if (!diagnostic) return null;
|
||||
const status = diagnostic.error
|
||||
? `error=${diagnostic.error}`
|
||||
: diagnostic.signal
|
||||
? `signal=${diagnostic.signal}`
|
||||
: `status=${diagnostic.status}`;
|
||||
return [
|
||||
`$ ${diagnostic.command.join(' ')}`,
|
||||
status,
|
||||
diagnostic.missing?.length ? `missing:\n${diagnostic.missing.join('\n')}` : null,
|
||||
diagnostic.redistributable?.length
|
||||
? `redistributableRuntimeImports (require VC++ redist on host):\n${diagnostic.redistributable.join('\n')}`
|
||||
: null,
|
||||
diagnostic.stdout ? `stdout:\n${diagnostic.stdout}` : null,
|
||||
diagnostic.stderr ? `stderr:\n${diagnostic.stderr}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function formatExtraDiagnostic(diagnostic) {
|
||||
if (!diagnostic) return null;
|
||||
if (typeof diagnostic === 'string') return diagnostic;
|
||||
if (typeof diagnostic === 'object' && diagnostic.name && diagnostic.text) {
|
||||
return `${diagnostic.name}:\n${diagnostic.text}`;
|
||||
}
|
||||
return `extra:\n${trimText(JSON.stringify(diagnostic, null, 2))}`;
|
||||
}
|
||||
|
||||
function collectNativeDiagnostics({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason,
|
||||
cause,
|
||||
skipNativeProbeEnv,
|
||||
extraDiagnostics = [],
|
||||
}) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
moduleName,
|
||||
reason,
|
||||
target: {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
packageDir: packageDir || null,
|
||||
nativeRoot: nativeRoot || null,
|
||||
nativePath: nativePath || null,
|
||||
nativeFile: nativePath ? basename(nativePath) : null,
|
||||
nativeFileStat: fileDiagnostic(nativePath),
|
||||
runtime: runtimeDiagnostics(),
|
||||
environment: environmentDiagnostics(skipNativeProbeEnv),
|
||||
nativeRootEntries: directoryDiagnostic(nativeRoot),
|
||||
dependencyProbe: dependencyProbe(nativePath),
|
||||
extraDiagnostics: extraDiagnostics.filter(Boolean),
|
||||
cause: errorDiagnostic(cause),
|
||||
};
|
||||
}
|
||||
|
||||
function formatNativeDiagnostics(diagnostics) {
|
||||
const sections = [
|
||||
`module=${diagnostics.moduleName}`,
|
||||
`reason=${diagnostics.reason}`,
|
||||
`target=${diagnostics.target.platform}/${diagnostics.target.arch}`,
|
||||
`packageDir=${diagnostics.packageDir || '<unknown>'}`,
|
||||
`nativeRoot=${diagnostics.nativeRoot || '<unknown>'}`,
|
||||
`nativePath=${diagnostics.nativePath || '<unknown>'}`,
|
||||
`nativeFile=${diagnostics.nativeFile || '<unknown>'}`,
|
||||
`nativeFileStat=${formatFileDiagnostic(diagnostics.nativeFileStat)}`,
|
||||
`runtime:\n${formatRuntimeDiagnostics(diagnostics.runtime)}`,
|
||||
`environment:\n${formatEnvironment(diagnostics.environment)}`,
|
||||
`nativeRootEntries:\n${formatDirectoryDiagnostic(diagnostics.nativeRootEntries)}`,
|
||||
...diagnostics.extraDiagnostics.map(formatExtraDiagnostic).filter(Boolean),
|
||||
];
|
||||
const dependencyOutput = formatDependencyProbe(diagnostics.dependencyProbe);
|
||||
if (dependencyOutput) sections.push(`dependencyProbe:\n${dependencyOutput}`);
|
||||
const causeText = formatErrorDiagnostic(diagnostics.cause);
|
||||
if (causeText) sections.push(`cause:\n${causeText}`);
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
function isNativeLoadError(error) {
|
||||
return Boolean(error?.[NATIVE_LOAD_ERROR_MARKER]);
|
||||
}
|
||||
|
||||
function createNativeLoadError({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason,
|
||||
cause,
|
||||
skipNativeProbeEnv,
|
||||
extraDiagnostics = [],
|
||||
}) {
|
||||
if (isNativeLoadError(cause)) return cause;
|
||||
const diagnostics = collectNativeDiagnostics({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason,
|
||||
cause,
|
||||
skipNativeProbeEnv,
|
||||
extraDiagnostics,
|
||||
});
|
||||
const error = new Error(`${moduleName} native module failed to load.\n${formatNativeDiagnostics(diagnostics)}`);
|
||||
error.name = 'NativeModuleLoadError';
|
||||
error[NATIVE_LOAD_ERROR_MARKER] = true;
|
||||
error.nativeDiagnostics = diagnostics;
|
||||
error.toJSON = () => ({
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
nativeDiagnostics: diagnostics,
|
||||
});
|
||||
if (cause) error.cause = cause;
|
||||
return error;
|
||||
}
|
||||
|
||||
function probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, timeoutMs = 5000}) {
|
||||
if (!skipNativeProbeEnv || process.env[skipNativeProbeEnv] === '1') {
|
||||
return null;
|
||||
}
|
||||
const result = spawnSync(process.execPath, ['-e', 'require(process.argv[1])', nativePath], {
|
||||
env: {...process.env, ELECTRON_RUN_AS_NODE: '1', [skipNativeProbeEnv]: '1'},
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
if (result.status === 0) return null;
|
||||
const reason = result.error
|
||||
? result.error.message
|
||||
: result.signal
|
||||
? `safety probe terminated by signal ${result.signal}`
|
||||
: `safety probe exited with code ${result.status}`;
|
||||
return createNativeLoadError({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason,
|
||||
skipNativeProbeEnv,
|
||||
extraDiagnostics: [
|
||||
result.stdout ? {name: 'probeStdout', text: trimText(result.stdout)} : null,
|
||||
result.stderr ? {name: 'probeStderr', text: trimText(result.stderr)} : null,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function loadNativeBinding({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv, probe = true}) {
|
||||
if (!existsSync(nativePath)) {
|
||||
return {
|
||||
binding: null,
|
||||
loadError: createNativeLoadError({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason: 'native binary not found',
|
||||
skipNativeProbeEnv,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const nativeProbeError = probe
|
||||
? probeNativeBinary({moduleName, nativePath, nativeRoot, packageDir, skipNativeProbeEnv})
|
||||
: null;
|
||||
if (nativeProbeError) {
|
||||
return {binding: null, loadError: nativeProbeError};
|
||||
}
|
||||
try {
|
||||
return {binding: require(nativePath), loadError: null};
|
||||
} catch (error) {
|
||||
return {
|
||||
binding: null,
|
||||
loadError: createNativeLoadError({
|
||||
moduleName,
|
||||
nativePath,
|
||||
nativeRoot,
|
||||
packageDir,
|
||||
reason: 'require(nativePath) threw',
|
||||
cause: error,
|
||||
skipNativeProbeEnv,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
collectNativeDiagnostics,
|
||||
createNativeLoadError,
|
||||
formatNativeDiagnostics,
|
||||
isNativeLoadError,
|
||||
loadNativeBinding,
|
||||
probeNativeBinary,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@fluxer/webauthn",
|
||||
"version": "0.0.0",
|
||||
"description": "Fluxer-owned WebAuthn native backend",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
],
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"loader-diagnostics.cjs",
|
||||
"*.so*",
|
||||
"webauthn.linux-x64-gnu.node",
|
||||
"webauthn.linux-arm64-gnu.node",
|
||||
"webauthn.darwin-x64.node",
|
||||
"webauthn.darwin-arm64.node",
|
||||
"webauthn.win32-x64-msvc.node",
|
||||
"webauthn.win32-arm64-msvc.node"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "cargo run --locked --quiet --manifest-path ../../../tools/ci/Cargo.toml -- build-desktop-native-addon",
|
||||
"test": "cargo test --manifest-path Cargo.toml && node --test test/*.test.mjs"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use napi::Result;
|
||||
use napi::bindgen_prelude::{Buffer, Error, Status};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn buffer_from_bytes(bytes: &[u8]) -> Buffer {
|
||||
Buffer::from(bytes.to_vec())
|
||||
}
|
||||
|
||||
pub fn ceremony_error(prefix: &str, message: &str) -> Error {
|
||||
Error::new(Status::GenericFailure, format!("{prefix}: {message}"))
|
||||
}
|
||||
|
||||
pub const CREATE_PREFIX: &str = "WebAuthn registration failed";
|
||||
pub const GET_PREFIX: &str = "WebAuthn authentication failed";
|
||||
|
||||
pub const TRANSPORT_USB: u32 = 0x0000_0001;
|
||||
#[allow(dead_code)]
|
||||
pub const TRANSPORT_NFC: u32 = 0x0000_0002;
|
||||
#[allow(dead_code)]
|
||||
pub const TRANSPORT_BLE: u32 = 0x0000_0004;
|
||||
pub const TRANSPORT_INTERNAL: u32 = 0x0000_0010;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const ATTACHMENT_ANY: u32 = 0;
|
||||
#[allow(dead_code)]
|
||||
pub const ATTACHMENT_PLATFORM: u32 = 1;
|
||||
#[allow(dead_code)]
|
||||
pub const ATTACHMENT_CROSS_PLATFORM: u32 = 2;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const USER_VERIFICATION_REQUIRED: u32 = 1;
|
||||
#[allow(dead_code)]
|
||||
pub const USER_VERIFICATION_PREFERRED: u32 = 2;
|
||||
#[allow(dead_code)]
|
||||
pub const USER_VERIFICATION_DISCOURAGED: u32 = 3;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const ATTESTATION_NONE: u32 = 1;
|
||||
#[allow(dead_code)]
|
||||
pub const ATTESTATION_INDIRECT: u32 = 2;
|
||||
#[allow(dead_code)]
|
||||
pub const ATTESTATION_DIRECT: u32 = 3;
|
||||
|
||||
pub const ENTERPRISE_NONE: u32 = 0;
|
||||
#[allow(dead_code)]
|
||||
pub const ENTERPRISE_VENDOR_FACILITATED: u32 = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct DescriptorInput {
|
||||
pub id: Vec<u8>,
|
||||
pub transports: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CreateInput {
|
||||
pub rp_id: String,
|
||||
pub rp_name: String,
|
||||
pub challenge: Vec<u8>,
|
||||
pub user_id: Vec<u8>,
|
||||
pub user_name: String,
|
||||
pub user_display_name: String,
|
||||
pub client_data_json: Vec<u8>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub pub_key_algs: Vec<i32>,
|
||||
pub exclude_credentials: Vec<DescriptorInput>,
|
||||
pub timeout_ms: u32,
|
||||
pub authenticator_attachment: u32,
|
||||
pub user_verification: u32,
|
||||
pub attestation: u32,
|
||||
pub enterprise_attestation: u32,
|
||||
pub require_resident_key: bool,
|
||||
pub prefer_resident_key: bool,
|
||||
pub window_handle: u64,
|
||||
pub pin: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct GetInput {
|
||||
pub rp_id: String,
|
||||
pub challenge: Vec<u8>,
|
||||
pub client_data_json: Vec<u8>,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub allow_credentials: Vec<DescriptorInput>,
|
||||
pub timeout_ms: u32,
|
||||
pub authenticator_attachment: u32,
|
||||
pub user_verification: u32,
|
||||
pub window_handle: u64,
|
||||
pub pin: Option<String>,
|
||||
}
|
||||
|
||||
pub struct CreateResult {
|
||||
pub raw_id: Vec<u8>,
|
||||
pub attestation_object: Vec<u8>,
|
||||
pub client_data_json: Vec<u8>,
|
||||
pub used_transport: u32,
|
||||
}
|
||||
|
||||
pub struct GetResult {
|
||||
pub raw_id: Vec<u8>,
|
||||
pub authenticator_data: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub user_handle: Option<Vec<u8>>,
|
||||
pub client_data_json: Vec<u8>,
|
||||
pub used_transport: u32,
|
||||
}
|
||||
|
||||
pub fn attachment_from_transport(transport: u32) -> Option<&'static str> {
|
||||
if (transport & TRANSPORT_INTERNAL) != 0 {
|
||||
Some("platform")
|
||||
} else if transport != 0 {
|
||||
Some("cross-platform")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn append_cbor_len(out: &mut Vec<u8>, major: u8, len: usize) {
|
||||
if len < 24 {
|
||||
out.push((major << 5) | (len as u8));
|
||||
} else if len <= u8::MAX as usize {
|
||||
out.push((major << 5) | 24);
|
||||
out.push(len as u8);
|
||||
} else if len <= u16::MAX as usize {
|
||||
out.push((major << 5) | 25);
|
||||
out.push(((len >> 8) & 0xff) as u8);
|
||||
out.push((len & 0xff) as u8);
|
||||
} else if len <= u32::MAX as usize {
|
||||
out.push((major << 5) | 26);
|
||||
out.push(((len >> 24) & 0xff) as u8);
|
||||
out.push(((len >> 16) & 0xff) as u8);
|
||||
out.push(((len >> 8) & 0xff) as u8);
|
||||
out.push((len & 0xff) as u8);
|
||||
} else {
|
||||
out.push((major << 5) | 27);
|
||||
out.push(((len >> 56) & 0xff) as u8);
|
||||
out.push(((len >> 48) & 0xff) as u8);
|
||||
out.push(((len >> 40) & 0xff) as u8);
|
||||
out.push(((len >> 32) & 0xff) as u8);
|
||||
out.push(((len >> 24) & 0xff) as u8);
|
||||
out.push(((len >> 16) & 0xff) as u8);
|
||||
out.push(((len >> 8) & 0xff) as u8);
|
||||
out.push((len & 0xff) as u8);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn append_cbor_text(out: &mut Vec<u8>, text: &str) {
|
||||
let bytes = text.as_bytes();
|
||||
append_cbor_len(out, 3, bytes.len());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn append_cbor_bytes(out: &mut Vec<u8>, bytes: &[u8]) {
|
||||
append_cbor_len(out, 2, bytes.len());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn build_attestation_object(
|
||||
fmt: &str,
|
||||
auth_data: &[u8],
|
||||
att_stmt_cbor: &[u8],
|
||||
) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::with_capacity(auth_data.len() + att_stmt_cbor.len() + 32);
|
||||
|
||||
append_cbor_len(&mut out, 5, 3);
|
||||
append_cbor_text(&mut out, "fmt");
|
||||
append_cbor_text(&mut out, fmt);
|
||||
append_cbor_text(&mut out, "attStmt");
|
||||
if att_stmt_cbor.is_empty() {
|
||||
out.push(0xa0);
|
||||
} else {
|
||||
out.extend_from_slice(att_stmt_cbor);
|
||||
}
|
||||
append_cbor_text(&mut out, "authData");
|
||||
append_cbor_bytes(&mut out, auth_data);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attachment_from_transport_matches_legacy() {
|
||||
assert_eq!(
|
||||
Some("platform"),
|
||||
attachment_from_transport(TRANSPORT_INTERNAL)
|
||||
);
|
||||
assert_eq!(
|
||||
Some("platform"),
|
||||
attachment_from_transport(TRANSPORT_INTERNAL | TRANSPORT_USB)
|
||||
);
|
||||
assert_eq!(
|
||||
Some("cross-platform"),
|
||||
attachment_from_transport(TRANSPORT_USB)
|
||||
);
|
||||
assert_eq!(
|
||||
Some("cross-platform"),
|
||||
attachment_from_transport(TRANSPORT_NFC)
|
||||
);
|
||||
assert_eq!(
|
||||
Some("cross-platform"),
|
||||
attachment_from_transport(TRANSPORT_BLE)
|
||||
);
|
||||
assert_eq!(None, attachment_from_transport(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_len_small() {
|
||||
let mut out = Vec::new();
|
||||
append_cbor_len(&mut out, 5, 3);
|
||||
assert_eq!(out, vec![(5 << 5) | 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_text_encodes_short_string() {
|
||||
let mut out = Vec::new();
|
||||
append_cbor_text(&mut out, "fmt");
|
||||
|
||||
assert_eq!(out, vec![0x63, b'f', b'm', b't']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_bytes_one_byte_length() {
|
||||
let payload = vec![0xab; 24];
|
||||
let mut out = Vec::new();
|
||||
append_cbor_bytes(&mut out, &payload);
|
||||
assert_eq!(out[0], (2 << 5) | 24);
|
||||
assert_eq!(out[1], 24);
|
||||
assert_eq!(&out[2..], &payload[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_len_two_byte_form() {
|
||||
let mut out = Vec::new();
|
||||
append_cbor_len(&mut out, 2, 0x0123);
|
||||
assert_eq!(out, vec![(2 << 5) | 25, 0x01, 0x23]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_len_four_byte_form() {
|
||||
let mut out = Vec::new();
|
||||
append_cbor_len(&mut out, 2, 0x0001_0203);
|
||||
assert_eq!(out, vec![(2 << 5) | 26, 0x00, 0x01, 0x02, 0x03]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_attestation_object_empty_att_stmt() {
|
||||
let auth_data = [0x01u8, 0x02, 0x03];
|
||||
let bytes = build_attestation_object("none", &auth_data, &[]).unwrap();
|
||||
|
||||
let expected: Vec<u8> = vec![
|
||||
0xa3, 0x63, b'f', b'm', b't', 0x64, b'n', b'o', b'n', b'e', 0x67, b'a', b't', b't',
|
||||
b'S', b't', b'm', b't', 0xa0, 0x68, b'a', b'u', b't', b'h', b'D', b'a', b't', b'a',
|
||||
0x43, 0x01, 0x02, 0x03,
|
||||
];
|
||||
assert_eq!(bytes, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_attestation_object_passes_through_packed_att_stmt() {
|
||||
let att_stmt = [
|
||||
0xa2u8, 0x63, b'a', b'l', b'g', 0x26, 0x63, b's', b'i', b'g', 0x41, 0x00,
|
||||
];
|
||||
let auth_data = [0xaau8; 4];
|
||||
let bytes = build_attestation_object("packed", &auth_data, &att_stmt).unwrap();
|
||||
|
||||
let needle = att_stmt.as_slice();
|
||||
let position = bytes
|
||||
.windows(needle.len())
|
||||
.position(|w| w == needle)
|
||||
.expect("att_stmt must be spliced unmodified");
|
||||
|
||||
assert!(position >= 8);
|
||||
let header = &bytes[position - 8..position];
|
||||
assert_eq!(header, b"\x67attStmt");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
#![allow(clippy::too_many_lines)]
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use napi::Task;
|
||||
use napi::bindgen_prelude::{
|
||||
AsyncTask, Buffer, Env, Error, JsObjectValue, Object, Result, Status, Unknown,
|
||||
};
|
||||
use napi::{JsValue, ValueType};
|
||||
use napi_derive::napi;
|
||||
|
||||
mod common;
|
||||
use common::{
|
||||
CreateInput, CreateResult, DescriptorInput, GetInput, GetResult, attachment_from_transport,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const BACKEND_NAME: &str = "macos-authenticationservices";
|
||||
#[cfg(target_os = "windows")]
|
||||
const BACKEND_NAME: &str = "windows-webauthn";
|
||||
#[cfg(target_os = "linux")]
|
||||
const BACKEND_NAME: &str = "linux-libfido2";
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
const BACKEND_NAME: &str = "unavailable";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const BACKEND_REASON: &str = "macOS WebAuthn backend (AuthenticationServices). \
|
||||
Requires macOS 12.0+ (ASAuthorizationPlatformPublicKeyCredentialProvider, \
|
||||
introduced in Monterey). On older macOS the bundled \
|
||||
ASAuthorizationPlatform/SecurityKey provider classes are not present and \
|
||||
`isSupported()` returns false.";
|
||||
#[cfg(target_os = "windows")]
|
||||
const BACKEND_REASON: &str = "Windows WebAuthn broker backend (webauthn.dll). \
|
||||
Requires Windows 10 build 18362 (May 2019 / version 1903) or newer; \
|
||||
webauthn.dll is delay-loaded via /DELAYLOAD, so the addon still loads on \
|
||||
older builds but `isSupported()` returns false there.";
|
||||
#[cfg(target_os = "linux")]
|
||||
const BACKEND_REASON: &str = "Linux WebAuthn backend (libfido2). Requires \
|
||||
libfido2 >= 1.5.0 (HID transport, U2F fallback disabled) and a CTAP \
|
||||
authenticator on USB-HID; `isSupported()` returns false when no \
|
||||
authenticator is enumerated.";
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
const BACKEND_REASON: &str = "no WebAuthn backend is compiled in for this platform";
|
||||
|
||||
const TARGET_PLATFORM: &str = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"win32"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
const TARGET_ARCH: &str = if cfg!(target_arch = "x86_64") {
|
||||
"x64"
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"arm64"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
fn target_string() -> String {
|
||||
format!("{TARGET_PLATFORM}/{TARGET_ARCH}")
|
||||
}
|
||||
|
||||
fn platform_broker_available() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows::api_version() > 0
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux::is_supported()
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::is_supported()
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn platform_authenticator_available() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows::is_user_verifying_platform_authenticator_available()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn ceremonies_implemented() -> bool {
|
||||
cfg!(any(
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
target_os = "linux"
|
||||
))
|
||||
}
|
||||
|
||||
fn is_supported_native() -> bool {
|
||||
ceremonies_implemented() && platform_broker_available()
|
||||
}
|
||||
|
||||
fn api_version() -> u32 {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows::api_version()
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fn backend_reason() -> String {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
format!("{} {}", BACKEND_REASON, linux::support_diagnostics())
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
BACKEND_REASON.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object, js_name = "WebAuthnBackendInfo")]
|
||||
pub struct WebAuthnBackendInfoJs {
|
||||
pub target: String,
|
||||
pub backend: String,
|
||||
#[napi(js_name = "nativeLoaded")]
|
||||
pub native_loaded: bool,
|
||||
#[napi(js_name = "ceremoniesImplemented")]
|
||||
pub ceremonies_implemented: bool,
|
||||
#[napi(js_name = "platformBrokerAvailable")]
|
||||
pub platform_broker_available: bool,
|
||||
#[napi(js_name = "platformAuthenticatorAvailable")]
|
||||
pub platform_authenticator_available: bool,
|
||||
pub supported: bool,
|
||||
#[napi(js_name = "apiVersion")]
|
||||
pub api_version: u32,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[napi(js_name = "getBackendInfo")]
|
||||
pub fn get_backend_info() -> WebAuthnBackendInfoJs {
|
||||
WebAuthnBackendInfoJs {
|
||||
target: target_string(),
|
||||
backend: BACKEND_NAME.to_owned(),
|
||||
native_loaded: true,
|
||||
ceremonies_implemented: ceremonies_implemented(),
|
||||
platform_broker_available: platform_broker_available(),
|
||||
platform_authenticator_available: platform_authenticator_available(),
|
||||
supported: is_supported_native(),
|
||||
api_version: api_version(),
|
||||
reason: backend_reason(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct IsSupportedTask;
|
||||
impl Task for IsSupportedTask {
|
||||
type Output = bool;
|
||||
type JsValue = bool;
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
Ok(is_supported_native())
|
||||
}
|
||||
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "isSupported")]
|
||||
pub fn is_supported() -> AsyncTask<IsSupportedTask> {
|
||||
AsyncTask::new(IsSupportedTask)
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct CreateCeremonyResultJs {
|
||||
#[napi(js_name = "rawId")]
|
||||
pub raw_id: Buffer,
|
||||
#[napi(js_name = "attestationObject")]
|
||||
pub attestation_object: Buffer,
|
||||
#[napi(js_name = "clientDataJSON")]
|
||||
pub client_data_json: Buffer,
|
||||
|
||||
#[napi(js_name = "authenticatorAttachment")]
|
||||
pub authenticator_attachment: Option<String>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct GetCeremonyResultJs {
|
||||
#[napi(js_name = "rawId")]
|
||||
pub raw_id: Buffer,
|
||||
#[napi(js_name = "authenticatorData")]
|
||||
pub authenticator_data: Buffer,
|
||||
pub signature: Buffer,
|
||||
|
||||
#[napi(js_name = "userHandle")]
|
||||
pub user_handle: Option<Buffer>,
|
||||
#[napi(js_name = "clientDataJSON")]
|
||||
pub client_data_json: Buffer,
|
||||
#[napi(js_name = "authenticatorAttachment")]
|
||||
pub authenticator_attachment: Option<String>,
|
||||
}
|
||||
|
||||
pub struct CreateTask {
|
||||
input: CreateInput,
|
||||
}
|
||||
|
||||
impl Task for CreateTask {
|
||||
type Output = CreateResult;
|
||||
type JsValue = CreateCeremonyResultJs;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
run_create(&mut self.input)
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
||||
Ok(CreateCeremonyResultJs {
|
||||
raw_id: Buffer::from(output.raw_id),
|
||||
attestation_object: Buffer::from(output.attestation_object),
|
||||
client_data_json: Buffer::from(output.client_data_json),
|
||||
authenticator_attachment: attachment_from_transport(output.used_transport)
|
||||
.map(str::to_owned),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetTask {
|
||||
input: GetInput,
|
||||
}
|
||||
|
||||
impl Task for GetTask {
|
||||
type Output = GetResult;
|
||||
type JsValue = GetCeremonyResultJs;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
run_get(&mut self.input)
|
||||
}
|
||||
|
||||
fn resolve(&mut self, _env: Env, output: Self::Output) -> Result<Self::JsValue> {
|
||||
Ok(GetCeremonyResultJs {
|
||||
raw_id: Buffer::from(output.raw_id),
|
||||
authenticator_data: Buffer::from(output.authenticator_data),
|
||||
signature: Buffer::from(output.signature),
|
||||
user_handle: output.user_handle.map(Buffer::from),
|
||||
client_data_json: Buffer::from(output.client_data_json),
|
||||
authenticator_attachment: attachment_from_transport(output.used_transport)
|
||||
.map(str::to_owned),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn run_create(input: &mut CreateInput) -> Result<CreateResult> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows::make_credential(input)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux::make_credential(input)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::make_credential(input)
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
let _ = input;
|
||||
Err(common::ceremony_error(
|
||||
common::CREATE_PREFIX,
|
||||
"UnknownPlatform",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn run_get(input: &mut GetInput) -> Result<GetResult> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
windows::get_assertion(input)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux::get_assertion(input)
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::get_assertion(input)
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
|
||||
{
|
||||
let _ = input;
|
||||
Err(common::ceremony_error(
|
||||
common::GET_PREFIX,
|
||||
"UnknownPlatform",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "create")]
|
||||
pub fn create(options: Object) -> Result<AsyncTask<CreateTask>> {
|
||||
let input = parse_create_input(&options)?;
|
||||
Ok(AsyncTask::new(CreateTask { input }))
|
||||
}
|
||||
|
||||
#[napi(js_name = "get")]
|
||||
pub fn get(options: Object) -> Result<AsyncTask<GetTask>> {
|
||||
let input = parse_get_input(&options)?;
|
||||
Ok(AsyncTask::new(GetTask { input }))
|
||||
}
|
||||
|
||||
fn invalid_create_options() -> Error {
|
||||
Error::new(
|
||||
Status::InvalidArg,
|
||||
"invalid WebAuthn registration options".to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
fn invalid_get_options() -> Error {
|
||||
Error::new(
|
||||
Status::InvalidArg,
|
||||
"invalid WebAuthn assertion options".to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_create_input(object: &Object) -> Result<CreateInput> {
|
||||
let map_err = |_e: Error| invalid_create_options();
|
||||
Ok(CreateInput {
|
||||
rp_id: required_string(object, "rpId").map_err(map_err)?,
|
||||
rp_name: required_string(object, "rpName").map_err(map_err)?,
|
||||
challenge: required_buffer(object, "challenge").map_err(map_err)?,
|
||||
user_id: required_buffer(object, "userId").map_err(map_err)?,
|
||||
user_name: required_string(object, "userName").map_err(map_err)?,
|
||||
user_display_name: required_string(object, "userDisplayName").map_err(map_err)?,
|
||||
client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?,
|
||||
client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?,
|
||||
pub_key_algs: required_alg_array(object).map_err(map_err)?,
|
||||
exclude_credentials: optional_descriptor_array(object, "excludeCredentials")
|
||||
.map_err(map_err)?,
|
||||
timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?,
|
||||
authenticator_attachment: optional_u32(
|
||||
object,
|
||||
"authenticatorAttachment",
|
||||
default_attachment(),
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
user_verification: optional_u32(object, "userVerification", default_user_verification())
|
||||
.map_err(map_err)?,
|
||||
attestation: optional_u32(object, "attestation", default_attestation()).map_err(map_err)?,
|
||||
enterprise_attestation: optional_u32(
|
||||
object,
|
||||
"enterpriseAttestation",
|
||||
common::ENTERPRISE_NONE,
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
require_resident_key: optional_bool(object, "requireResidentKey", false)
|
||||
.map_err(map_err)?,
|
||||
prefer_resident_key: optional_bool(object, "preferResidentKey", false).map_err(map_err)?,
|
||||
window_handle: optional_window_handle(object).map_err(map_err)?,
|
||||
pin: optional_string(object, "pin").map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_get_input(object: &Object) -> Result<GetInput> {
|
||||
let map_err = |_e: Error| invalid_get_options();
|
||||
Ok(GetInput {
|
||||
rp_id: required_string(object, "rpId").map_err(map_err)?,
|
||||
challenge: required_buffer(object, "challenge").map_err(map_err)?,
|
||||
client_data_json: required_buffer(object, "clientDataJSON").map_err(map_err)?,
|
||||
client_data_hash: required_buffer(object, "clientDataHash").map_err(map_err)?,
|
||||
allow_credentials: optional_descriptor_array(object, "allowCredentials")
|
||||
.map_err(map_err)?,
|
||||
timeout_ms: optional_u32(object, "timeout", 0).map_err(map_err)?,
|
||||
authenticator_attachment: optional_u32(
|
||||
object,
|
||||
"authenticatorAttachment",
|
||||
default_attachment(),
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
user_verification: optional_u32(object, "userVerification", default_user_verification())
|
||||
.map_err(map_err)?,
|
||||
window_handle: optional_window_handle(object).map_err(map_err)?,
|
||||
pin: optional_string(object, "pin").map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn optional_string(object: &Object, key: &str) -> Result<Option<String>> {
|
||||
let Some(value) = read_unknown(object, key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(None);
|
||||
}
|
||||
if value.get_type()? != ValueType::String {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a string"),
|
||||
));
|
||||
}
|
||||
let s: String = unsafe { value.cast::<String>()? };
|
||||
if s.is_empty() { Ok(None) } else { Ok(Some(s)) }
|
||||
}
|
||||
|
||||
fn optional_window_handle(object: &Object) -> Result<u64> {
|
||||
let Some(value) = read_unknown(object, "windowHandle") else {
|
||||
return Ok(0);
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(0);
|
||||
}
|
||||
let bytes = read_buffer_bytes(&value, "windowHandle")?;
|
||||
if bytes.len() > 8 {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"windowHandle must be at most 8 bytes".to_owned(),
|
||||
));
|
||||
}
|
||||
let mut padded = [0u8; 8];
|
||||
padded[..bytes.len()].copy_from_slice(&bytes);
|
||||
Ok(u64::from_le_bytes(padded))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default_attachment() -> u32 {
|
||||
common::ATTACHMENT_ANY
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default_user_verification() -> u32 {
|
||||
common::USER_VERIFICATION_PREFERRED
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
fn default_attestation() -> u32 {
|
||||
common::ATTESTATION_NONE
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn default_attachment() -> u32 {
|
||||
0
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn default_user_verification() -> u32 {
|
||||
0
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn default_attestation() -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
fn read_unknown<'a>(object: &Object<'a>, key: &str) -> Option<Unknown<'a>> {
|
||||
object.get::<Unknown>(key).ok().flatten()
|
||||
}
|
||||
|
||||
fn is_nullish(v: &Unknown<'_>) -> bool {
|
||||
match v.get_type() {
|
||||
Ok(t) => t == ValueType::Null || t == ValueType::Undefined,
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_string(object: &Object, key: &str) -> Result<String> {
|
||||
let value = read_unknown(object, key)
|
||||
.ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?;
|
||||
if is_nullish(&value) {
|
||||
return Err(Error::new(Status::InvalidArg, format!("missing {key}")));
|
||||
}
|
||||
if value.get_type()? != ValueType::String {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a string"),
|
||||
));
|
||||
}
|
||||
let s: String = unsafe { value.cast::<String>()? };
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
fn optional_u32(object: &Object, key: &str, default: u32) -> Result<u32> {
|
||||
let Some(value) = read_unknown(object, key) else {
|
||||
return Ok(default);
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(default);
|
||||
}
|
||||
if value.get_type()? != ValueType::Number {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a number"),
|
||||
));
|
||||
}
|
||||
let n: u32 = unsafe { value.cast::<u32>()? };
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn optional_bool(object: &Object, key: &str, default: bool) -> Result<bool> {
|
||||
let Some(value) = read_unknown(object, key) else {
|
||||
return Ok(default);
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(default);
|
||||
}
|
||||
if value.get_type()? != ValueType::Boolean {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a boolean"),
|
||||
));
|
||||
}
|
||||
let b: bool = unsafe { value.cast::<bool>()? };
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
fn required_buffer(object: &Object, key: &str) -> Result<Vec<u8>> {
|
||||
let value = read_unknown(object, key)
|
||||
.ok_or_else(|| Error::new(Status::InvalidArg, format!("missing {key}")))?;
|
||||
if is_nullish(&value) {
|
||||
return Err(Error::new(Status::InvalidArg, format!("missing {key}")));
|
||||
}
|
||||
read_buffer_bytes(&value, key)
|
||||
}
|
||||
|
||||
fn read_buffer_bytes(value: &Unknown<'_>, key: &str) -> Result<Vec<u8>> {
|
||||
let raw = value.raw();
|
||||
let raw_env = value.value().env;
|
||||
let mut is_buffer = false;
|
||||
let status = unsafe { napi::sys::napi_is_buffer(raw_env, raw, &mut is_buffer) };
|
||||
if status != napi::sys::Status::napi_ok || !is_buffer {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a Buffer"),
|
||||
));
|
||||
}
|
||||
let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
|
||||
let mut len: usize = 0;
|
||||
let status = unsafe { napi::sys::napi_get_buffer_info(raw_env, raw, &mut data_ptr, &mut len) };
|
||||
if status != napi::sys::Status::napi_ok {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} buffer read failed"),
|
||||
));
|
||||
}
|
||||
if len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if data_ptr.is_null() {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} buffer pointer was null"),
|
||||
));
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(data_ptr.cast::<u8>(), len) };
|
||||
Ok(slice.to_vec())
|
||||
}
|
||||
|
||||
fn required_alg_array(object: &Object) -> Result<Vec<i32>> {
|
||||
let Some(value) = read_unknown(object, "pubKeyCredParams") else {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"missing pubKeyCredParams".to_owned(),
|
||||
));
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"missing pubKeyCredParams".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let raw = value.raw();
|
||||
let raw_env = value.value().env;
|
||||
let mut is_array = false;
|
||||
let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) };
|
||||
if status != napi::sys::Status::napi_ok || !is_array {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"pubKeyCredParams must be an array".to_owned(),
|
||||
));
|
||||
}
|
||||
let arr: Object = unsafe { value.cast::<Object>()? };
|
||||
let len = read_array_length(&arr)?;
|
||||
if len == 0 {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"pubKeyCredParams must not be empty".to_owned(),
|
||||
));
|
||||
}
|
||||
let mut algs = Vec::with_capacity(len as usize);
|
||||
for i in 0..len {
|
||||
let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut();
|
||||
let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) };
|
||||
if status != napi::sys::Status::napi_ok {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"pubKeyCredParams element read failed".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) };
|
||||
if elem_value.get_type()? != ValueType::Object {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
"pubKeyCredParams[] must be objects".to_owned(),
|
||||
));
|
||||
}
|
||||
let elem: Object = unsafe { elem_value.cast::<Object>()? };
|
||||
algs.push(optional_i32_field(&elem, "alg", 0)?);
|
||||
}
|
||||
Ok(algs)
|
||||
}
|
||||
|
||||
fn optional_i32_field(object: &Object, key: &str, default: i32) -> Result<i32> {
|
||||
let Some(value) = read_unknown(object, key) else {
|
||||
return Ok(default);
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(default);
|
||||
}
|
||||
if value.get_type()? != ValueType::Number {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be a number"),
|
||||
));
|
||||
}
|
||||
let n: i32 = unsafe { value.cast::<i32>()? };
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn optional_descriptor_array(object: &Object, key: &str) -> Result<Vec<DescriptorInput>> {
|
||||
let Some(value) = read_unknown(object, key) else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
if is_nullish(&value) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let raw = value.raw();
|
||||
let raw_env = value.value().env;
|
||||
let mut is_array = false;
|
||||
let status = unsafe { napi::sys::napi_is_array(raw_env, raw, &mut is_array) };
|
||||
if status != napi::sys::Status::napi_ok || !is_array {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key} must be an array"),
|
||||
));
|
||||
}
|
||||
let arr: Object = unsafe { value.cast::<Object>()? };
|
||||
let len = read_array_length(&arr)?;
|
||||
let mut out = Vec::with_capacity(len as usize);
|
||||
for i in 0..len {
|
||||
let mut elem_raw: napi::sys::napi_value = std::ptr::null_mut();
|
||||
let status = unsafe { napi::sys::napi_get_element(raw_env, raw, i, &mut elem_raw) };
|
||||
if status != napi::sys::Status::napi_ok {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key}[] read failed"),
|
||||
));
|
||||
}
|
||||
let elem_value: Unknown = unsafe { Unknown::from_raw_unchecked(raw_env, elem_raw) };
|
||||
if elem_value.get_type()? != ValueType::Object {
|
||||
return Err(Error::new(
|
||||
Status::InvalidArg,
|
||||
format!("{key}[] must be objects"),
|
||||
));
|
||||
}
|
||||
let elem: Object = unsafe { elem_value.cast::<Object>()? };
|
||||
let id = required_buffer(&elem, "id")?;
|
||||
let transports = optional_u32(&elem, "transports", 0)?;
|
||||
out.push(DescriptorInput { id, transports });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn read_array_length(obj: &Object) -> Result<u32> {
|
||||
let length: u32 = obj.get_named_property::<u32>("length")?;
|
||||
Ok(length)
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
#![allow(clippy::manual_c_str_literals)]
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::ffi::{CStr, CString, c_char, c_int};
|
||||
use std::ptr;
|
||||
|
||||
use libfido2_sys as f;
|
||||
use napi::Result;
|
||||
|
||||
use crate::common::{
|
||||
CREATE_PREFIX, CreateInput, CreateResult, GET_PREFIX, GetInput, GetResult, TRANSPORT_USB,
|
||||
USER_VERIFICATION_REQUIRED, build_attestation_object, ceremony_error,
|
||||
};
|
||||
|
||||
const MANIFEST_COUNT: usize = 64;
|
||||
const SUPPORT_DIAGNOSTIC_DEVICE_LIMIT: usize = 8;
|
||||
|
||||
fn init() {
|
||||
unsafe { f::fido_init(f::FIDO_DISABLE_U2F_FALLBACK as c_int) };
|
||||
}
|
||||
|
||||
pub fn is_supported() -> bool {
|
||||
init();
|
||||
|
||||
unsafe {
|
||||
let infos = f::fido_dev_info_new(MANIFEST_COUNT);
|
||||
if infos.is_null() {
|
||||
return false;
|
||||
}
|
||||
let mut found: usize = 0;
|
||||
let rc = f::fido_dev_info_manifest(infos, MANIFEST_COUNT, &mut found);
|
||||
let mut local = infos;
|
||||
f::fido_dev_info_free(&mut local, MANIFEST_COUNT);
|
||||
rc == f::FIDO_OK as c_int && found > 0
|
||||
}
|
||||
}
|
||||
|
||||
fn c_string(ptr: *const c_char) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
let value = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
}
|
||||
|
||||
fn fido_error_message(code: c_int) -> String {
|
||||
c_string(unsafe { f::fido_strerr(code) }).unwrap_or_else(|| "unknown libfido2 error".to_owned())
|
||||
}
|
||||
|
||||
fn fido_call_detail(operation: &str, code: c_int) -> String {
|
||||
format!("{operation} rc={code} error={}", fido_error_message(code))
|
||||
}
|
||||
|
||||
fn describe_device_info(info: *const f::fido_dev_info_t) -> String {
|
||||
if info.is_null() {
|
||||
return "deviceInfo=null".to_owned();
|
||||
}
|
||||
let path =
|
||||
c_string(unsafe { f::fido_dev_info_path(info) }).unwrap_or_else(|| "<unknown>".to_owned());
|
||||
let manufacturer = c_string(unsafe { f::fido_dev_info_manufacturer_string(info) })
|
||||
.unwrap_or_else(|| "<unknown>".to_owned());
|
||||
let product_name = c_string(unsafe { f::fido_dev_info_product_string(info) })
|
||||
.unwrap_or_else(|| "<unknown>".to_owned());
|
||||
let vendor_id = unsafe { f::fido_dev_info_vendor(info) } as u16;
|
||||
let product_id = unsafe { f::fido_dev_info_product(info) } as u16;
|
||||
format!(
|
||||
"path={path} vendorId=0x{vendor_id:04x} productId=0x{product_id:04x} manufacturer={manufacturer:?} product={product_name:?}"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn support_diagnostics() -> String {
|
||||
init();
|
||||
unsafe {
|
||||
let infos = f::fido_dev_info_new(MANIFEST_COUNT);
|
||||
if infos.is_null() {
|
||||
return "Probe: fido_dev_info_new returned null.".to_owned();
|
||||
}
|
||||
let mut infos_owned = infos;
|
||||
let mut found: usize = 0;
|
||||
let rc = f::fido_dev_info_manifest(infos, MANIFEST_COUNT, &mut found);
|
||||
let detail = if rc != f::FIDO_OK as c_int {
|
||||
format!("Probe: {}.", fido_call_detail("fido_dev_info_manifest", rc))
|
||||
} else if found == 0 {
|
||||
"Probe: fido_dev_info_manifest found 0 devices. Check that a USB-HID CTAP security key is connected and that udev/hidraw permissions allow this user to read it.".to_owned()
|
||||
} else {
|
||||
let mut devices = Vec::new();
|
||||
for index in 0..found.min(SUPPORT_DIAGNOSTIC_DEVICE_LIMIT) {
|
||||
devices.push(describe_device_info(f::fido_dev_info_ptr(infos, index)));
|
||||
}
|
||||
let suffix = if found > SUPPORT_DIAGNOSTIC_DEVICE_LIMIT {
|
||||
format!(
|
||||
"; truncated={} more",
|
||||
found - SUPPORT_DIAGNOSTIC_DEVICE_LIMIT
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
format!(
|
||||
"Probe: fido_dev_info_manifest found {found} device(s): [{}]{suffix}.",
|
||||
devices.join("; ")
|
||||
)
|
||||
};
|
||||
f::fido_dev_info_free(&mut infos_owned, MANIFEST_COUNT);
|
||||
detail
|
||||
}
|
||||
}
|
||||
|
||||
fn uv_opt(requirement: u32, pin: Option<&CString>) -> f::fido_opt_t {
|
||||
if pin.is_some() {
|
||||
return f::fido_opt_t_FIDO_OPT_OMIT;
|
||||
}
|
||||
match requirement {
|
||||
USER_VERIFICATION_REQUIRED => f::fido_opt_t_FIDO_OPT_TRUE,
|
||||
_ => f::fido_opt_t_FIDO_OPT_OMIT,
|
||||
}
|
||||
}
|
||||
|
||||
fn pin_cstring(pin: Option<&str>, prefix: &str) -> Result<Option<CString>> {
|
||||
let Some(pin) = pin.filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
CString::new(pin)
|
||||
.map(Some)
|
||||
.map_err(|_| ceremony_error(prefix, "PinInvalid (PIN contains an invalid character)"))
|
||||
}
|
||||
|
||||
fn pin_ptr(pin: Option<&CString>) -> *const c_char {
|
||||
pin.map_or(ptr::null(), |value| value.as_ptr())
|
||||
}
|
||||
|
||||
fn dev_has_pin(dev: *mut f::fido_dev_t) -> bool {
|
||||
unsafe { f::fido_dev_has_pin(dev) }
|
||||
}
|
||||
|
||||
fn dev_has_uv(dev: *mut f::fido_dev_t) -> bool {
|
||||
unsafe { f::fido_dev_has_uv(dev) }
|
||||
}
|
||||
|
||||
fn pin_retry_count(dev: *mut f::fido_dev_t) -> Option<c_int> {
|
||||
let mut retries: c_int = 0;
|
||||
let rc = unsafe { f::fido_dev_get_retry_count(dev, &mut retries) };
|
||||
if rc == f::FIDO_OK as c_int {
|
||||
Some(retries)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn ceremony_failure(prefix: &str, dev: &DevGuard, operation: &str, rc: c_int) -> napi::Error {
|
||||
if rc == f::FIDO_ERR_PIN_REQUIRED as c_int || rc == f::FIDO_ERR_PIN_AUTH_INVALID as c_int {
|
||||
return ceremony_error(prefix, "PinRequired");
|
||||
}
|
||||
if rc == f::FIDO_ERR_PIN_INVALID as c_int {
|
||||
let detail = match pin_retry_count(dev.0) {
|
||||
Some(retries) => format!("PinInvalid retriesRemaining={retries}"),
|
||||
None => "PinInvalid".to_owned(),
|
||||
};
|
||||
return ceremony_error(prefix, &detail);
|
||||
}
|
||||
if rc == f::FIDO_ERR_PIN_AUTH_BLOCKED as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"PinAuthBlocked (too many failed attempts; unplug and reinsert the security key)",
|
||||
);
|
||||
}
|
||||
if rc == f::FIDO_ERR_PIN_BLOCKED as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"PinBlocked (the security key PIN is locked; the key must be reset)",
|
||||
);
|
||||
}
|
||||
if rc == f::FIDO_ERR_PIN_NOT_SET as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"PinNotSet (the security key requires a PIN to be configured first)",
|
||||
);
|
||||
}
|
||||
if rc == f::FIDO_ERR_UV_BLOCKED as c_int || rc == f::FIDO_ERR_UV_INVALID as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"UserVerificationBlocked (on-key user verification failed; use the key PIN or reset the key)",
|
||||
);
|
||||
}
|
||||
if rc == f::FIDO_ERR_ACTION_TIMEOUT as c_int || rc == f::FIDO_ERR_USER_ACTION_TIMEOUT as c_int {
|
||||
return ceremony_error(prefix, "Timeout (the security key was not touched in time)");
|
||||
}
|
||||
if rc == f::FIDO_ERR_OPERATION_DENIED as c_int || rc == f::FIDO_ERR_KEEPALIVE_CANCEL as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"NotAllowed (the request was denied on the security key)",
|
||||
);
|
||||
}
|
||||
if rc == f::FIDO_ERR_UNSUPPORTED_OPTION as c_int {
|
||||
return ceremony_error(
|
||||
prefix,
|
||||
"UnsupportedOption (the security key does not support the requested options)",
|
||||
);
|
||||
}
|
||||
let message = format!("Fido2CallFailed ({})", fido_call_detail(operation, rc));
|
||||
ceremony_error(prefix, &message)
|
||||
}
|
||||
|
||||
fn selected_cose_alg(algs: &[i32]) -> c_int {
|
||||
for &alg in algs {
|
||||
if alg == f::COSE_ES256
|
||||
|| alg == f::COSE_RS256
|
||||
|| alg == f::COSE_EDDSA
|
||||
|| alg == f::COSE_ES384
|
||||
{
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
f::COSE_ES256
|
||||
}
|
||||
|
||||
struct DevGuard(*mut f::fido_dev_t);
|
||||
impl Drop for DevGuard {
|
||||
fn drop(&mut self) {
|
||||
if !self.0.is_null() {
|
||||
unsafe {
|
||||
f::fido_dev_close(self.0);
|
||||
let mut p = self.0;
|
||||
f::fido_dev_free(&mut p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_first_device(timeout_ms: u32, prefix: &str) -> Result<DevGuard> {
|
||||
init();
|
||||
|
||||
unsafe {
|
||||
let infos = f::fido_dev_info_new(MANIFEST_COUNT);
|
||||
if infos.is_null() {
|
||||
return Err(ceremony_error(prefix, "OutOfMemory"));
|
||||
}
|
||||
let mut infos_owned = infos;
|
||||
let mut found: usize = 0;
|
||||
let rc = f::fido_dev_info_manifest(infos, MANIFEST_COUNT, &mut found);
|
||||
if rc != f::FIDO_OK as c_int {
|
||||
f::fido_dev_info_free(&mut infos_owned, MANIFEST_COUNT);
|
||||
let message = format!(
|
||||
"Fido2CallFailed ({})",
|
||||
fido_call_detail("fido_dev_info_manifest", rc)
|
||||
);
|
||||
return Err(ceremony_error(prefix, &message));
|
||||
}
|
||||
if found == 0 {
|
||||
f::fido_dev_info_free(&mut infos_owned, MANIFEST_COUNT);
|
||||
return Err(ceremony_error(
|
||||
prefix,
|
||||
"NoAuthenticator (fido_dev_info_manifest found 0 devices; check USB-HID CTAP authenticator presence and udev/hidraw permissions)",
|
||||
));
|
||||
}
|
||||
|
||||
let mut opened: Option<*mut f::fido_dev_t> = None;
|
||||
let mut open_failures: Vec<String> = Vec::new();
|
||||
for index in 0..found {
|
||||
let info = f::fido_dev_info_ptr(infos, index);
|
||||
if info.is_null() {
|
||||
continue;
|
||||
}
|
||||
let path = f::fido_dev_info_path(info);
|
||||
if path.is_null() {
|
||||
continue;
|
||||
}
|
||||
let dev = f::fido_dev_new();
|
||||
if dev.is_null() {
|
||||
f::fido_dev_info_free(&mut infos_owned, MANIFEST_COUNT);
|
||||
return Err(ceremony_error(prefix, "OutOfMemory"));
|
||||
}
|
||||
let open_rc = f::fido_dev_open(dev, path);
|
||||
if open_rc == f::FIDO_OK as c_int {
|
||||
if timeout_ms > 0 {
|
||||
let clamped: c_int = timeout_ms.min(c_int::MAX as u32) as c_int;
|
||||
let _ = f::fido_dev_set_timeout(dev, clamped);
|
||||
}
|
||||
opened = Some(dev);
|
||||
break;
|
||||
}
|
||||
open_failures.push(format!(
|
||||
"{} ({})",
|
||||
describe_device_info(info),
|
||||
fido_call_detail("fido_dev_open", open_rc)
|
||||
));
|
||||
let mut p = dev;
|
||||
f::fido_dev_free(&mut p);
|
||||
}
|
||||
|
||||
f::fido_dev_info_free(&mut infos_owned, MANIFEST_COUNT);
|
||||
opened.map(DevGuard).ok_or_else(|| {
|
||||
let message = format!(
|
||||
"NoAuthenticator (manifest found {found} device(s), but none opened: {})",
|
||||
open_failures.join("; ")
|
||||
);
|
||||
ceremony_error(prefix, &message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn check(code: c_int, prefix: &str) -> Result<()> {
|
||||
if code == f::FIDO_OK as c_int {
|
||||
Ok(())
|
||||
} else {
|
||||
let message = format!("Fido2CallFailed ({})", fido_call_detail("libfido2", code));
|
||||
Err(ceremony_error(prefix, &message))
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_bytes(ptr: *const u8, len: usize) -> Vec<u8> {
|
||||
if len == 0 || ptr.is_null() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
slice.to_vec()
|
||||
}
|
||||
|
||||
pub fn make_credential(input: &mut CreateInput) -> Result<CreateResult> {
|
||||
let dev = open_first_device(input.timeout_ms, CREATE_PREFIX)?;
|
||||
let pin = pin_cstring(input.pin.as_deref(), CREATE_PREFIX)?;
|
||||
if pin.is_none() && dev_has_pin(dev.0) && !dev_has_uv(dev.0) {
|
||||
return Err(ceremony_error(CREATE_PREFIX, "PinRequired"));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let cred = f::fido_cred_new();
|
||||
if cred.is_null() {
|
||||
return Err(ceremony_error(CREATE_PREFIX, "OutOfMemory"));
|
||||
}
|
||||
struct CredGuard(*mut f::fido_cred_t);
|
||||
impl Drop for CredGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut p = self.0;
|
||||
unsafe { f::fido_cred_free(&mut p) };
|
||||
}
|
||||
}
|
||||
let cred_owned = CredGuard(cred);
|
||||
|
||||
check(
|
||||
f::fido_cred_set_type(cred, selected_cose_alg(&input.pub_key_algs)),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
check(
|
||||
f::fido_cred_set_clientdata(
|
||||
cred,
|
||||
input.client_data_json.as_ptr(),
|
||||
input.client_data_json.len(),
|
||||
),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
let rp_id_c = CString::new(input.rp_id.clone())
|
||||
.map_err(|_| ceremony_error(CREATE_PREFIX, "InvalidRpId"))?;
|
||||
let rp_name_c = CString::new(input.rp_name.clone())
|
||||
.map_err(|_| ceremony_error(CREATE_PREFIX, "InvalidRpName"))?;
|
||||
check(
|
||||
f::fido_cred_set_rp(cred, rp_id_c.as_ptr(), rp_name_c.as_ptr()),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
let user_name_c = CString::new(input.user_name.clone())
|
||||
.map_err(|_| ceremony_error(CREATE_PREFIX, "InvalidUserName"))?;
|
||||
let display_c = CString::new(input.user_display_name.clone())
|
||||
.map_err(|_| ceremony_error(CREATE_PREFIX, "InvalidDisplayName"))?;
|
||||
check(
|
||||
f::fido_cred_set_user(
|
||||
cred,
|
||||
input.user_id.as_ptr(),
|
||||
input.user_id.len(),
|
||||
user_name_c.as_ptr(),
|
||||
display_c.as_ptr(),
|
||||
ptr::null(),
|
||||
),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
if input.require_resident_key {
|
||||
check(
|
||||
f::fido_cred_set_rk(cred, f::fido_opt_t_FIDO_OPT_TRUE),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
}
|
||||
check(
|
||||
f::fido_cred_set_uv(cred, uv_opt(input.user_verification, pin.as_ref())),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
for credential in &input.exclude_credentials {
|
||||
check(
|
||||
f::fido_cred_exclude(cred, credential.id.as_ptr(), credential.id.len()),
|
||||
CREATE_PREFIX,
|
||||
)?;
|
||||
}
|
||||
|
||||
let rc = f::fido_dev_make_cred(dev.0, cred, pin_ptr(pin.as_ref()));
|
||||
if rc != f::FIDO_OK as c_int {
|
||||
let _ = f::fido_dev_cancel(dev.0);
|
||||
return Err(ceremony_failure(
|
||||
CREATE_PREFIX,
|
||||
&dev,
|
||||
"fido_dev_make_cred",
|
||||
rc,
|
||||
));
|
||||
}
|
||||
|
||||
let raw_ptr = f::fido_cred_authdata_raw_ptr(cred);
|
||||
let auth_data = if !raw_ptr.is_null() {
|
||||
let len = f::fido_cred_authdata_raw_len(cred);
|
||||
copy_bytes(raw_ptr, len)
|
||||
} else {
|
||||
let ptr = f::fido_cred_authdata_ptr(cred);
|
||||
let len = f::fido_cred_authdata_len(cred);
|
||||
copy_bytes(ptr, len)
|
||||
};
|
||||
let att_stmt = copy_bytes(
|
||||
f::fido_cred_attstmt_ptr(cred),
|
||||
f::fido_cred_attstmt_len(cred),
|
||||
);
|
||||
let fmt_ptr = f::fido_cred_fmt(cred);
|
||||
let fmt = if fmt_ptr.is_null() {
|
||||
"none".to_owned()
|
||||
} else {
|
||||
CStr::from_ptr(fmt_ptr).to_string_lossy().into_owned()
|
||||
};
|
||||
let attestation_object = build_attestation_object(&fmt, &auth_data, &att_stmt)?;
|
||||
|
||||
let raw_id = copy_bytes(f::fido_cred_id_ptr(cred), f::fido_cred_id_len(cred));
|
||||
let client_data_json = input.client_data_json.clone();
|
||||
|
||||
drop(cred_owned);
|
||||
drop(dev);
|
||||
|
||||
Ok(CreateResult {
|
||||
raw_id,
|
||||
attestation_object,
|
||||
client_data_json,
|
||||
used_transport: TRANSPORT_USB,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_assertion(input: &mut GetInput) -> Result<GetResult> {
|
||||
let dev = open_first_device(input.timeout_ms, GET_PREFIX)?;
|
||||
let pin = pin_cstring(input.pin.as_deref(), GET_PREFIX)?;
|
||||
if pin.is_none()
|
||||
&& input.user_verification == USER_VERIFICATION_REQUIRED
|
||||
&& dev_has_pin(dev.0)
|
||||
&& !dev_has_uv(dev.0)
|
||||
{
|
||||
return Err(ceremony_error(GET_PREFIX, "PinRequired"));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let assertion = f::fido_assert_new();
|
||||
if assertion.is_null() {
|
||||
return Err(ceremony_error(GET_PREFIX, "OutOfMemory"));
|
||||
}
|
||||
struct AssertGuard(*mut f::fido_assert_t);
|
||||
impl Drop for AssertGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut p = self.0;
|
||||
unsafe { f::fido_assert_free(&mut p) };
|
||||
}
|
||||
}
|
||||
let assert_owned = AssertGuard(assertion);
|
||||
|
||||
check(
|
||||
f::fido_assert_set_clientdata(
|
||||
assertion,
|
||||
input.client_data_json.as_ptr(),
|
||||
input.client_data_json.len(),
|
||||
),
|
||||
GET_PREFIX,
|
||||
)?;
|
||||
let rp_id_c = CString::new(input.rp_id.clone())
|
||||
.map_err(|_| ceremony_error(GET_PREFIX, "InvalidRpId"))?;
|
||||
check(
|
||||
f::fido_assert_set_rp(assertion, rp_id_c.as_ptr()),
|
||||
GET_PREFIX,
|
||||
)?;
|
||||
check(
|
||||
f::fido_assert_set_up(assertion, f::fido_opt_t_FIDO_OPT_TRUE),
|
||||
GET_PREFIX,
|
||||
)?;
|
||||
check(
|
||||
f::fido_assert_set_uv(assertion, uv_opt(input.user_verification, pin.as_ref())),
|
||||
GET_PREFIX,
|
||||
)?;
|
||||
for credential in &input.allow_credentials {
|
||||
check(
|
||||
f::fido_assert_allow_cred(assertion, credential.id.as_ptr(), credential.id.len()),
|
||||
GET_PREFIX,
|
||||
)?;
|
||||
}
|
||||
|
||||
let rc = f::fido_dev_get_assert(dev.0, assertion, pin_ptr(pin.as_ref()));
|
||||
if rc != f::FIDO_OK as c_int {
|
||||
let _ = f::fido_dev_cancel(dev.0);
|
||||
return Err(ceremony_failure(
|
||||
GET_PREFIX,
|
||||
&dev,
|
||||
"fido_dev_get_assert",
|
||||
rc,
|
||||
));
|
||||
}
|
||||
if f::fido_assert_count(assertion) == 0 {
|
||||
return Err(ceremony_error(GET_PREFIX, "InvalidWebAuthnResult"));
|
||||
}
|
||||
|
||||
let raw_ptr = f::fido_assert_authdata_raw_ptr(assertion, 0);
|
||||
let auth_data = if !raw_ptr.is_null() {
|
||||
let len = f::fido_assert_authdata_raw_len(assertion, 0);
|
||||
copy_bytes(raw_ptr, len)
|
||||
} else {
|
||||
let ptr = f::fido_assert_authdata_ptr(assertion, 0);
|
||||
let len = f::fido_assert_authdata_len(assertion, 0);
|
||||
copy_bytes(ptr, len)
|
||||
};
|
||||
let user_id_len = f::fido_assert_user_id_len(assertion, 0);
|
||||
let user_handle = if user_id_len > 0 {
|
||||
Some(copy_bytes(
|
||||
f::fido_assert_user_id_ptr(assertion, 0),
|
||||
user_id_len,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut raw_id = copy_bytes(
|
||||
f::fido_assert_id_ptr(assertion, 0),
|
||||
f::fido_assert_id_len(assertion, 0),
|
||||
);
|
||||
if raw_id.is_empty() && input.allow_credentials.len() == 1 {
|
||||
raw_id = input.allow_credentials[0].id.clone();
|
||||
}
|
||||
if raw_id.is_empty() {
|
||||
return Err(ceremony_error(
|
||||
GET_PREFIX,
|
||||
"InvalidWebAuthnResult (authenticator omitted the credential id)",
|
||||
));
|
||||
}
|
||||
let signature = copy_bytes(
|
||||
f::fido_assert_sig_ptr(assertion, 0),
|
||||
f::fido_assert_sig_len(assertion, 0),
|
||||
);
|
||||
let client_data_json = input.client_data_json.clone();
|
||||
|
||||
drop(assert_owned);
|
||||
drop(dev);
|
||||
|
||||
Ok(GetResult {
|
||||
raw_id,
|
||||
authenticator_data: auth_data,
|
||||
signature,
|
||||
user_handle,
|
||||
client_data_json,
|
||||
used_transport: TRANSPORT_USB,
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,304 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
use napi::Result;
|
||||
use windows::Win32::Foundation::{HWND, LPARAM};
|
||||
use windows::Win32::Networking::WindowsWebServices::{
|
||||
WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM, WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM,
|
||||
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS,
|
||||
WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION,
|
||||
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS,
|
||||
WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION, WEBAUTHN_CLIENT_DATA,
|
||||
WEBAUTHN_CLIENT_DATA_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETER,
|
||||
WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION, WEBAUTHN_COSE_CREDENTIAL_PARAMETERS,
|
||||
WEBAUTHN_CREDENTIAL, WEBAUTHN_CREDENTIAL_CURRENT_VERSION, WEBAUTHN_CREDENTIALS,
|
||||
WEBAUTHN_CTAP_TRANSPORT_INTERNAL, WEBAUTHN_CTAP_TRANSPORT_USB, WEBAUTHN_HASH_ALGORITHM_SHA_256,
|
||||
WEBAUTHN_LARGE_BLOB_SUPPORT_NONE, WEBAUTHN_RP_ENTITY_INFORMATION,
|
||||
WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION, WEBAUTHN_USER_ENTITY_INFORMATION,
|
||||
WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION, WebAuthNAuthenticatorGetAssertion,
|
||||
WebAuthNAuthenticatorMakeCredential, WebAuthNFreeAssertion, WebAuthNFreeCredentialAttestation,
|
||||
WebAuthNGetApiVersionNumber, WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable,
|
||||
};
|
||||
use windows::Win32::System::Threading::GetCurrentProcessId;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
EnumWindows, GetDesktopWindow, GetForegroundWindow, GetWindowThreadProcessId, IsWindow,
|
||||
IsWindowVisible,
|
||||
};
|
||||
use windows_core::{BOOL, PCWSTR};
|
||||
|
||||
use crate::common::{
|
||||
CREATE_PREFIX, CreateInput, CreateResult, DescriptorInput, GET_PREFIX, GetInput, GetResult,
|
||||
ceremony_error,
|
||||
};
|
||||
|
||||
pub fn api_version() -> u32 {
|
||||
unsafe { WebAuthNGetApiVersionNumber() }
|
||||
}
|
||||
|
||||
pub fn is_user_verifying_platform_authenticator_available() -> bool {
|
||||
match unsafe { WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable() } {
|
||||
Ok(b) => b.as_bool(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_wide(value: &str) -> Vec<u16> {
|
||||
OsString::from(value).encode_wide().chain(Some(0)).collect()
|
||||
}
|
||||
|
||||
fn window_from_handle(handle: u64) -> Option<HWND> {
|
||||
if handle == 0 {
|
||||
return None;
|
||||
}
|
||||
let hwnd = HWND(handle as usize as *mut core::ffi::c_void);
|
||||
if unsafe { IsWindow(Some(hwnd)) }.as_bool() {
|
||||
Some(hwnd)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn window_process_id(hwnd: HWND) -> u32 {
|
||||
let mut pid = 0u32;
|
||||
unsafe { GetWindowThreadProcessId(hwnd, Some(&mut pid)) };
|
||||
pid
|
||||
}
|
||||
|
||||
unsafe extern "system" fn collect_own_visible_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
|
||||
let slot = unsafe { &mut *(lparam.0 as *mut Option<HWND>) };
|
||||
if window_process_id(hwnd) == unsafe { GetCurrentProcessId() }
|
||||
&& unsafe { IsWindowVisible(hwnd) }.as_bool()
|
||||
{
|
||||
*slot = Some(hwnd);
|
||||
return BOOL(0);
|
||||
}
|
||||
BOOL(1)
|
||||
}
|
||||
|
||||
fn interaction_window(handle: u64) -> HWND {
|
||||
if let Some(hwnd) = window_from_handle(handle) {
|
||||
return hwnd;
|
||||
}
|
||||
let current_pid = unsafe { GetCurrentProcessId() };
|
||||
let foreground = unsafe { GetForegroundWindow() };
|
||||
if !foreground.is_invalid() && window_process_id(foreground) == current_pid {
|
||||
return foreground;
|
||||
}
|
||||
let mut own_window: Option<HWND> = None;
|
||||
let _ = unsafe {
|
||||
EnumWindows(
|
||||
Some(collect_own_visible_window),
|
||||
LPARAM(&raw mut own_window as isize),
|
||||
)
|
||||
};
|
||||
if let Some(hwnd) = own_window {
|
||||
return hwnd;
|
||||
}
|
||||
if !foreground.is_invalid() {
|
||||
return foreground;
|
||||
}
|
||||
unsafe { GetDesktopWindow() }
|
||||
}
|
||||
|
||||
const PUBLIC_KEY_WIDE: &[u16] = &[
|
||||
'p' as u16, 'u' as u16, 'b' as u16, 'l' as u16, 'i' as u16, 'c' as u16, '-' as u16, 'k' as u16,
|
||||
'e' as u16, 'y' as u16, 0,
|
||||
];
|
||||
|
||||
fn build_credential_list(
|
||||
descriptors: &[DescriptorInput],
|
||||
) -> (Vec<WEBAUTHN_CREDENTIAL>, WEBAUTHN_CREDENTIALS) {
|
||||
let mut backing: Vec<WEBAUTHN_CREDENTIAL> = descriptors
|
||||
.iter()
|
||||
.map(|d| WEBAUTHN_CREDENTIAL {
|
||||
dwVersion: WEBAUTHN_CREDENTIAL_CURRENT_VERSION,
|
||||
cbId: d.id.len() as u32,
|
||||
pbId: d.id.as_ptr() as *mut u8,
|
||||
pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
|
||||
})
|
||||
.collect();
|
||||
let list = WEBAUTHN_CREDENTIALS {
|
||||
cCredentials: backing.len() as u32,
|
||||
pCredentials: if backing.is_empty() {
|
||||
std::ptr::null_mut()
|
||||
} else {
|
||||
backing.as_mut_ptr()
|
||||
},
|
||||
};
|
||||
(backing, list)
|
||||
}
|
||||
|
||||
fn copy_buffer(ptr: *const u8, len: u32) -> Result<Vec<u8>> {
|
||||
if len == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if ptr.is_null() {
|
||||
return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
|
||||
}
|
||||
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
|
||||
Ok(slice.to_vec())
|
||||
}
|
||||
|
||||
pub fn make_credential(input: &mut CreateInput) -> Result<CreateResult> {
|
||||
let rp_id_w = to_wide(&input.rp_id);
|
||||
let rp_name_w = to_wide(&input.rp_name);
|
||||
let user_name_w = to_wide(&input.user_name);
|
||||
let display_w = to_wide(&input.user_display_name);
|
||||
|
||||
let rp = WEBAUTHN_RP_ENTITY_INFORMATION {
|
||||
dwVersion: WEBAUTHN_RP_ENTITY_INFORMATION_CURRENT_VERSION,
|
||||
pwszId: PCWSTR(rp_id_w.as_ptr()),
|
||||
pwszName: PCWSTR(rp_name_w.as_ptr()),
|
||||
pwszIcon: PCWSTR::null(),
|
||||
};
|
||||
let user = WEBAUTHN_USER_ENTITY_INFORMATION {
|
||||
dwVersion: WEBAUTHN_USER_ENTITY_INFORMATION_CURRENT_VERSION,
|
||||
cbId: input.user_id.len() as u32,
|
||||
pbId: input.user_id.as_ptr() as *mut u8,
|
||||
pwszName: PCWSTR(user_name_w.as_ptr()),
|
||||
pwszIcon: PCWSTR::null(),
|
||||
pwszDisplayName: PCWSTR(display_w.as_ptr()),
|
||||
};
|
||||
|
||||
let mut pub_key_params: Vec<WEBAUTHN_COSE_CREDENTIAL_PARAMETER> = input
|
||||
.pub_key_algs
|
||||
.iter()
|
||||
.map(|&alg| WEBAUTHN_COSE_CREDENTIAL_PARAMETER {
|
||||
dwVersion: WEBAUTHN_COSE_CREDENTIAL_PARAMETER_CURRENT_VERSION,
|
||||
pwszCredentialType: PCWSTR(PUBLIC_KEY_WIDE.as_ptr()),
|
||||
lAlg: alg,
|
||||
})
|
||||
.collect();
|
||||
let params = WEBAUTHN_COSE_CREDENTIAL_PARAMETERS {
|
||||
cCredentialParameters: pub_key_params.len() as u32,
|
||||
pCredentialParameters: pub_key_params.as_mut_ptr(),
|
||||
};
|
||||
|
||||
let client_data = WEBAUTHN_CLIENT_DATA {
|
||||
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
|
||||
cbClientDataJSON: input.client_data_json.len() as u32,
|
||||
pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
|
||||
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
|
||||
};
|
||||
|
||||
let (_exclude_keep, exclude_list) = build_credential_list(&input.exclude_credentials);
|
||||
|
||||
let options = WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS {
|
||||
dwVersion: api_version()
|
||||
.min(WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS_CURRENT_VERSION),
|
||||
dwTimeoutMilliseconds: input.timeout_ms,
|
||||
CredentialList: exclude_list,
|
||||
dwAuthenticatorAttachment: input.authenticator_attachment,
|
||||
bRequireResidentKey: BOOL(input.require_resident_key as i32),
|
||||
dwUserVerificationRequirement: input.user_verification,
|
||||
dwAttestationConveyancePreference: input.attestation,
|
||||
dwEnterpriseAttestation: input.enterprise_attestation,
|
||||
dwLargeBlobSupport: WEBAUTHN_LARGE_BLOB_SUPPORT_NONE,
|
||||
bPreferResidentKey: BOOL(input.prefer_resident_key as i32),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let attestation_ptr = unsafe {
|
||||
WebAuthNAuthenticatorMakeCredential(
|
||||
interaction_window(input.window_handle),
|
||||
&rp,
|
||||
&user,
|
||||
¶ms,
|
||||
&client_data,
|
||||
Some(&options),
|
||||
)
|
||||
};
|
||||
let attestation_ptr = attestation_ptr
|
||||
.map_err(|err| ceremony_error(CREATE_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
|
||||
if attestation_ptr.is_null() {
|
||||
return Err(ceremony_error(CREATE_PREFIX, "InvalidWebAuthnResult"));
|
||||
}
|
||||
|
||||
let att = unsafe { &*attestation_ptr };
|
||||
let raw_id = copy_buffer(att.pbCredentialId, att.cbCredentialId)?;
|
||||
let attestation_object = copy_buffer(att.pbAttestationObject, att.cbAttestationObject)?;
|
||||
let used_transport = if att.dwVersion >= 3 {
|
||||
att.dwUsedTransport
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
unsafe { WebAuthNFreeCredentialAttestation(Some(attestation_ptr)) };
|
||||
|
||||
Ok(CreateResult {
|
||||
raw_id,
|
||||
attestation_object,
|
||||
client_data_json: input.client_data_json.clone(),
|
||||
used_transport,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_assertion(input: &mut GetInput) -> Result<GetResult> {
|
||||
let rp_id_w = to_wide(&input.rp_id);
|
||||
|
||||
let client_data = WEBAUTHN_CLIENT_DATA {
|
||||
dwVersion: WEBAUTHN_CLIENT_DATA_CURRENT_VERSION,
|
||||
cbClientDataJSON: input.client_data_json.len() as u32,
|
||||
pbClientDataJSON: input.client_data_json.as_ptr() as *mut u8,
|
||||
pwszHashAlgId: WEBAUTHN_HASH_ALGORITHM_SHA_256,
|
||||
};
|
||||
|
||||
let (_allow_keep, allow_list) = build_credential_list(&input.allow_credentials);
|
||||
|
||||
let options = WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS {
|
||||
dwVersion: api_version().min(WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS_CURRENT_VERSION),
|
||||
dwTimeoutMilliseconds: input.timeout_ms,
|
||||
CredentialList: allow_list,
|
||||
dwAuthenticatorAttachment: input.authenticator_attachment,
|
||||
dwUserVerificationRequirement: input.user_verification,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let assertion_ptr = unsafe {
|
||||
WebAuthNAuthenticatorGetAssertion(
|
||||
interaction_window(input.window_handle),
|
||||
PCWSTR(rp_id_w.as_ptr()),
|
||||
&client_data,
|
||||
Some(&options),
|
||||
)
|
||||
};
|
||||
let assertion_ptr = assertion_ptr
|
||||
.map_err(|err| ceremony_error(GET_PREFIX, &format!("WebAuthnCallFailed: {err}")))?;
|
||||
if assertion_ptr.is_null() {
|
||||
return Err(ceremony_error(GET_PREFIX, "InvalidWebAuthnResult"));
|
||||
}
|
||||
|
||||
let asrt = unsafe { &*assertion_ptr };
|
||||
let raw_id = copy_buffer(asrt.Credential.pbId, asrt.Credential.cbId)?;
|
||||
let authenticator_data = copy_buffer(asrt.pbAuthenticatorData, asrt.cbAuthenticatorData)?;
|
||||
let signature = copy_buffer(asrt.pbSignature, asrt.cbSignature)?;
|
||||
let user_handle = if asrt.cbUserId > 0 {
|
||||
Some(copy_buffer(asrt.pbUserId, asrt.cbUserId)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let used_transport = if asrt.dwVersion >= 4 {
|
||||
asrt.dwUsedTransport
|
||||
} else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_PLATFORM {
|
||||
WEBAUTHN_CTAP_TRANSPORT_INTERNAL
|
||||
} else if input.authenticator_attachment == WEBAUTHN_AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM {
|
||||
WEBAUTHN_CTAP_TRANSPORT_USB
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
unsafe { WebAuthNFreeAssertion(assertion_ptr) };
|
||||
|
||||
Ok(GetResult {
|
||||
raw_id,
|
||||
authenticator_data,
|
||||
signature,
|
||||
user_handle,
|
||||
client_data_json: input.client_data_json.clone(),
|
||||
used_transport,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import webauthn from '../index.js';
|
||||
|
||||
test('loader resolves supported native filenames', () => {
|
||||
assert.equal(webauthn._private.nativeFileName('darwin', 'x64'), 'webauthn.darwin-x64.node');
|
||||
assert.equal(webauthn._private.nativeFileName('darwin', 'arm64'), 'webauthn.darwin-arm64.node');
|
||||
assert.equal(webauthn._private.nativeFileName('linux', 'x64'), 'webauthn.linux-x64-gnu.node');
|
||||
assert.equal(webauthn._private.nativeFileName('linux', 'arm64'), 'webauthn.linux-arm64-gnu.node');
|
||||
assert.equal(webauthn._private.nativeFileName('win32', 'x64'), 'webauthn.win32-x64-msvc.node');
|
||||
assert.equal(webauthn._private.nativeFileName('win32', 'arm64'), 'webauthn.win32-arm64-msvc.node');
|
||||
assert.equal(webauthn._private.nativeFileName('freebsd', 'x64'), null);
|
||||
});
|
||||
|
||||
test('loader exposes the Fluxer WebAuthn surface', async () => {
|
||||
assert.equal(typeof webauthn.create, 'function');
|
||||
assert.equal(typeof webauthn.get, 'function');
|
||||
assert.equal(typeof webauthn.getBackendInfo, 'function');
|
||||
assert.equal(typeof webauthn.isSupported, 'function');
|
||||
const info = webauthn.getBackendInfo();
|
||||
const supported = await webauthn.isSupported();
|
||||
assert.equal(typeof supported, 'boolean');
|
||||
assert.equal(supported, info.supported);
|
||||
assert.equal(typeof info, 'object');
|
||||
assert.equal(typeof info.target, 'string');
|
||||
assert.equal(typeof info.backend, 'string');
|
||||
assert.equal(typeof info.supported, 'boolean');
|
||||
assert.equal(typeof info.ceremoniesImplemented, 'boolean');
|
||||
});
|
||||
|
||||
test('normalization creates spec-shaped client data and Windows transport bits', () => {
|
||||
const challenge = Buffer.from([1, 2, 3, 4]);
|
||||
const normalized = webauthn._private.normalizeCreateOptions({
|
||||
origin: 'https://web.canary.fluxer.app/channels/@me',
|
||||
challenge,
|
||||
rp: {id: 'fluxer.app', name: 'Fluxer'},
|
||||
user: {id: Buffer.from('user'), name: 'name', displayName: 'Name'},
|
||||
pubKeyCredParams: [{type: 'public-key', alg: -7}],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
residentKey: 'preferred',
|
||||
userVerification: 'required',
|
||||
},
|
||||
excludeCredentials: [{type: 'public-key', id: Buffer.from('cred'), transports: ['internal', 'hybrid']}],
|
||||
attestation: 'none',
|
||||
});
|
||||
assert.equal(normalized.rpId, 'fluxer.app');
|
||||
assert.equal(normalized.authenticatorAttachment, 1);
|
||||
assert.equal(normalized.userVerification, 1);
|
||||
assert.equal(normalized.preferResidentKey, true);
|
||||
assert.equal(normalized.requireResidentKey, false);
|
||||
assert.equal(normalized.excludeCredentials[0].transports, 0x10 | 0x20);
|
||||
assert.deepEqual(JSON.parse(normalized.clientDataJSON.toString('utf8')), {
|
||||
type: 'webauthn.create',
|
||||
challenge: 'AQIDBA',
|
||||
origin: 'https://web.canary.fluxer.app',
|
||||
crossOrigin: false,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Command,
|
||||
[string]$OutFile = 'C:\tools\vm_test_out.txt',
|
||||
[int]$TimeoutSec = 120
|
||||
)
|
||||
|
||||
$taskName = 'FluxerVmTest'
|
||||
if (Test-Path $OutFile) { Remove-Item -Force $OutFile }
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $Command + ' > "' + $OutFile + '" 2>&1')
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'hampus' -LogonType Interactive
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Principal $principal -Settings $settings -Force | Out-Null
|
||||
Start-ScheduledTask -TaskName $taskName
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$state = (Get-ScheduledTask -TaskName $taskName).State
|
||||
} while ($state -ne 'Ready' -and (Get-Date) -lt $deadline)
|
||||
|
||||
$info = Get-ScheduledTaskInfo -TaskName $taskName
|
||||
Write-Output ('TASK-STATE: ' + $state + ' LAST-RESULT: ' + $info.LastTaskResult)
|
||||
if (Test-Path $OutFile) {
|
||||
Write-Output '--- OUTPUT ---'
|
||||
Get-Content $OutFile
|
||||
} else {
|
||||
Write-Output 'NO OUTPUT FILE'
|
||||
}
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
@@ -0,0 +1,22 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
device_path = sys.argv[1].encode() if len(sys.argv) > 1 else b"/dev/hidraw4"
|
||||
pin = sys.argv[2].encode() if len(sys.argv) > 2 else b"123456"
|
||||
|
||||
lib = ctypes.CDLL("libfido2.so.1")
|
||||
lib.fido_init(0)
|
||||
lib.fido_dev_new.restype = ctypes.c_void_p
|
||||
lib.fido_strerr.restype = ctypes.c_char_p
|
||||
|
||||
dev = lib.fido_dev_new()
|
||||
rc = lib.fido_dev_open(ctypes.c_void_p(dev), device_path)
|
||||
if rc != 0:
|
||||
print(f"open failed rc={rc} {lib.fido_strerr(rc).decode()}")
|
||||
sys.exit(1)
|
||||
rc = lib.fido_dev_set_pin(ctypes.c_void_p(dev), pin, None)
|
||||
print(f"set_pin rc={rc} {lib.fido_strerr(rc).decode()}")
|
||||
lib.fido_dev_close(ctypes.c_void_p(dev))
|
||||
sys.exit(0 if rc == 0 else 1)
|
||||
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
virtual_fido "github.com/bulwarkid/virtual-fido"
|
||||
"github.com/bulwarkid/virtual-fido/fido_client"
|
||||
)
|
||||
|
||||
type autoApproveSupport struct {
|
||||
vaultFilename string
|
||||
}
|
||||
|
||||
func (support *autoApproveSupport) ApproveClientAction(action fido_client.ClientAction, params fido_client.ClientActionRequestParams) bool {
|
||||
fmt.Printf("auto-approving action=%d relyingParty=%q user=%q\n", action, params.RelyingParty, params.UserName)
|
||||
return true
|
||||
}
|
||||
|
||||
func (support *autoApproveSupport) SaveData(data []byte) {
|
||||
err := os.WriteFile(support.vaultFilename, data, 0o600)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not write vault: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func (support *autoApproveSupport) RetrieveData() []byte {
|
||||
f, err := os.Open(support.vaultFilename)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not open vault: %s", err))
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not read vault: %s", err))
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (support *autoApproveSupport) Passphrase() string {
|
||||
return "vm-test-passphrase"
|
||||
}
|
||||
|
||||
func main() {
|
||||
vault := os.Getenv("VFIDO_VAULT")
|
||||
if vault == "" {
|
||||
vault = "/tmp/vfido-vault.json"
|
||||
}
|
||||
authority := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(0),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Fluxer VM Test Virtual FIDO"},
|
||||
Country: []string{"US"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
authorityCertBytes, err := x509.CreateCertificate(rand.Reader, authority, authority, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
encryptionKey := sha256.Sum256([]byte("fluxer-vm-test"))
|
||||
|
||||
virtual_fido.SetLogOutput(os.Stdout)
|
||||
support := &autoApproveSupport{vaultFilename: vault}
|
||||
client := fido_client.NewDefaultClient(authorityCertBytes, privateKey, encryptionKey, support, support)
|
||||
fmt.Println("virtual-fido USBIP server starting on 127.0.0.1:3240 (bus 2-2)")
|
||||
virtual_fido.Start(client)
|
||||
}
|
||||
Reference in New Issue
Block a user