From 37e4a15383d586af74e4afc3ed60ccad2c889fe7 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Thu, 12 Jun 2025 11:54:32 -0400 Subject: [PATCH 1/6] Add webauthn-rs --- Cargo.lock | 194 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + apps/labrinth/Cargo.toml | 2 + 3 files changed, 197 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 3c29be8c68..607a0c3c5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -514,6 +514,45 @@ dependencies = [ "zbus", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -987,6 +1026,17 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "base64urlsafedata" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f0ad38ce7fbed55985ad5b2197f05cff8324ee6eb6638304e78f0108fae56c" +dependencies = [ + "base64 0.21.7", + "paste", + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1524,6 +1574,23 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "compact_jwt" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "hex", + "openssl", + "serde", + "serde_json", + "tracing", + "url", + "uuid 1.17.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -2020,6 +2087,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.4.0" @@ -4395,6 +4476,7 @@ dependencies = [ "urlencoding", "uuid 1.17.0", "validator", + "webauthn-rs", "webp", "woothee", "yaserde", @@ -5494,6 +5576,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -7010,6 +7101,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -7508,6 +7608,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_cbor_2" +version = "0.12.0-dev" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55" +dependencies = [ + "half 1.8.3", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -9994,6 +10104,73 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-attestation-ca" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e77e8859ecb93b00e4a8e56ae45f8a8dd69b1539e3d32cf4cce1db9a3a0b99" +dependencies = [ + "base64urlsafedata", + "openssl", + "serde", + "tracing", + "uuid 1.17.0", +] + +[[package]] +name = "webauthn-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b44347ee0d66f222043663a6aaf5ec78022b9b11c3a9ed488c21f2bd5680856" +dependencies = [ + "base64urlsafedata", + "serde", + "tracing", + "url", + "uuid 1.17.0", + "webauthn-rs-core", +] + +[[package]] +name = "webauthn-rs-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef48f07ed8f3dfe304d6c48e85317feba0439675f31a13063b2936c9b4eaf0d" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "compact_jwt", + "der-parser", + "hex", + "nom 7.1.3", + "openssl", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tracing", + "url", + "uuid 1.17.0", + "webauthn-attestation-ca", + "webauthn-rs-proto", + "x509-parser", +] + +[[package]] +name = "webauthn-rs-proto" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e1367f70e7dc7b83afc971ce8a54d578f4fdf488ea093021180e073744a69f" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "serde", + "serde_json", + "url", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -10677,6 +10854,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "xattr" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index eabe40a958..c9c58eee8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ url = "2.5.4" urlencoding = "2.1.3" uuid = "1.17.0" validator = "0.20.0" +webauthn-rs = "0.5.1" webp = { version = "0.3.0", default-features = false } whoami = "1.6.0" winreg = "0.55.0" diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 53f30a27c4..3423b1be24 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -119,6 +119,8 @@ ariadne.workspace = true clap = { workspace = true, features = ["derive"] } +webauthn-rs.workspace = true + [target.'cfg(target_os = "linux")'.dependencies] tikv-jemallocator = { workspace = true, features = ["profiling", "unprefixed_malloc_on_supported_platforms"] } tikv-jemalloc-ctl = { workspace = true, features = ["stats"] } From 4cf6429a06133a065e5fb32b244354f4d73da9c0 Mon Sep 17 00:00:00 2001 From: IThundxr Date: Mon, 14 Jul 2025 20:32:48 -0400 Subject: [PATCH 2/6] Start work on hw security key support --- .gitignore | 2 + Cargo.lock | 52 +++++------ Cargo.toml | 2 +- apps/frontend/src/pages/settings/account.vue | 89 +++++++++++++++++++ apps/labrinth/Cargo.toml | 2 +- apps/labrinth/src/auth/mod.rs | 2 + apps/labrinth/src/auth/webauthn.rs | 30 +++++++ .../labrinth/src/database/models/flow_item.rs | 7 ++ apps/labrinth/src/lib.rs | 2 + apps/labrinth/src/models/v3/users.rs | 7 +- apps/labrinth/src/routes/internal/flows.rs | 56 +++++++++++- apps/labrinth/src/routes/internal/session.rs | 19 ++++ apps/labrinth/src/routes/mod.rs | 4 + packages/assets/icons/security-key.svg | 1 + packages/assets/index.ts | 2 + packages/utils/types.ts | 1 + 16 files changed, 242 insertions(+), 36 deletions(-) create mode 100644 apps/labrinth/src/auth/webauthn.rs create mode 100644 packages/assets/icons/security-key.svg diff --git a/.gitignore b/.gitignore index 41ffebf9a3..ac719fc2cc 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ app-playground-data/* apps/frontend/.env .astro + +container_data/ diff --git a/Cargo.lock b/Cargo.lock index 607a0c3c5d..bbe7c1edab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1028,12 +1028,12 @@ checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "base64urlsafedata" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f0ad38ce7fbed55985ad5b2197f05cff8324ee6eb6638304e78f0108fae56c" +checksum = "e5913e643e4dfb43d5908e9e6f1386f8e0dfde086ecef124a6450c6195d89160" dependencies = [ "base64 0.21.7", - "paste", + "pastey", "serde", ] @@ -1574,23 +1574,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "compact_jwt" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bbab6445446e8d0b07468a01d0bfdae15879de5c440c5e47ae4ae0e18a1fba" -dependencies = [ - "base64 0.21.7", - "base64urlsafedata", - "hex", - "openssl", - "serde", - "serde_json", - "tracing", - "url", - "uuid 1.17.0", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -5641,9 +5624,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.108" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -5797,6 +5780,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8cb46bdc156b1c90460339ae6bfd45ba0394e5effbaa640badb4987fdc261" + [[package]] name = "pathdiff" version = "0.2.3" @@ -10106,12 +10095,13 @@ dependencies = [ [[package]] name = "webauthn-attestation-ca" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e77e8859ecb93b00e4a8e56ae45f8a8dd69b1539e3d32cf4cce1db9a3a0b99" +checksum = "384e43534efe4e8f56c4eb1615a27e24d2ff29281385c843cf9f16ac1077dbdc" dependencies = [ "base64urlsafedata", "openssl", + "openssl-sys", "serde", "tracing", "uuid 1.17.0", @@ -10119,9 +10109,9 @@ dependencies = [ [[package]] name = "webauthn-rs" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b44347ee0d66f222043663a6aaf5ec78022b9b11c3a9ed488c21f2bd5680856" +checksum = "ed1f861a94557baeb0cf711e3e55d623c46b68f4aab7aa932562f785b8b5f1ab" dependencies = [ "base64urlsafedata", "serde", @@ -10133,17 +10123,17 @@ dependencies = [ [[package]] name = "webauthn-rs-core" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef48f07ed8f3dfe304d6c48e85317feba0439675f31a13063b2936c9b4eaf0d" +checksum = "269c210cd5f183aaca860bb5733187d1dd110ebed54640f8fc1aca31a04aa4dc" dependencies = [ "base64 0.21.7", "base64urlsafedata", - "compact_jwt", "der-parser", "hex", "nom 7.1.3", "openssl", + "openssl-sys", "rand 0.8.5", "rand_chacha 0.3.1", "serde", @@ -10160,9 +10150,9 @@ dependencies = [ [[package]] name = "webauthn-rs-proto" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14e1367f70e7dc7b83afc971ce8a54d578f4fdf488ea093021180e073744a69f" +checksum = "144dbee9abb4bfad78fd283a2613f0312a0ed5955051b7864cfc98679112ae60" dependencies = [ "base64 0.21.7", "base64urlsafedata", diff --git a/Cargo.toml b/Cargo.toml index c9c58eee8f..579828e01b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,7 +155,7 @@ url = "2.5.4" urlencoding = "2.1.3" uuid = "1.17.0" validator = "0.20.0" -webauthn-rs = "0.5.1" +webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation"] } webp = { version = "0.3.0", default-features = false } whoami = "1.6.0" winreg = "0.55.0" diff --git a/apps/frontend/src/pages/settings/account.vue b/apps/frontend/src/pages/settings/account.vue index 10c153f3d3..f54e8b510b 100644 --- a/apps/frontend/src/pages/settings/account.vue +++ b/apps/frontend/src/pages/settings/account.vue @@ -355,6 +355,20 @@ +
+ +
+ +
+
@@ -428,6 +430,7 @@ import { PlusIcon, RightArrowIcon, SaveIcon, + SecurityKeyIcon, SettingsIcon, TrashIcon, UpdatedIcon, @@ -594,32 +597,47 @@ async function removeTwoFactor() { } const mangeWebauthnModal = ref(); -const webauthnSecret = ref(null); -const webauthnFlow = ref(null); -const webauthnStep = ref(0); async function showWebauthnModal() { - webauthnStep.value = 0; - // twoFactorCode.value = null; - // twoFactorIncorrect.value = false; if (auth.value.user.has_webauthn) { mangeWebauthnModal.value.show(); return; } - webauthnSecret.value = null; - webauthnFlow.value = null; - // backupCodes.value = []; - mangeWebauthnModal.value.show(); - startLoading(); try { - const res = await useBaseFetch(`auth/webauthn/register/${auth.value.user.username}`, { + const res = await useBaseFetch(`auth/webauthn/register/start/${auth.value.user.username}`, { + method: "POST", + internal: true, + }); + + const publicKey = res.challenge.publicKey; + publicKey.challenge = b64urlToUint8Array(publicKey.challenge); + publicKey.user.id = b64urlToUint8Array(publicKey.user.id); + + const credential = await navigator.credentials.create({ publicKey }); + + console.log("Created webauthn credential", credential); + + const jsonCredential = { + id: credential.id, + rawId: uint8ArrayToB64url(credential.rawId), + type: credential.type, + response: { + clientDataJSON: uint8ArrayToB64url(credential.response.clientDataJSON), + attestationObject: uint8ArrayToB64url(credential.response.attestationObject), + }, + }; + + await useBaseFetch("auth/webauthn/register/finish", { method: "POST", - internal: true + internal: true, + body: { + cred: jsonCredential, + flow: res.flow + }, }); - webauthnSecret.value = res.secret; - webauthnFlow.value = res.flow; + mangeWebauthnModal.value.show(); } catch (err) { data.$notify({ group: "main", @@ -631,41 +649,23 @@ async function showWebauthnModal() { stopLoading(); } -// TODO - webauthn -async function createCredential(ccr) { - const publicKeyOptions = convertCcrToPublicKeyOptions(ccr); - - try { - const credential = await navigator.credentials.create({ - publicKey: publicKeyOptions, - }); - - console.log("created cred", credential); - return credential; - } catch (err) { - console.error("failed to create cred", err); - throw err; - } +function b64urlToUint8Array(b64url) { + const base64 = b64url + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(b64url.length + (4 - b64url.length % 4) % 4, '='); + const binary = atob(base64); + return Uint8Array.from(binary, c => c.charCodeAt(0)); } -function convertCcrToPublicKeyOptions(ccr) { - return { - challenge: Uint8Array.from(atob(ccr.challenge), c => c.charCodeAt(0)), - rp: { - name: ccr.rp.name, - id: ccr.rp.id, - }, - user: { - id: Uint8Array.from(atob(ccr.user.id), c => c.charCodeAt(0)), - name: ccr.user.name, - displayName: ccr.user.displayName, - }, - pubKeyCredParams: ccr.pubKeyCredParams, - timeout: ccr.timeout, - attestation: ccr.attestation, - authenticatorSelection: ccr.authenticatorSelection, - extensions: ccr.extensions, - }; +function uint8ArrayToB64url(buf) { + let binary = ""; + const bytes = new Uint8Array(buf); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + const base64 = btoa(binary); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } const authProviders = [ diff --git a/apps/frontend/src/pages/user/[id].vue b/apps/frontend/src/pages/user/[id].vue index 11f5678667..98499ca2f4 100644 --- a/apps/frontend/src/pages/user/[id].vue +++ b/apps/frontend/src/pages/user/[id].vue @@ -51,6 +51,13 @@ {{ user.has_totp ? "Yes" : "No" }}
+ +
+ Has Hardware Security Key + + {{ user.has_webauthn ? "Yes" : "No" }} + +