From c380d9c3792f6587b22e417c82adf4de54695d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 7 Jun 2021 23:34:00 +0200 Subject: [PATCH] Support for webauthn and u2f->webauthn migrations --- Cargo.lock | 177 ++++++++----- Cargo.toml | 22 +- docker/Dockerfile.j2 | 4 +- docker/amd64/Dockerfile | 12 +- docker/amd64/Dockerfile.alpine | 12 +- docker/arm64/Dockerfile | 12 +- docker/armv6/Dockerfile | 12 +- docker/armv7/Dockerfile | 12 +- docker/armv7/Dockerfile.alpine | 12 +- src/api/core/two_factor/mod.rs | 2 + src/api/core/two_factor/u2f.rs | 13 +- src/api/core/two_factor/webauthn.rs | 394 ++++++++++++++++++++++++++++ src/api/identity.rs | 6 + src/api/mod.rs | 4 +- src/db/mod.rs | 4 +- src/db/models/two_factor.rs | 72 +++++ src/error.rs | 10 +- src/main.rs | 2 + 18 files changed, 655 insertions(+), 127 deletions(-) create mode 100644 src/api/core/two_factor/webauthn.rs diff --git a/Cargo.lock b/Cargo.lock index e33faa12..bc50c829 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "percent-encoding 2.1.0", - "time 0.2.26", + "time 0.2.27", "version_check 0.9.3", ] @@ -328,7 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdf8865bac3d9a3bde5bde9088ca431b11f5d37c7a578b8086af77248b76627" dependencies = [ "percent-encoding 2.1.0", - "time 0.2.26", + "time 0.2.27", "version_check 0.9.3", ] @@ -344,7 +344,7 @@ dependencies = [ "publicsuffix 1.5.6", "serde", "serde_json", - "time 0.2.26", + "time 0.2.27", "url 2.2.2", ] @@ -360,7 +360,7 @@ dependencies = [ "publicsuffix 2.1.0", "serde", "serde_json", - "time 0.2.26", + "time 0.2.27", "url 2.2.2", ] @@ -469,7 +469,7 @@ dependencies = [ "bitflags", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -497,7 +497,7 @@ checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -747,7 +747,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -866,10 +866,16 @@ dependencies = [ ] [[package]] -name = "handlebars" -version = "3.5.5" +name = "half" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" + +[[package]] +name = "handlebars" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2060119114dd8a8bc87facce6384751af8280a7adc8e203c023c95cbb11f5663" dependencies = [ "log 0.4.14", "pest", @@ -944,7 +950,7 @@ dependencies = [ "markup5ever", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -1101,9 +1107,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" +checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itoa" @@ -1195,9 +1201,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.96" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5600b4e6efc5421841a2138a6b082e07fe12f9aaa12783d50e5d13325b26b4fc" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "libsqlite3-sys" @@ -1317,7 +1323,7 @@ dependencies = [ "migrations_internals", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -1376,9 +1382,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.11" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" dependencies = [ "libc", "log 0.4.14", @@ -1422,9 +1428,9 @@ dependencies = [ [[package]] name = "multipart" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" dependencies = [ "buf_redux", "httparse", @@ -1432,7 +1438,7 @@ dependencies = [ "mime 0.3.16", "mime_guess", "quick-error 1.2.3", - "rand 0.7.3", + "rand 0.8.4", "safemem", "tempfile", "twoway", @@ -1538,7 +1544,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -1585,18 +1591,18 @@ dependencies = [ [[package]] name = "object" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bc1d42047cf336f0f939c99e97183cf31551bf0f2865a2ec9c8d91fd4ffb5e" +checksum = "a38f2be3697a57b4060074ff41b44c16870d916ad7877c17696e063257482bc7" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "opaque-debug" @@ -1816,7 +1822,7 @@ dependencies = [ "pest_meta", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2053,14 +2059,14 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha 0.3.0", - "rand_core 0.6.2", - "rand_hc 0.3.0", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] @@ -2075,12 +2081,12 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.2", + "rand_core 0.6.3", ] [[package]] @@ -2109,9 +2115,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom 0.2.3", ] @@ -2127,11 +2133,11 @@ dependencies = [ [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core 0.6.2", + "rand_core 0.6.3", ] [[package]] @@ -2154,9 +2160,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" dependencies = [ "bitflags", ] @@ -2216,7 +2222,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "time 0.2.26", + "time 0.2.27", "tokio", "tokio-native-tls", "tokio-socks", @@ -2277,7 +2283,7 @@ dependencies = [ "rocket_codegen", "rocket_http", "state", - "time 0.2.26", + "time 0.2.27", "toml", "version_check 0.9.3", "yansi", @@ -2322,7 +2328,7 @@ dependencies = [ "rustls", "smallvec 1.6.1", "state", - "time 0.2.26", + "time 0.2.27", "unicode-xid 0.2.2", ] @@ -2463,6 +2469,25 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.126" @@ -2471,7 +2496,7 @@ checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2653,7 +2678,7 @@ dependencies = [ "quote 1.0.9", "serde", "serde_derive", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2669,7 +2694,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2722,9 +2747,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", @@ -2757,7 +2782,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand 0.8.3", + "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi 0.3.9", @@ -2791,7 +2816,7 @@ checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2816,9 +2841,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" dependencies = [ "const_fn", "libc", @@ -2841,15 +2866,15 @@ dependencies = [ [[package]] name = "time-macros-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", "proc-macro2 1.0.27", "quote 1.0.9", "standback", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -2869,17 +2894,18 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" +checksum = "c79ba603c337335df6ba6dd6afc38c38a7d5e1b0c871678439ea973cd62a118e" dependencies = [ "autocfg", "bytes 1.0.1", "libc", "memchr", - "mio 0.7.11", + "mio 0.7.13", "num_cpus", "pin-project-lite", + "winapi 0.3.9", ] [[package]] @@ -2954,7 +2980,7 @@ checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", ] [[package]] @@ -3150,7 +3176,7 @@ dependencies = [ "paste", "percent-encoding 2.1.0", "pico-args", - "rand 0.8.3", + "rand 0.8.4", "regex", "reqwest", "ring", @@ -3160,11 +3186,12 @@ dependencies = [ "serde", "serde_json", "syslog", - "time 0.2.26", + "time 0.2.27", "tracing", "u2f", "url 2.2.2", "uuid", + "webauthn-rs", "yubico", ] @@ -3242,7 +3269,7 @@ dependencies = [ "log 0.4.14", "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", "wasm-bindgen-shared", ] @@ -3276,7 +3303,7 @@ checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" dependencies = [ "proc-macro2 1.0.27", "quote 1.0.9", - "syn 1.0.72", + "syn 1.0.73", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3297,6 +3324,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-rs" +version = "0.3.0-alpha.7" +source = "git+https://github.com/dani-garcia/webauthn-rs?rev=70b458f246d207e5b6333ada9f1d5794c5e01da1#70b458f246d207e5b6333ada9f1d5794c5e01da1" +dependencies = [ + "base64 0.13.0", + "log 0.4.14", + "nom 4.1.1", + "openssl", + "rand 0.8.4", + "serde", + "serde_bytes", + "serde_cbor", + "serde_derive", + "serde_json", + "thiserror", +] + [[package]] name = "webpki" version = "0.21.4" @@ -3412,7 +3457,7 @@ dependencies = [ "crypto-mac 0.10.0", "futures", "hmac 0.10.1", - "rand 0.8.3", + "rand 0.8.4", "reqwest", "sha-1 0.9.6", "threadpool", diff --git a/Cargo.toml b/Cargo.toml index ac1c47b9..abb71bb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,8 @@ syslog = "4.0.1" [dependencies] # Web framework for nightly with a focus on ease-of-use, expressibility, and speed. -rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false } -rocket_contrib = "0.5.0-dev" +rocket = { version = "=0.5.0-dev", features = ["tls"], default-features = false } +rocket_contrib = "=0.5.0-dev" # HTTP client reqwest = { version = "0.11.3", features = ["blocking", "json", "gzip", "brotli", "socks", "cookies"] } @@ -41,7 +41,7 @@ bytes = "1.0.1" url = "2.2.2" # multipart/form-data support -multipart = { version = "0.17.1", features = ["server"], default-features = false } +multipart = { version = "0.18.0", features = ["server"], default-features = false } # WebSockets library ws = { version = "0.10.0", package = "parity-ws" } @@ -77,7 +77,7 @@ uuid = { version = "0.8.2", features = ["v4"] } # Date and time libraries chrono = { version = "0.4.19", features = ["serde"] } chrono-tz = "0.5.3" -time = "0.2.26" +time = "0.2.27" # Job scheduler job_scheduler = "1.2.1" @@ -93,6 +93,7 @@ jsonwebtoken = "7.2.0" # U2F library u2f = "0.2.0" +webauthn-rs = "0.3.0-alpha.7" # Yubico Library yubico = { version = "0.10.0", features = ["online-tokio"], default-features = false } @@ -101,7 +102,7 @@ yubico = { version = "0.10.0", features = ["online-tokio"], default-features = f dotenv = { version = "0.15.0", default-features = false } # Lazy initialization -once_cell = "1.7.2" +once_cell = "1.8.0" # Numerical libraries num-traits = "0.2.14" @@ -109,10 +110,10 @@ num-derive = "0.3.3" # Email libraries tracing = { version = "0.1.26", features = ["log"] } # Needed to have lettre trace logging used when SMTP_DEBUG is enabled. -lettre = { version = "0.10.0-rc.1", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false } +lettre = { version = "0.10.0-rc.3", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false } # Template library -handlebars = { version = "3.5.5", features = ["dir_source"] } +handlebars = { version = "4.0.0", features = ["dir_source"] } # For favicon extraction from main website html5ever = "0.25.1" @@ -129,10 +130,10 @@ percent-encoding = "2.1.0" idna = "0.2.3" # CLI argument parsing -pico-args = "0.4.1" +pico-args = "0.4.2" # Logging panics to logfile instead stderr only -backtrace = "0.3.59" +backtrace = "0.3.60" # Macro ident concatenation paste = "1.0.5" @@ -151,3 +152,6 @@ data-url = { git = 'https://github.com/servo/rust-url', package="data-url", rev # In particular, `cron` has since implemented parsing of some common syntax # that wasn't previously supported (https://github.com/zslayton/cron/pull/64). job_scheduler = { git = 'https://github.com/jjlin/job_scheduler', rev = 'ee023418dbba2bfe1e30a5fd7d937f9e33739806' } + +# Add support for U2F appid extension compatibility +webauthn-rs = { git = 'https://github.com/dani-garcia/webauthn-rs', rev = '70b458f246d207e5b6333ada9f1d5794c5e01da1' } diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index e370995f..bca0e0a9 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -44,8 +44,8 @@ # https://docs.docker.com/develop/develop-images/multistage-build/ # https://whitfin.io/speeding-up-rust-docker-builds/ ####################### VAULT BUILD IMAGE ####################### -{% set vault_version = "2.19.0d" %} -{% set vault_image_digest = "sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233" %} +{% set vault_version = "2.20.4" %} +{% set vault_image_digest = "sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b" %} # The web-vault digest specifies a particular web-vault build on Docker Hub. # Using the digest instead of the tag name provides better security, # as the digest of an image is immutable, whereas a tag name can later diff --git a/docker/amd64/Dockerfile b/docker/amd64/Dockerfile index 17e1df81..9ca047c6 100644 --- a/docker/amd64/Dockerfile +++ b/docker/amd64/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM rust:1.51 as build diff --git a/docker/amd64/Dockerfile.alpine b/docker/amd64/Dockerfile.alpine index c699307e..e761aa93 100644 --- a/docker/amd64/Dockerfile.alpine +++ b/docker/amd64/Dockerfile.alpine @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM clux/muslrust:nightly-2021-04-14 as build diff --git a/docker/arm64/Dockerfile b/docker/arm64/Dockerfile index ccd6ad93..4b6a992f 100644 --- a/docker/arm64/Dockerfile +++ b/docker/arm64/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM rust:1.51 as build diff --git a/docker/armv6/Dockerfile b/docker/armv6/Dockerfile index bcf0d8e3..29db7ac3 100644 --- a/docker/armv6/Dockerfile +++ b/docker/armv6/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM rust:1.51 as build diff --git a/docker/armv7/Dockerfile b/docker/armv7/Dockerfile index 689a8591..56b27b83 100644 --- a/docker/armv7/Dockerfile +++ b/docker/armv7/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM rust:1.51 as build diff --git a/docker/armv7/Dockerfile.alpine b/docker/armv7/Dockerfile.alpine index 8148c6b7..e1349eec 100644 --- a/docker/armv7/Dockerfile.alpine +++ b/docker/armv7/Dockerfile.alpine @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/vaultwarden/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull vaultwarden/web-vault:v2.19.0d -# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.19.0d -# [vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233] +# $ docker pull vaultwarden/web-vault:v2.20.4 +# $ docker image inspect --format "{{.RepoDigests}}" vaultwarden/web-vault:v2.20.4 +# [vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 -# [vaultwarden/web-vault:v2.19.0d] +# $ docker image inspect --format "{{.RepoTags}}" vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b +# [vaultwarden/web-vault:v2.20.4] # -FROM vaultwarden/web-vault@sha256:a7bd6bc4db33bd45f723c4b1ac90918b7f80204560683cfc8efd9efd03a9b233 as vault +FROM vaultwarden/web-vault@sha256:810919341388a50d3a88225ce234333f72eb80382953997e9fd5590cca829e1b as vault ########################## BUILD IMAGE ########################## FROM messense/rust-musl-cross:armv7-musleabihf as build diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 0d0d2bd2..83661e37 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -17,6 +17,7 @@ pub mod authenticator; pub mod duo; pub mod email; pub mod u2f; +pub mod webauthn; pub mod yubikey; pub fn routes() -> Vec { @@ -26,6 +27,7 @@ pub fn routes() -> Vec { routes.append(&mut duo::routes()); routes.append(&mut email::routes()); routes.append(&mut u2f::routes()); + routes.append(&mut webauthn::routes()); routes.append(&mut yubikey::routes()); routes diff --git a/src/api/core/two_factor/u2f.rs b/src/api/core/two_factor/u2f.rs index 3455beab..bd40076d 100644 --- a/src/api/core/two_factor/u2f.rs +++ b/src/api/core/two_factor/u2f.rs @@ -94,13 +94,14 @@ struct RegistrationDef { } #[derive(Serialize, Deserialize)] -struct U2FRegistration { - id: i32, - name: String, +pub struct U2FRegistration { + pub id: i32, + pub name: String, #[serde(with = "RegistrationDef")] - reg: Registration, - counter: u32, + pub reg: Registration, + pub counter: u32, compromised: bool, + pub migrated: Option, } impl U2FRegistration { @@ -168,6 +169,7 @@ fn activate_u2f(data: JsonUpcase, headers: Headers, conn: DbConn) reg: registration, compromised: false, counter: 0, + migrated: None, }; let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1; @@ -273,6 +275,7 @@ fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec Vec { + routes![get_webauthn, generate_webauthn_challenge, activate_webauthn, activate_webauthn_put, delete_webauthn,] +} + +struct WebauthnConfig { + url: String, + rpid: String, +} + +impl WebauthnConfig { + fn load() -> Webauthn { + let domain = CONFIG.domain(); + Webauthn::new(Self { + rpid: reqwest::Url::parse(&domain) + .map(|u| u.domain().map(str::to_owned)) + .ok() + .flatten() + .unwrap_or_default(), + url: domain, + }) + } +} + +impl webauthn_rs::WebauthnConfig for WebauthnConfig { + fn get_relying_party_name(&self) -> &str { + &self.url + } + + fn get_origin(&self) -> &str { + &self.url + } + + fn get_relying_party_id(&self) -> &str { + &self.rpid + } +} + +impl webauthn_rs::WebauthnConfig for &WebauthnConfig { + fn get_relying_party_name(&self) -> &str { + &self.url + } + + fn get_origin(&self) -> &str { + &self.url + } + + fn get_relying_party_id(&self) -> &str { + &self.rpid + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WebauthnRegistration { + pub id: i32, + pub name: String, + pub migrated: bool, + + pub credential: Credential, +} + +impl WebauthnRegistration { + fn to_json(&self) -> Value { + json!({ + "Id": self.id, + "Name": self.name, + "migrated": self.migrated, + }) + } +} + +#[post("/two-factor/get-webauthn", data = "")] +fn get_webauthn(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + if !CONFIG.domain_set() { + err!("`DOMAIN` environment variable is not set. Webauthn disabled") + } + + if !headers.user.check_valid_password(&data.data.MasterPasswordHash) { + err!("Invalid password"); + } + + let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &conn)?; + let registrations_json: Vec = registrations.iter().map(WebauthnRegistration::to_json).collect(); + + Ok(Json(json!({ + "Enabled": enabled, + "Keys": registrations_json, + "Object": "twoFactorWebAuthn" + }))) +} + +#[post("/two-factor/get-webauthn-challenge", data = "")] +fn generate_webauthn_challenge(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + if !headers.user.check_valid_password(&data.data.MasterPasswordHash) { + err!("Invalid password"); + } + + let registrations = get_webauthn_registrations(&headers.user.uuid, &conn)? + .1 + .into_iter() + .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering + .collect(); + + let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options( + headers.user.uuid.as_bytes().to_vec(), + headers.user.email, + headers.user.name, + Some(registrations), + None, + None, + )?; + + let type_ = TwoFactorType::WebauthnRegisterChallenge; + TwoFactor::new(headers.user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&conn)?; + + let mut challenge_value = serde_json::to_value(challenge.public_key)?; + challenge_value["status"] = "ok".into(); + challenge_value["errorMessage"] = "".into(); + Ok(Json(challenge_value)) +} + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +struct EnableWebauthnData { + Id: NumberOrString, // 1..5 + Name: String, + MasterPasswordHash: String, + DeviceResponse: RegisterPublicKeyCredentialCopy, +} + +// This is copied from RegisterPublicKeyCredential to change the Response objects casing +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +struct RegisterPublicKeyCredentialCopy { + pub Id: String, + pub RawId: Base64UrlSafeData, + pub Response: AuthenticatorAttestationResponseRawCopy, + pub Type: String, +} + +// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +pub struct AuthenticatorAttestationResponseRawCopy { + pub AttestationObject: Base64UrlSafeData, + pub ClientDataJson: Base64UrlSafeData, +} + +impl From for RegisterPublicKeyCredential { + fn from(r: RegisterPublicKeyCredentialCopy) -> Self { + Self { + id: r.Id, + raw_id: r.RawId, + response: AuthenticatorAttestationResponseRaw { + attestation_object: r.Response.AttestationObject, + client_data_json: r.Response.ClientDataJson, + }, + type_: r.Type, + } + } +} + +// This is copied from PublicKeyCredential to change the Response objects casing +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +pub struct PublicKeyCredentialCopy { + pub Id: String, + pub RawId: Base64UrlSafeData, + pub Response: AuthenticatorAssertionResponseRawCopy, + pub Extensions: Option, + pub Type: String, +} + +// This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +pub struct AuthenticatorAssertionResponseRawCopy { + pub AuthenticatorData: Base64UrlSafeData, + pub ClientDataJson: Base64UrlSafeData, + pub Signature: Base64UrlSafeData, + pub UserHandle: Option, +} + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case)] +pub struct AuthenticationExtensionsClientOutputsCopy { + #[serde(default)] + pub Appid: bool, +} + +impl From for PublicKeyCredential { + fn from(r: PublicKeyCredentialCopy) -> Self { + Self { + id: r.Id, + raw_id: r.RawId, + response: AuthenticatorAssertionResponseRaw { + authenticator_data: r.Response.AuthenticatorData, + client_data_json: r.Response.ClientDataJson, + signature: r.Response.Signature, + user_handle: r.Response.UserHandle, + }, + extensions: r.Extensions.map(|e| AuthenticationExtensionsClientOutputs { + appid: e.Appid, + }), + type_: r.Type, + } + } +} + +#[post("/two-factor/webauthn", data = "")] +fn activate_webauthn(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + let data: EnableWebauthnData = data.into_inner().data; + let mut user = headers.user; + + if !user.check_valid_password(&data.MasterPasswordHash) { + err!("Invalid password"); + } + + // Retrieve and delete the saved challenge state + let type_ = TwoFactorType::WebauthnRegisterChallenge as i32; + let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { + Some(tf) => { + let state: RegistrationState = serde_json::from_str(&tf.data)?; + tf.delete(&conn)?; + state + } + None => err!("Can't recover challenge"), + }; + + // Verify the credentials with the saved state + let (credential, _data) = + WebauthnConfig::load().register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; + + let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn)?.1; + // TODO: Check for repeated ID's + registrations.push(WebauthnRegistration { + id: data.Id.into_i32()?, + name: data.Name, + migrated: false, + + credential, + }); + + // Save the registrations and return them + TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?).save(&conn)?; + _generate_recover_code(&mut user, &conn); + + let keys_json: Vec = registrations.iter().map(WebauthnRegistration::to_json).collect(); + Ok(Json(json!({ + "Enabled": true, + "Keys": keys_json, + "Object": "twoFactorU2f" + }))) +} + +#[put("/two-factor/webauthn", data = "")] +fn activate_webauthn_put(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + activate_webauthn(data, headers, conn) +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct DeleteU2FData { + Id: NumberOrString, + MasterPasswordHash: String, +} + +#[delete("/two-factor/webauthn", data = "")] +fn delete_webauthn(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + let id = data.data.Id.into_i32()?; + if !headers.user.check_valid_password(&data.data.MasterPasswordHash) { + err!("Invalid password"); + } + + let type_ = TwoFactorType::Webauthn as i32; + let mut tf = match TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn) { + Some(tf) => tf, + None => err!("Webauthn data not found!"), + }; + + let mut data: Vec = serde_json::from_str(&tf.data)?; + + let item_pos = match data.iter().position(|r| r.id != id) { + Some(p) => p, + None => err!("Webauthn entry not found"), + }; + + let removed_item = data.remove(item_pos); + tf.data = serde_json::to_string(&data)?; + tf.save(&conn)?; + drop(tf); + + // If entry is migrated from u2f, delete the u2f entry as well + if let Some(mut u2f) = TwoFactor::find_by_user_and_type(&headers.user.uuid, TwoFactorType::U2f as i32, &conn) { + use crate::api::core::two_factor::u2f::U2FRegistration; + let mut data: Vec = match serde_json::from_str(&u2f.data) { + Ok(d) => d, + Err(_) => err!("Error parsing U2F data"), + }; + + data.retain(|r| r.reg.key_handle != removed_item.credential.cred_id); + let new_data_str = serde_json::to_string(&data)?; + + u2f.data = new_data_str; + u2f.save(&conn)?; + } + + let keys_json: Vec = data.iter().map(WebauthnRegistration::to_json).collect(); + + Ok(Json(json!({ + "Enabled": true, + "Keys": keys_json, + "Object": "twoFactorU2f" + }))) +} + +pub fn get_webauthn_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec), Error> { + let type_ = TwoFactorType::Webauthn as i32; + match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) { + Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)), + None => Ok((false, Vec::new())), // If no data, return empty list + } +} + +pub fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult { + // Load saved credentials + let creds: Vec = + get_webauthn_registrations(user_uuid, conn)?.1.into_iter().map(|r| r.credential).collect(); + + if creds.is_empty() { + err!("No Webauthn devices registered") + } + + // Generate a challenge based on the credentials + let ext = RequestAuthenticationExtensions::builder().appid(format!("{}/app-id.json", &CONFIG.domain())).build(); + let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; + + // Save the challenge state for later validation + TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?) + .save(&conn)?; + + // Return challenge to the clients + Ok(Json(serde_json::to_value(response.public_key)?)) +} + +pub fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult { + let type_ = TwoFactorType::WebauthnLoginChallenge as i32; + let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) { + Some(tf) => { + let state: AuthenticationState = serde_json::from_str(&tf.data)?; + tf.delete(&conn)?; + state + } + None => err!("Can't recover login challenge"), + }; + + let rsp: crate::util::UpCase = serde_json::from_str(response)?; + let rsp: PublicKeyCredential = rsp.data.into(); + + let mut registrations = get_webauthn_registrations(user_uuid, conn)?.1; + + // If the credential we received is migrated from U2F, enable the U2F compatibility + //let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0); + let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?; + + for reg in &mut registrations { + if ®.credential.cred_id == cred_id { + reg.credential.counter = auth_data.counter; + + TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(®istrations)?) + .save(&conn)?; + return Ok(()); + } + } + + err!("Credential not present") +} diff --git a/src/api/identity.rs b/src/api/identity.rs index 31f686c2..9f9dde66 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -240,6 +240,7 @@ fn twofactor_auth( _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, ip, conn)? } Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?, + Some(TwoFactorType::Webauthn) => _tf::webauthn::validate_webauthn_login(user_uuid, twofactor_code, conn)?, Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?, Some(TwoFactorType::Duo) => { _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)? @@ -309,6 +310,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api }); } + Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => { + let request = two_factor::webauthn::generate_webauthn_login(user_uuid, conn)?; + result["TwoFactorProviders2"][provider.to_string()] = request.0; + } + Some(TwoFactorType::Duo) => { let email = match User::find_by_uuid(user_uuid, &conn) { Some(u) => u.email, diff --git a/src/api/mod.rs b/src/api/mod.rs index 7312aeec..dca32f1f 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -51,10 +51,10 @@ impl NumberOrString { } } - fn into_i32(self) -> ApiResult { + fn into_i32(&self) -> ApiResult { use std::num::ParseIntError as PIE; match self { - NumberOrString::Number(n) => Ok(n), + NumberOrString::Number(n) => Ok(*n), NumberOrString::String(s) => { s.parse().map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())) } diff --git a/src/db/mod.rs b/src/db/mod.rs index eca0c956..b52f4661 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -114,7 +114,7 @@ macro_rules! db_run { }; // Different code for each db - ( $conn:ident: $( $($db:ident),+ $body:block )+ ) => { + ( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {{ #[allow(unused)] use diesel::prelude::*; match $conn { $($( @@ -128,7 +128,7 @@ macro_rules! db_run { $body }, )+)+ - } + }} }; // Same for all dbs diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 18073bad..bbfb7529 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -31,11 +31,14 @@ pub enum TwoFactorType { U2f = 4, Remember = 5, OrganizationDuo = 6, + Webauthn = 7, // These are implementation details U2fRegisterChallenge = 1000, U2fLoginChallenge = 1001, EmailVerificationChallenge = 1002, + WebauthnRegisterChallenge = 1003, + WebauthnLoginChallenge = 1004, } /// Local methods @@ -146,4 +149,73 @@ impl TwoFactor { .map_res("Error deleting twofactors") }} } + + pub fn migrate_u2f_to_webauthn(conn: &DbConn) -> EmptyResult { + let u2f_factors = db_run! { conn: { + twofactor::table + .filter(twofactor::atype.eq(TwoFactorType::U2f as i32)) + .load::(conn) + .expect("Error loading twofactor") + .from_db() + }}; + + use crate::api::core::two_factor::u2f::U2FRegistration; + use crate::api::core::two_factor::webauthn::{get_webauthn_registrations, WebauthnRegistration}; + use std::convert::TryInto; + use webauthn_rs::proto::*; + + for mut u2f in u2f_factors { + let mut regs: Vec = serde_json::from_str(&u2f.data)?; + // If there are no registrations or they are migrated (we do the migration in batch so we can consider them all migrated when the first one is) + if regs.is_empty() || regs[0].migrated == Some(true) { + continue; + } + + let (_, mut webauthn_regs) = get_webauthn_registrations(&u2f.user_uuid, &conn)?; + + // If the user already has webauthn registrations saved, don't overwrite them + if !webauthn_regs.is_empty() { + continue; + } + + for reg in &mut regs { + let x: [u8; 32] = reg.reg.pub_key[1..33].try_into().unwrap(); + let y: [u8; 32] = reg.reg.pub_key[33..65].try_into().unwrap(); + + let key = COSEKey { + type_: COSEAlgorithm::ES256, + key: COSEKeyType::EC_EC2(COSEEC2Key { + curve: ECDSACurve::SECP256R1, + x, + y, + }), + }; + + let new_reg = WebauthnRegistration { + id: reg.id, + migrated: true, + name: reg.name.clone(), + credential: Credential { + counter: reg.counter, + verified: false, + cred: key, + cred_id: reg.reg.key_handle.clone(), + registration_policy: UserVerificationPolicy::Discouraged, + }, + }; + + webauthn_regs.push(new_reg); + + reg.migrated = Some(true); + } + + u2f.data = serde_json::to_string(®s)?; + u2f.save(&conn)?; + + TwoFactor::new(u2f.user_uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&webauthn_regs)?) + .save(&conn)?; + } + + Ok(()) + } } diff --git a/src/error.rs b/src/error.rs index 08a499af..e3a9c239 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,19 +39,18 @@ use diesel::ConnectionError as DieselConErr; use diesel_migrations::RunMigrationsError as DieselMigErr; use handlebars::RenderError as HbErr; use jsonwebtoken::errors::Error as JwtErr; +use lettre::address::AddressError as AddrErr; +use lettre::error::Error as LettreErr; +use lettre::transport::smtp::Error as SmtpErr; use regex::Error as RegexErr; use reqwest::Error as ReqErr; use serde_json::{Error as SerdeErr, Value}; use std::io::Error as IoErr; - use std::time::SystemTimeError as TimeErr; use u2f::u2ferror::U2fError as U2fErr; +use webauthn_rs::error::WebauthnError as WebauthnErr; use yubico::yubicoerror::YubicoError as YubiErr; -use lettre::address::AddressError as AddrErr; -use lettre::error::Error as LettreErr; -use lettre::transport::smtp::Error as SmtpErr; - #[derive(Serialize)] pub struct Empty {} @@ -86,6 +85,7 @@ make_error! { DieselConError(DieselConErr): _has_source, _api_error, DieselMigError(DieselMigErr): _has_source, _api_error, + WebauthnError(WebauthnErr): _has_source, _api_error, } impl std::fmt::Debug for Error { diff --git a/src/main.rs b/src/main.rs index 80d2c242..c60d16df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,8 @@ fn main() { let pool = create_db_pool(); schedule_jobs(pool.clone()); + crate::db::models::TwoFactor::migrate_u2f_to_webauthn(&pool.get().unwrap()).unwrap(); + launch_rocket(pool, extra_debug); // Blocks until program termination. }