diff --git a/Cargo.lock b/Cargo.lock index 00ceccce..914d53cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,7 +33,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -46,7 +46,7 @@ dependencies = [ "cipher", "cpufeatures", "ctr", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -63,12 +63,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" - [[package]] name = "ahash" version = "0.7.6" @@ -354,6 +348,12 @@ dependencies = [ "syn", ] +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.13.0" @@ -406,25 +406,13 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -433,7 +421,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -442,19 +430,10 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" dependencies = [ - "block-padding 0.2.1", + "block-padding", "cipher", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - [[package]] name = "block-padding" version = "0.2.1" @@ -483,7 +462,7 @@ checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab" dependencies = [ "byteorder", "cipher", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -493,10 +472,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] -name = "byte-tools" -version = "0.3.1" +name = "bytemuck" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] name = "byteorder" @@ -534,6 +513,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.19" @@ -554,7 +539,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -623,6 +608,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "concurrent-queue" version = "1.2.2" @@ -639,15 +630,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86" dependencies = [ "async-trait", - "json5", "lazy_static 1.4.0", "nom", "pathdiff", - "ron", - "rust-ini", "serde", - "serde_json", - "toml", "yaml-rust", ] @@ -828,7 +814,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "generic-array 0.14.5", + "generic-array", "typenum", ] @@ -838,7 +824,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.5", + "generic-array", "subtle", ] @@ -941,22 +927,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -990,15 +967,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "dlv-list" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" -dependencies = [ - "rand", -] - [[package]] name = "dotenv" version = "0.15.0" @@ -1032,12 +1000,6 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" version = "1.7.0" @@ -1244,15 +1206,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - [[package]] name = "generic-array" version = "0.14.5" @@ -1280,7 +1233,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" dependencies = [ - "opaque-debug 0.3.0", + "opaque-debug", "polyval", ] @@ -1321,22 +1274,13 @@ dependencies = [ "tracing", ] -[[package]] -name = "hashbrown" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" -dependencies = [ - "ahash 0.4.7", -] - [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash 0.7.6", + "ahash", ] [[package]] @@ -1345,7 +1289,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown 0.11.2", + "hashbrown", ] [[package]] @@ -1374,7 +1318,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.10.0", + "sha-1", ] [[package]] @@ -1556,6 +1500,20 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -1563,7 +1521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" dependencies = [ "autocfg", - "hashbrown 0.11.2", + "hashbrown", ] [[package]] @@ -1631,17 +1589,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1747,12 +1694,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matchers" version = "0.1.0" @@ -1947,6 +1888,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1990,12 +1953,6 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -2045,16 +2002,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" -dependencies = [ - "dlv-list", - "hashbrown 0.9.1", -] - [[package]] name = "os_str_bytes" version = "6.0.0" @@ -2216,49 +2163,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1 0.8.2", -] - [[package]] name = "petgraph" version = "0.6.0" @@ -2442,7 +2346,7 @@ checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "opaque-debug 0.3.0", + "opaque-debug", "universal-hash", ] @@ -2558,6 +2462,16 @@ dependencies = [ "prost", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quote" version = "1.0.15" @@ -2707,17 +2621,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "ron" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678" -dependencies = [ - "base64", - "bitflags", - "serde", -] - [[package]] name = "russh" version = "0.34.0-beta.2" @@ -2729,7 +2632,7 @@ dependencies = [ "digest 0.9.0", "flate2", "futures", - "generic-array 0.14.5", + "generic-array", "log", "openssl", "rand", @@ -2831,16 +2734,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust-ini" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" -dependencies = [ - "cfg-if 1.0.0", - "ordered-multimap", -] - [[package]] name = "rust_decimal" version = "1.22.0" @@ -3147,18 +3040,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.10.0" @@ -3180,7 +3061,7 @@ dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -3282,7 +3163,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "195183bf6ff8328bb82c0511a83faf60aacf75840103388851db61d7a9854ae3" dependencies = [ - "ahash 0.7.6", + "ahash", "atoi", "bitflags", "byteorder", @@ -3688,6 +3569,18 @@ dependencies = [ "syn", ] +[[package]] +name = "totp-rs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8707de57599ceb299004ba63de6733d7eb393116e07d4492dcf11191297e373" +dependencies = [ + "base32", + "hmac 0.12.1", + "sha-1", + "sha2 0.10.2", +] + [[package]] name = "tower" version = "0.4.12" @@ -3806,12 +3699,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "ucd-util" version = "0.1.8" @@ -3872,7 +3759,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" dependencies = [ - "generic-array 0.14.5", + "generic-array", "subtle", ] @@ -3975,6 +3862,7 @@ dependencies = [ name = "warpgate" version = "0.1.0" dependencies = [ + "ansi_term", "anyhow", "async-trait", "bytes", @@ -3982,12 +3870,14 @@ dependencies = [ "config", "console 0.1.0", "console-subscriber", + "data-encoding", "dhat", "dialoguer", "futures", "isatty", "notify", "openssl", + "qrcode", "rcgen", "sd-notify", "serde_yaml", @@ -4038,16 +3928,19 @@ dependencies = [ "chrono", "data-encoding", "humantime-serde", + "lazy_static 1.4.0", "packet", "password-hash 0.3.2", "poem-openapi", "rand", + "rand_chacha", "rand_core", "sea-orm", "serde", "serde_json", "thiserror", "tokio", + "totp-rs", "tracing", "url", "uuid", diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml index e7af4ed9..dd8b9e54 100644 --- a/warpgate-admin/Cargo.toml +++ b/warpgate-admin/Cargo.toml @@ -11,7 +11,7 @@ bytes = "1.1" chrono = "0.4" futures = "0.3" hex = "0.4" -mime_guess = "2.0" +mime_guess = {version = "2.0", default_features = false} poem = {version = "^1.3.24", features = ["cookie", "session", "anyhow", "rustls"]} poem-openapi = {version = "^1.3.24", features = ["swagger-ui", "chrono", "uuid", "static-files"]} russh-keys = {version = "0.22.0-beta.1", features = ["openssl"]} diff --git a/warpgate-admin/src/api/auth.rs b/warpgate-admin/src/api/auth.rs index b7b93d5e..ba35a9af 100644 --- a/warpgate-admin/src/api/auth.rs +++ b/warpgate-admin/src/api/auth.rs @@ -63,6 +63,7 @@ impl Api { Ok(LoginResponse::Failure) } AuthResult::Rejected => Ok(LoginResponse::Failure), + AuthResult::OTPNeeded => Ok(LoginResponse::Failure), // TODO } } diff --git a/warpgate-admin/src/api/tickets_list.rs b/warpgate-admin/src/api/tickets_list.rs index fbffd186..e333e8fe 100644 --- a/warpgate-admin/src/api/tickets_list.rs +++ b/warpgate-admin/src/api/tickets_list.rs @@ -9,7 +9,7 @@ use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; use std::sync::Arc; use tokio::sync::Mutex; use uuid::Uuid; -use warpgate_common::hash::generate_ticket_secret; +use warpgate_common::helpers::hash::generate_ticket_secret; use warpgate_db_entities::Ticket; pub struct Api; diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml index 55b2de30..437c263b 100644 --- a/warpgate-common/Cargo.toml +++ b/warpgate-common/Cargo.toml @@ -12,16 +12,19 @@ bytes = "1.1" chrono = {version = "0.4", features = ["serde"]} data-encoding = "2.3" humantime-serde = "1.1" +lazy_static = "1.4" packet = "0.1" password-hash = "0.3" poem-openapi = {version = "^1.3.24", features = ["swagger-ui", "chrono", "uuid", "static-files"]} rand = "0.8" +rand_chacha = "0.3" rand_core = {version = "0.6", features = ["std"]} sea-orm = {version = "^0.6", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false} serde = "1.0" serde_json = "1.0" thiserror = "1.0" tokio = {version = "1.17", features = ["tracing"]} +totp-rs = "1.0" tracing = "0.1" url = "2.2" uuid = {version = "0.8", features = ["v4", "serde"]} diff --git a/warpgate-common/src/config.rs b/warpgate-common/src/config.rs index 945081d2..cac97867 100644 --- a/warpgate-common/src/config.rs +++ b/warpgate-common/src/config.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::time::Duration; +use crate::helpers::otp::OtpSecretKey; use crate::Secret; const fn _default_true() -> bool { @@ -89,6 +90,11 @@ pub enum UserAuthCredential { Password { hash: Secret }, #[serde(rename = "publickey")] PublicKey { key: Secret }, + #[serde(rename = "otp")] + TOTP { + #[serde(with = "crate::helpers::serde_base64_secret")] + key: OtpSecretKey, + }, } #[derive(Debug, Deserialize, Serialize, Clone)] diff --git a/warpgate-common/src/config_providers/file.rs b/warpgate-common/src/config_providers/file.rs index 1dee5a6c..dbbf9983 100644 --- a/warpgate-common/src/config_providers/file.rs +++ b/warpgate-common/src/config_providers/file.rs @@ -1,5 +1,6 @@ use super::ConfigProvider; -use crate::hash::verify_password_hash; +use crate::helpers::hash::verify_password_hash; +use crate::helpers::otp::verify_totp; use crate::{ AuthCredential, AuthResult, Target, User, UserAuthCredential, UserSnapshot, WarpgateConfig, }; @@ -36,6 +37,7 @@ fn credential_is_type(c: &UserAuthCredential, k: &str) -> bool { match c { UserAuthCredential::Password { .. } => k == "password", UserAuthCredential::PublicKey { .. } => k == "publickey", + UserAuthCredential::TOTP { .. } => k == "otp", } } @@ -92,48 +94,59 @@ impl ConfigProvider for FileConfigProvider { let mut valid_credentials = vec![]; for client_credential in credentials { - if let AuthCredential::PublicKey { - kind, - public_key_bytes, - } = client_credential - { - let mut base64_bytes = BASE64_MIME.encode(public_key_bytes); - base64_bytes.pop(); - base64_bytes.pop(); + match client_credential { + AuthCredential::PublicKey { + kind, + public_key_bytes, + } => { + let mut base64_bytes = BASE64_MIME.encode(public_key_bytes); + base64_bytes.pop(); + base64_bytes.pop(); - let client_key = format!("{} {}", kind, base64_bytes); - debug!(username=%user.username, "Client key: {}", client_key); + let client_key = format!("{} {}", kind, base64_bytes); + debug!(username=%user.username, "Client key: {}", client_key); - for credential in user.credentials.iter() { - if let UserAuthCredential::PublicKey { key: ref user_key } = credential { - if &client_key == user_key.expose_secret() { - valid_credentials.push(credential); - break; - } - } - } - } - } - - for client_credential in credentials { - if let AuthCredential::Password(client_password) = client_credential { - for credential in user.credentials.iter() { - if let UserAuthCredential::Password { - hash: ref user_password_hash, - } = credential - { - match verify_password_hash( - client_password.expose_secret(), - user_password_hash.expose_secret(), - ) { - Ok(true) => { + for credential in user.credentials.iter() { + if let UserAuthCredential::PublicKey { key: ref user_key } = credential { + if &client_key == user_key.expose_secret() { valid_credentials.push(credential); break; } - Ok(false) => continue, - Err(e) => { - error!(username=%user.username, "Error verifying password hash: {}", e); - continue; + } + } + } + AuthCredential::Password(client_password) => { + for credential in user.credentials.iter() { + if let UserAuthCredential::Password { + hash: ref user_password_hash, + } = credential + { + match verify_password_hash( + client_password.expose_secret(), + user_password_hash.expose_secret(), + ) { + Ok(true) => { + valid_credentials.push(credential); + break; + } + Ok(false) => continue, + Err(e) => { + error!(username=%user.username, "Error verifying password hash: {}", e); + continue; + } + } + } + } + } + AuthCredential::OTP(client_otp) => { + for credential in user.credentials.iter() { + if let UserAuthCredential::TOTP { + key: ref user_otp_key, + } = credential + { + if verify_totp(client_otp.expose_secret(), user_otp_key) { + valid_credentials.push(credential); + break; } } } @@ -141,31 +154,38 @@ impl ConfigProvider for FileConfigProvider { } } - if !valid_credentials.is_empty() { - match user.require { - Some(ref required_kinds) => { - for kind in required_kinds { - if !valid_credentials - .iter() - .any(|x| credential_is_type(x, kind)) - { - return Ok(AuthResult::Rejected); - } + if valid_credentials.is_empty() { + warn!(username=%user.username, "Client credentials did not match"); + } + + match user.require { + Some(ref required_kinds) => { + let mut remaining_required_kinds = HashSet::new(); + remaining_required_kinds.extend(required_kinds); + for kind in required_kinds { + if valid_credentials + .iter() + .any(|x| credential_is_type(x, kind)) + { + remaining_required_kinds.remove(kind); } + } + if remaining_required_kinds.is_empty() { return Ok(AuthResult::Accepted { username: user.username.clone(), }); - } - None => { - return Ok(AuthResult::Accepted { - username: user.username.clone(), - }) + } else if remaining_required_kinds.contains(&"otp".to_string()) { + return Ok(AuthResult::OTPNeeded); + } else { + return Ok(AuthResult::Rejected); } } + None => { + return Ok(AuthResult::Accepted { + username: user.username.clone(), + }) + } } - - warn!(username=%user.username, "Client credentials did not match"); - Ok(AuthResult::Rejected) } async fn authorize_target(&mut self, username: &str, target_name: &str) -> Result { diff --git a/warpgate-common/src/config_providers/mod.rs b/warpgate-common/src/config_providers/mod.rs index 18012c7f..726fa160 100644 --- a/warpgate-common/src/config_providers/mod.rs +++ b/warpgate-common/src/config_providers/mod.rs @@ -13,10 +13,12 @@ use warpgate_db_entities::Ticket; pub enum AuthResult { Accepted { username: String }, + OTPNeeded, Rejected, } pub enum AuthCredential { + OTP(Secret), Password(Secret), PublicKey { kind: String, diff --git a/warpgate-common/src/hash.rs b/warpgate-common/src/helpers/hash.rs similarity index 100% rename from warpgate-common/src/hash.rs rename to warpgate-common/src/helpers/hash.rs diff --git a/warpgate-common/src/helpers/mod.rs b/warpgate-common/src/helpers/mod.rs index 952aa130..2f42125d 100644 --- a/warpgate-common/src/helpers/mod.rs +++ b/warpgate-common/src/helpers/mod.rs @@ -1,2 +1,6 @@ pub mod fs; +pub mod hash; +pub mod otp; +pub mod rng; pub mod serde_base64; +pub mod serde_base64_secret; diff --git a/warpgate-common/src/helpers/otp.rs b/warpgate-common/src/helpers/otp.rs new file mode 100644 index 00000000..57f3749a --- /dev/null +++ b/warpgate-common/src/helpers/otp.rs @@ -0,0 +1,31 @@ +use std::time::SystemTime; + +use super::rng::get_crypto_rng; +use crate::types::Secret; +use bytes::Bytes; +use rand::Rng; +use totp_rs::{Algorithm, TOTP}; + +pub type OtpExposedSecretKey = Bytes; +pub type OtpSecretKey = Secret; + +pub fn generate_key() -> OtpSecretKey { + Secret::new(Bytes::from_iter(get_crypto_rng().gen::<[u8; 32]>())) +} + +pub fn generate_setup_url(key: &OtpSecretKey, label: &str) -> Secret { + let totp = get_totp(key); + Secret::new(totp.get_url(label, "Warpgate")) +} + +fn get_totp(key: &OtpSecretKey) -> TOTP { + TOTP::new(Algorithm::SHA1, 6, 1, 30, key.expose_secret().clone()) +} + +pub fn verify_totp(code: &str, key: &OtpSecretKey) -> bool { + let time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + get_totp(key).check(code, time) +} diff --git a/warpgate-common/src/helpers/rng.rs b/warpgate-common/src/helpers/rng.rs new file mode 100644 index 00000000..6e9369a9 --- /dev/null +++ b/warpgate-common/src/helpers/rng.rs @@ -0,0 +1,6 @@ +use rand::SeedableRng; +use rand_chacha::ChaCha20Rng; + +pub fn get_crypto_rng() -> ChaCha20Rng { + ChaCha20Rng::from_entropy() +} diff --git a/warpgate-common/src/helpers/serde_base64.rs b/warpgate-common/src/helpers/serde_base64.rs index 4507da01..38b7c984 100644 --- a/warpgate-common/src/helpers/serde_base64.rs +++ b/warpgate-common/src/helpers/serde_base64.rs @@ -1,18 +1,16 @@ -use bytes::Bytes; use data_encoding::BASE64; use serde::{Deserialize, Serializer}; -pub fn serialize(bytes: &Bytes, serializer: S) -> Result -where - S: Serializer, -{ - serializer.serialize_str(&BASE64.encode(bytes)) +pub fn serialize>( + bytes: B, + serializer: S, +) -> Result { + serializer.serialize_str(&BASE64.encode(bytes.as_ref())) } -pub fn deserialize<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ +pub fn deserialize<'de, D: serde::Deserializer<'de>, B: From>>( + deserializer: D, +) -> Result { let s = String::deserialize(deserializer)?; Ok(BASE64 .decode(s.as_bytes()) diff --git a/warpgate-common/src/helpers/serde_base64_secret.rs b/warpgate-common/src/helpers/serde_base64_secret.rs new file mode 100644 index 00000000..cd1b4beb --- /dev/null +++ b/warpgate-common/src/helpers/serde_base64_secret.rs @@ -0,0 +1,15 @@ +use super::serde_base64; +use crate::Secret; +use bytes::Bytes; +use serde::Serializer; + +pub fn serialize(secret: &Secret, serializer: S) -> Result { + serde_base64::serialize(secret.expose_secret().as_ref(), serializer) +} + +pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + let inner = serde_base64::deserialize(deserializer)?; + Ok(Secret::new(inner)) +} diff --git a/warpgate-common/src/lib.rs b/warpgate-common/src/lib.rs index 3a88ccdc..305f0074 100644 --- a/warpgate-common/src/lib.rs +++ b/warpgate-common/src/lib.rs @@ -6,7 +6,6 @@ pub mod consts; mod data; pub mod db; pub mod eventhub; -pub mod hash; pub mod helpers; mod protocols; pub mod recordings; diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml index 1338f6dd..c13b253b 100644 --- a/warpgate-db-migrations/Cargo.toml +++ b/warpgate-db-migrations/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "warpgate-db-migrations" -version = "0.1.0" edition = "2021" -publish = false license = "Apache-2.0" +name = "warpgate-db-migrations" +publish = false +version = "0.1.0" [lib] [dependencies] -sea-schema = { version = "0.5", default-features = false, features = [ "migration", "debug-print" ] } -uuid = {version = "0.8", features = ["v4", "serde"]} chrono = "0.4" sea-orm = {version = "^0.6", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false} +sea-schema = {version = "0.5", default-features = false, features = ["migration", "debug-print"]} +uuid = {version = "0.8", features = ["v4", "serde"]} diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml index 94ffec5c..8db21fac 100644 --- a/warpgate-protocol-ssh/Cargo.toml +++ b/warpgate-protocol-ssh/Cargo.toml @@ -8,11 +8,12 @@ version = "0.1.0" ansi_term = "0.12" anyhow = "1.0" async-trait = "0.1" +bimap = "0.6" bytes = "1.1" dialoguer = "0.10" futures = "0.3" -russh = {version = "0.34.0-beta.2", features = ["openssl"] } -russh-keys = {version = "0.22.0-beta.1", features = ["openssl"] } +russh = {version = "0.34.0-beta.2", features = ["openssl"]} +russh-keys = {version = "0.22.0-beta.1", features = ["openssl"]} sea-orm = {version = "^0.6", features = ["runtime-tokio-native-tls"], default-features = false} thiserror = "1.0" time = "0.3" @@ -21,4 +22,3 @@ tracing = "0.1" uuid = {version = "0.8", features = ["v4"]} warpgate-common = {version = "*", path = "../warpgate-common"} warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"} -bimap = "0.6" diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs index c4646e2f..26ad244c 100644 --- a/warpgate-protocol-ssh/src/server/mod.rs +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -22,7 +22,7 @@ pub async fn run_server(services: Services, address: SocketAddr) -> Result<()> { let config = services.config.lock().await; russh::server::Config { auth_rejection_time: std::time::Duration::from_secs(1), - methods: MethodSet::PUBLICKEY | MethodSet::PASSWORD, + methods: MethodSet::PUBLICKEY | MethodSet::PASSWORD | MethodSet::KEYBOARD_INTERACTIVE, keys: load_host_keys(&config)?, ..Default::default() } diff --git a/warpgate-protocol-ssh/src/server/russh_handler.rs b/warpgate-protocol-ssh/src/server/russh_handler.rs index 29996351..eedb3d66 100644 --- a/warpgate-protocol-ssh/src/server/russh_handler.rs +++ b/warpgate-protocol-ssh/src/server/russh_handler.rs @@ -141,6 +141,28 @@ impl russh::server::Handler for ServerHandler { .boxed() } + fn auth_keyboard_interactive( + self, + user: &str, + _submethods: &str, + response: Option, + ) -> Self::FutureAuth { + let user = user.to_string(); + let response = response + .and_then(|mut r| r.next()) + .and_then(|b| String::from_utf8(b.to_vec()).ok()); + async move { + let result = self + .session + .lock() + .await + ._auth_keyboard_interactive(Secret::new(user), response.map(Secret::new)) + .await; + Ok((self, result)) + } + .boxed() + } + fn data(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { let data = BytesMut::from(data).freeze(); async move { @@ -344,15 +366,6 @@ impl russh::server::Handler for ServerHandler { // self.finished_auth(Auth::Reject) // } - // fn auth_keyboard_interactive( - // self, - // user: &str, - // submethods: &str, - // response: Option, - // ) -> Self::FutureAuth { - // self.finished_auth(Auth::Reject) - // } - // fn tcpip_forward(self, address: &str, port: u32, session: Session) -> Self::FutureBool { // self.finished_bool(false, session) // } diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 5a04f379..8ba4b4ac 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -14,6 +14,7 @@ use russh::server::Session; use russh::{CryptoVec, Sig}; use russh_keys::key::PublicKey; use russh_keys::PublicKeyBase64; +use std::borrow::Cow; use std::collections::hash_map::Entry::Vacant; use std::collections::HashMap; use std::net::{Ipv4Addr, SocketAddr}; @@ -850,6 +851,7 @@ impl ServerSession { match self.try_auth(&selector).await { Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, Ok(AuthResult::Rejected) => russh::server::Auth::Reject, + Ok(AuthResult::OTPNeeded) => russh::server::Auth::Reject, Err(error) => { error!(session=?self, ?error, "Failed to verify credentials"); russh::server::Auth::Reject @@ -870,6 +872,34 @@ impl ServerSession { match self.try_auth(&selector).await { Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, Ok(AuthResult::Rejected) => russh::server::Auth::Reject, + Ok(AuthResult::OTPNeeded) => russh::server::Auth::Reject, + Err(error) => { + error!(session=?self, ?error, "Failed to verify credentials"); + russh::server::Auth::Reject + } + } + } + + pub async fn _auth_keyboard_interactive( + &mut self, + ssh_username: Secret, + response: Option>, + ) -> russh::server::Auth { + let selector: AuthSelector = ssh_username.expose_secret().into(); + info!(session=?self, "Keyboard-interactive auth as {:?}", selector); + + if let Some(otp) = response { + self.credentials.push(AuthCredential::OTP(otp)); + } + + match self.try_auth(&selector).await { + Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, + Ok(AuthResult::Rejected) => russh::server::Auth::Reject, + Ok(AuthResult::OTPNeeded) => russh::server::Auth::Partial { + name: Cow::Borrowed("OTP"), + instructions: Cow::Borrowed(""), + prompts: Cow::Owned(vec![(Cow::Borrowed("One-time password: "), true)]), + }, Err(error) => { error!(session=?self, ?error, "Failed to verify credentials"); russh::server::Auth::Reject @@ -913,6 +943,7 @@ impl ServerSession { Ok(AuthResult::Accepted { username }) } AuthResult::Rejected => Ok(AuthResult::Rejected), + AuthResult::OTPNeeded => Ok(AuthResult::OTPNeeded), } } AuthSelector::Ticket { secret } => { diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml index 15a204f4..110ab322 100644 --- a/warpgate/Cargo.toml +++ b/warpgate/Cargo.toml @@ -5,19 +5,22 @@ name = "warpgate" version = "0.1.0" [dependencies] +ansi_term = "0.12" anyhow = {version = "1.0", features = ["backtrace"]} async-trait = "0.1" bytes = "1.1" clap = {version = "3.1", features = ["derive"]} -config = "0.12" -console = "0.1" +config = {version = "0.12", features = ["yaml"], default_features = false} +console = {version = "0.1", default_features = false} console-subscriber = {version = "0.1", optional = true} +data-encoding = "2.3" dhat = {version = "0.3", optional = true} dialoguer = "0.10" futures = "0.3" isatty = "0.1" notify = "^5.0.0-beta.1" openssl = {version = "0.10", features = ["vendored"]}# Embed OpenSSL +qrcode = "0.12" rcgen = {version = "0.9", features = ["zeroize"]} serde_yaml = "0.8.23" time = "0.3" diff --git a/warpgate/src/commands/hash.rs b/warpgate/src/commands/hash.rs index 48812c6e..82e1fb71 100644 --- a/warpgate/src/commands/hash.rs +++ b/warpgate/src/commands/hash.rs @@ -2,7 +2,7 @@ use anyhow::Result; use dialoguer::theme::ColorfulTheme; use isatty::stdin_isatty; use std::io::stdin; -use warpgate_common::hash::hash_password; +use warpgate_common::helpers::hash::hash_password; pub(crate) async fn command() -> Result<()> { let mut input = String::new(); diff --git a/warpgate/src/commands/mod.rs b/warpgate/src/commands/mod.rs index 5232d4cc..701f846f 100644 --- a/warpgate/src/commands/mod.rs +++ b/warpgate/src/commands/mod.rs @@ -1,6 +1,7 @@ pub mod check; pub mod client_keys; pub mod hash; +pub mod otp; pub mod run; pub mod setup; pub mod test_target; diff --git a/warpgate/src/commands/otp.rs b/warpgate/src/commands/otp.rs new file mode 100644 index 00000000..1337ca70 --- /dev/null +++ b/warpgate/src/commands/otp.rs @@ -0,0 +1,54 @@ +use ansi_term::Color::{Black, White}; +use ansi_term::Style; +use anyhow::Result; +use data_encoding::BASE64; +use qrcode::{Color, QrCode}; +use tracing::*; +use warpgate_common::helpers::otp::{generate_key, generate_setup_url}; + +pub(crate) async fn command() -> Result<()> { + let key = generate_key(); + let url = generate_setup_url(&key, "test"); + + let code = QrCode::new(url.expose_secret().as_bytes())?; + let width = code.width(); + let pixels = code.into_colors(); + + for _ in 0..width + 4 { + print!("{}", Style::new().on(White).paint(" ")); + } + println!(); + + for hy in 0..(pixels.len() + width - 1) / width / 2 + 1 { + print!("{}", Style::new().on(White).paint(" ")); + for x in 0..width { + let top = pixels + .get(hy * 2 * width + x) + .map(|x| *x == Color::Dark) + .unwrap_or(false); + let bottom = pixels + .get((hy * 2 + 1) * width + x) + .map(|x| *x == Color::Dark) + .unwrap_or(false); + + print!( + "{}", + match (top, bottom) { + (true, true) => Style::new().fg(Black).paint("█"), + (true, false) => Style::new().fg(Black).on(White).paint("▀"), + (false, true) => Style::new().fg(Black).on(White).paint("▄"), + (false, false) => Style::new().on(White).paint(" "), + } + ); + } + println!("{}", Style::new().on(White).paint(" ")); + } + + println!(); + info!("Setup URL: {}", url.expose_secret()); + info!("Config file snippet:"); + println!(); + println!(" - type: otp"); + println!(" key: {}", BASE64.encode(key.expose_secret())); + Ok(()) +} diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs index 68c66f2c..8eed7141 100644 --- a/warpgate/src/commands/setup.rs +++ b/warpgate/src/commands/setup.rs @@ -6,7 +6,7 @@ use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::{Path, PathBuf}; use tracing::*; -use warpgate_common::hash::hash_password; +use warpgate_common::helpers::hash::hash_password; use warpgate_common::helpers::fs::{secure_directory, secure_file}; use warpgate_common::{ Role, SSHConfig, Secret, Services, Target, TargetWebAdminOptions, User, UserAuthCredential, diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs index 24f5acd2..cbb2d406 100644 --- a/warpgate/src/main.rs +++ b/warpgate/src/main.rs @@ -37,6 +37,8 @@ enum Commands { Check, /// Test the connection to a target host TestTarget { target_name: String }, + /// Generate a new 2FA (TOTP) enrollment key + GenerateOtp, } async fn _main() -> Result<()> { @@ -51,6 +53,7 @@ async fn _main() -> Result<()> { } Commands::Setup => crate::commands::setup::command(&cli).await, Commands::ClientKeys => crate::commands::client_keys::command(&cli).await, + Commands::GenerateOtp => crate::commands::otp::command().await, } }