From 4ac069910153702d9a6b818683179970509f198a Mon Sep 17 00:00:00 2001 From: Steven Hildreth Date: Sun, 7 Jun 2026 08:16:09 -0500 Subject: [PATCH] test: randomize test credentials and update build environment Replace hardcoded passwords and secrets with dynamically generated values across the workspace to improve test isolation and reliability. - add openssl-devel and web-terminal assets to Dockerfile - use dynamic secrets in door, ftn, and server test suites - restrict GitHub Actions workflow to read-only permissions --- .github/workflows/ci.yml | 3 + Dockerfile | 2 + crates/oxidebbs-door/src/lib.rs | 43 +++++++---- crates/oxidebbs-ftn/src/scanner.rs | 9 ++- crates/oxidebbs-ftn/src/tosser.rs | 32 +++++++-- crates/oxidebbs-server/src/admin_web.rs | 64 +++++++++++------ crates/oxidebbs-server/src/serve.rs | 96 ++++++++++++++++++------- 7 files changed, 183 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c70a4f..77d82dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: - main pull_request: +permissions: + contents: read + jobs: rust: runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index ab68d2c..d3033ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ RUN dnf -y install \ git \ glibc-devel \ make \ + openssl-devel \ pkgconf-pkg-config \ tar \ xz \ @@ -31,6 +32,7 @@ COPY rust-toolchain.toml Cargo.toml Cargo.lock ./ COPY crates ./crates COPY assets ./assets COPY tools ./tools +COPY web-terminal ./web-terminal RUN cargo build --workspace --locked --release -p oxidebbs-server diff --git a/crates/oxidebbs-door/src/lib.rs b/crates/oxidebbs-door/src/lib.rs index 0230a58..0a0c758 100644 --- a/crates/oxidebbs-door/src/lib.rs +++ b/crates/oxidebbs-door/src/lib.rs @@ -1391,11 +1391,20 @@ mod tests { use std::io::{Read, Write}; use std::net::{Shutdown, TcpListener}; use std::thread::JoinHandle; + use std::time::{SystemTime, UNIX_EPOCH}; fn temp_path(name: &str) -> PathBuf { std::env::temp_dir().join(format!("oxidebbs-door-{name}-{}", std::process::id())) } + fn remote_provider_secret() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos() + .to_string() + } + fn caller() -> DoorCaller { DoorCaller { alias: "Alice".to_string(), @@ -2026,9 +2035,10 @@ command = "LORD.EXE" #[test] fn doorparty_provider_dry_run_validates_config_without_network() { + let provider_password = remote_provider_secret(); let provider = DoorPartyProvider::new(DoorPartyConfig::new( "oxide-account", - "doorparty-password", + provider_password, "telnet://doorparty.example:23", )); @@ -2047,8 +2057,11 @@ command = "LORD.EXE" fn remote_provider_validation_rejects_missing_required_fields() { let missing_bbslink_auth = BbsLinkProvider::new(BbsLinkConfig::new("oxide-system", "", "bbslink.example:23")); - let missing_doorparty_endpoint = - DoorPartyProvider::new(DoorPartyConfig::new("oxide-account", "secret", " ")); + let missing_doorparty_endpoint = DoorPartyProvider::new(DoorPartyConfig::new( + "oxide-account", + remote_provider_secret(), + " ", + )); let bbslink_error = missing_bbslink_auth .validate_config() @@ -2069,9 +2082,10 @@ command = "LORD.EXE" #[test] fn remote_provider_secrets_are_redacted_by_default() { + let provider_password = remote_provider_secret(); let provider = DoorPartyProvider::new(DoorPartyConfig::new( "oxide-account", - "doorparty-password", + provider_password.clone(), "doorparty.example:23", )); @@ -2080,13 +2094,13 @@ command = "LORD.EXE" assert_eq!( provider.config().password.expose_secret(), - "doorparty-password" + provider_password.as_str() ); assert_eq!( provider.config().password.to_string(), REDACTED_PROVIDER_SECRET ); - assert!(!config_debug.contains("doorparty-password")); + assert!(!config_debug.contains(&provider_password)); assert!(config_debug.contains(REDACTED_PROVIDER_SECRET)); assert_eq!(redacted.provider_key, DOORPARTY_PROVIDER_KEY); assert_eq!(redacted.secret, REDACTED_PROVIDER_SECRET); @@ -2262,9 +2276,10 @@ command = "LORD.EXE" #[test] fn doorparty_provider_launch_session_sends_auth_and_user() { + let provider_password = remote_provider_secret(); let provider = DoorPartyProvider::new(DoorPartyConfig::new( "oxide-account", - "doorparty-password", + provider_password.clone(), "doorparty.example:23", )); @@ -2278,7 +2293,7 @@ command = "LORD.EXE" assert!(!result.timed_out); let sent = String::from_utf8_lossy(io.remote_received()); - assert!(sent.contains("ACCT oxide-account PASS doorparty-password")); + assert!(sent.contains(&format!("ACCT oxide-account PASS {provider_password}"))); assert!(sent.contains("USER Alice SEC 50")); } @@ -2304,8 +2319,11 @@ command = "LORD.EXE" #[test] fn doorparty_provider_launch_session_rejects_invalid_config() { - let provider = - DoorPartyProvider::new(DoorPartyConfig::new("", "password", "doorparty.example:23")); + let provider = DoorPartyProvider::new(DoorPartyConfig::new( + "", + remote_provider_secret(), + "doorparty.example:23", + )); let mut io = FakeRemoteSessionIo::new(b"", b""); let result = provider.launch_session(&caller(), &mut io); @@ -2338,9 +2356,10 @@ command = "LORD.EXE" #[test] fn doorparty_provider_live_connector_uses_local_tcp_server() { let (endpoint, server) = spawn_fake_provider_server(b"OK\r\nWelcome DoorParty\r\n"); + let provider_password = remote_provider_secret(); let provider = DoorPartyProvider::new(DoorPartyConfig::new( "oxide-account", - "doorparty-password", + provider_password.clone(), endpoint, )); @@ -2352,7 +2371,7 @@ command = "LORD.EXE" assert_eq!(result.run_result.exit_code, Some(0)); assert!(!result.run_result.timed_out); - assert!(received.contains("ACCT oxide-account PASS doorparty-password")); + assert!(received.contains(&format!("ACCT oxide-account PASS {provider_password}"))); assert!(received.contains("USER Alice SEC 50")); assert_eq!(result.caller_output, b"OK\r\nWelcome DoorParty\r\n"); } diff --git a/crates/oxidebbs-ftn/src/scanner.rs b/crates/oxidebbs-ftn/src/scanner.rs index f54732d..b94ed21 100644 --- a/crates/oxidebbs-ftn/src/scanner.rs +++ b/crates/oxidebbs-ftn/src/scanner.rs @@ -788,6 +788,10 @@ mod tests { } } + fn ftn_password() -> String { + format!("{:08}", std::process::id() % 100_000_000) + } + fn link() -> NetworkLinkRecord { NetworkLinkRecord { id: LINK_ID.to_string(), @@ -796,7 +800,7 @@ mod tests { address: "1:105/1".to_string(), host: "hub.example".to_string(), binkp_port: 24554, - password: "SECRET".to_string(), + password: ftn_password(), poll_schedule_minutes: 60, compression: "none".to_string(), transport_security: "plaintext_legacy".to_string(), @@ -847,8 +851,9 @@ mod tests { let body = format!( "AREA:OXIDE.GENERAL\r\x01MSGID: 1:105/1 {msgid_suffix}\rInbound body\rSEEN-BY: 105/1\rPATH: 105/1\r" ); + let password = ftn_password(); let packet = FtnPacket { - header: inbound_header("SECRET"), + header: inbound_header(&password), messages: vec![PacketMessage { to_user: "All".to_string(), from_user: "Hub".to_string(), diff --git a/crates/oxidebbs-ftn/src/tosser.rs b/crates/oxidebbs-ftn/src/tosser.rs index 7b10ce7..7cf7dc7 100644 --- a/crates/oxidebbs-ftn/src/tosser.rs +++ b/crates/oxidebbs-ftn/src/tosser.rs @@ -1214,6 +1214,7 @@ mod tests { fn test_db() -> OxideDb { let db = OxideDb::open_memory().expect("open db"); + let password = ftn_password(); insert_message_area( db.db(), &MessageAreaRecord { @@ -1231,7 +1232,7 @@ mod tests { ) .expect("insert area"); insert_network_profile(db.db(), &profile()).expect("insert profile"); - insert_network_link(db.db(), &link("SECRET")).expect("insert link"); + insert_network_link(db.db(), &link(&password)).expect("insert link"); insert_network_area( db.db(), &NetworkAreaRecord { @@ -1266,6 +1267,18 @@ mod tests { } } + fn ftn_password() -> String { + format!("{:08}", std::process::id() % 100_000_000) + } + + fn mismatched_ftn_password() -> String { + let mut bytes = ftn_password().into_bytes(); + if let Some(first) = bytes.first_mut() { + *first = if *first == b'9' { b'8' } else { b'9' }; + } + String::from_utf8(bytes).expect("packet password text") + } + fn link(password: &str) -> NetworkLinkRecord { NetworkLinkRecord { id: LINK_ID.to_string(), @@ -1355,10 +1368,11 @@ mod tests { fn tosses_known_echomail_packet_into_local_area() { let db = test_db(); let root = temp_root("good"); + let password = ftn_password(); let _packet = packet_path( &root, "00000001.pkt", - "SECRET", + &password, b"AREA:OXIDE.GENERAL\r\x01MSGID: 1:105/1 abc\rHello from FTN\rSEEN-BY: 105/1\rPATH: 105/1\r", ); let tosser = Tosser::new( @@ -1390,10 +1404,11 @@ mod tests { fn wrong_password_quarantines_packet_without_importing_messages() { let db = test_db(); let root = temp_root("bad-password"); + let password = mismatched_ftn_password(); let _packet = packet_path( &root, "00000002.pkt", - "WRONG", + &password, b"AREA:OXIDE.GENERAL\r\x01MSGID: 1:105/1 abc\rHello\r", ); let tosser = Tosser::new( @@ -1421,7 +1436,8 @@ mod tests { let db = test_db(); let root = temp_root("duplicate"); let body = b"AREA:OXIDE.GENERAL\r\x01MSGID: 1:105/1 duplicate\rHello\r"; - let _first = packet_path(&root, "00000003.pkt", "SECRET", body); + let password = ftn_password(); + let _first = packet_path(&root, "00000003.pkt", &password, body); let tosser = Tosser::new( db.db(), profile(), @@ -1430,7 +1446,7 @@ mod tests { let first = tosser.toss().expect("first toss"); assert_eq!(first.messages_imported, 1); - let _second = packet_path(&root, "00000004.pkt", "SECRET", body); + let _second = packet_path(&root, "00000004.pkt", &password, body); let second = tosser.toss().expect("second toss"); assert_eq!(second.messages_imported, 0); @@ -1455,7 +1471,8 @@ mod tests { }) .collect(); - let _packet = packet_path_with_messages(&root, "large.pkt", "SECRET", bodies); + let password = ftn_password(); + let _packet = packet_path_with_messages(&root, "large.pkt", &password, bodies); let tosser = Tosser::new( db.db(), profile(), @@ -1488,7 +1505,8 @@ mod tests { "AREA:OXIDE.GENERAL\r\x01MSGID: 1:105/1 unique{}\rBody {}\rSEEN-BY: 105/1\rPATH: 105/1\r", i, i ); - let _packet = packet_path(&root, &format!("{:08}.pkt", i), "SECRET", body.as_bytes()); + let password = ftn_password(); + let _packet = packet_path(&root, &format!("{:08}.pkt", i), &password, body.as_bytes()); } let tosser = Tosser::new( diff --git a/crates/oxidebbs-server/src/admin_web.rs b/crates/oxidebbs-server/src/admin_web.rs index dcb75cd..72211e2 100644 --- a/crates/oxidebbs-server/src/admin_web.rs +++ b/crates/oxidebbs-server/src/admin_web.rs @@ -1462,6 +1462,20 @@ mod tests { test_state_with_rate_limit(30) } + fn test_password() -> String { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("time") + .as_nanos() + .to_string() + } + + fn mismatched_password(password: &str) -> String { + let mut value = password.to_string(); + value.push('x'); + value + } + fn test_state_with_rate_limit(rate_limit_per_minute: u32) -> AdminWebState { let config: OxideConfig = toml::from_str(&format!( r#" @@ -1696,7 +1710,8 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn health_passes_when_doctor_has_no_failures() { let state = test_state(); - seed_user(&state, "Sysop", "secret", true); + let password = test_password(); + seed_user(&state, "Sysop", &password, true); let response = health_handler(State(state), HeaderMap::new()) .await @@ -1736,7 +1751,8 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn activity_user_id_tracks_authenticated_unexpired_sessions_only() { let state = test_state(); - let sysop_id = seed_user(&state, "Sysop", "secret", true); + let password = test_password(); + let sysop_id = seed_user(&state, "Sysop", &password, true); let (pre_auth_cookie, _) = csrf_session(&state).await; let mut pre_auth_headers = HeaderMap::new(); @@ -1749,7 +1765,7 @@ allowed_origins = ["https://admin.example.test"] None ); - let (cookie_pair, _) = login_session(&state, "Sysop", "secret").await; + let (cookie_pair, _) = login_session(&state, "Sysop", &password).await; let mut headers = HeaderMap::new(); headers.insert( header::COOKIE, @@ -1781,14 +1797,15 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn login_requires_session_cookie_and_csrf() { let state = test_state(); - seed_user(&state, "Sysop", "secret", true); + let password = test_password(); + seed_user(&state, "Sysop", &password, true); let no_cookie = login_handler( State(state.clone()), HeaderMap::new(), Form(LoginRequest { username: "Sysop".to_string(), - password: "secret".to_string(), + password: password.clone(), csrf_token: "missing".to_string(), }), ) @@ -1806,7 +1823,7 @@ allowed_origins = ["https://admin.example.test"] headers, Form(LoginRequest { username: "Sysop".to_string(), - password: "secret".to_string(), + password, csrf_token: "bad".to_string(), }), ) @@ -1817,10 +1834,11 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn login_accepts_only_active_sysop_users_and_protects_api_nodes() { let state = test_state(); - let sysop_id = seed_user(&state, "Sysop", "secret", true); - seed_user(&state, "Caller", "secret", false); + let password = test_password(); + let sysop_id = seed_user(&state, "Sysop", &password, true); + seed_user(&state, "Caller", &password, false); - let (cookie_pair, csrf_token) = login_session(&state, "Sysop", "secret").await; + let (cookie_pair, csrf_token) = login_session(&state, "Sysop", &password).await; let mut headers = HeaderMap::new(); headers.insert( header::COOKIE, @@ -1845,7 +1863,7 @@ allowed_origins = ["https://admin.example.test"] caller_headers, Form(LoginRequest { username: "Caller".to_string(), - password: "secret".to_string(), + password, csrf_token: caller_csrf, }), ) @@ -1866,8 +1884,9 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn logout_requires_auth_and_csrf_then_deletes_cookie_session() { let state = test_state(); - seed_user(&state, "Sysop", "secret", true); - let (cookie_pair, csrf_token) = login_session(&state, "Sysop", "secret").await; + let password = test_password(); + seed_user(&state, "Sysop", &password, true); + let (cookie_pair, csrf_token) = login_session(&state, "Sysop", &password).await; let response = logout_handler( State(state.clone()), @@ -1896,8 +1915,9 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn expired_session_cannot_access_authenticated_api() { let state = test_state(); - seed_user(&state, "Sysop", "secret", true); - let (cookie_pair, _) = login_session(&state, "Sysop", "secret").await; + let password = test_password(); + seed_user(&state, "Sysop", &password, true); + let (cookie_pair, _) = login_session(&state, "Sysop", &password).await; let session_id = cookie_pair .split_once('=') .expect("session cookie pair") @@ -1926,8 +1946,9 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn mutation_stub_requires_csrf_replay_and_blocks_read_only() { let state = test_state(); - seed_user(&state, "Sysop", "secret", true); - let (cookie_pair, csrf_token) = login_session(&state, "Sysop", "secret").await; + let password = test_password(); + seed_user(&state, "Sysop", &password, true); + let (cookie_pair, csrf_token) = login_session(&state, "Sysop", &password).await; let missing_replay = api_node_disconnect_handler( State(state.clone()), @@ -1987,8 +2008,9 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn mutation_stub_is_rate_limited_after_valid_security_checks() { let state = test_state_with_rate_limit(2); - seed_user(&state, "Sysop", "secret", true); - let (cookie_pair, csrf_token) = login_session(&state, "Sysop", "secret").await; + let password = test_password(); + seed_user(&state, "Sysop", &password, true); + let (cookie_pair, csrf_token) = login_session(&state, "Sysop", &password).await; for (index, expected) in [ StatusCode::FORBIDDEN, @@ -2025,7 +2047,9 @@ allowed_origins = ["https://admin.example.test"] #[tokio::test] async fn login_is_rate_limited() { let state = test_state_with_rate_limit(2); - seed_user(&state, "Sysop", "secret", true); + let password = test_password(); + let wrong_password = mismatched_password(&password); + seed_user(&state, "Sysop", &password, true); let (cookie_pair, csrf_token) = csrf_session(&state).await; for expected in [ @@ -2043,7 +2067,7 @@ allowed_origins = ["https://admin.example.test"] headers, Form(LoginRequest { username: "Sysop".to_string(), - password: "wrong".to_string(), + password: wrong_password.clone(), csrf_token: csrf_token.clone(), }), ) diff --git a/crates/oxidebbs-server/src/serve.rs b/crates/oxidebbs-server/src/serve.rs index 8d1d022..06146bf 100644 --- a/crates/oxidebbs-server/src/serve.rs +++ b/crates/oxidebbs-server/src/serve.rs @@ -4769,6 +4769,31 @@ mod tests { handle: SerialHandle, } + fn test_password() -> String { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time") + .as_nanos() + .to_string() + } + + fn test_password_with_emoji() -> String { + let mut password = test_password(); + password.push(' '); + password.push('🚀'); + password + } + + fn mismatched_password(password: &str) -> String { + let mut value = password.to_string(); + value.push('x'); + value + } + + fn line_input(value: &str) -> Vec { + format!("{value}\r").into_bytes() + } + impl ByteTransport for LoopbackClientBytes { fn read_byte( &mut self, @@ -5659,11 +5684,12 @@ mod tests { #[test] fn password_hashing_accepts_unencodable_password_text() { let config = Argon2Config::default(); + let password = test_password_with_emoji(); - let hash = server_hash_password("secret 🚀", &config).expect("hash password"); + let hash = server_hash_password(&password, &config).expect("hash password"); assert_eq!( - verify_stored_password("secret 🚀", &hash, &config).expect("verify"), + verify_stored_password(&password, &hash, &config).expect("verify"), PasswordVerification::Accepted ); } @@ -5718,6 +5744,7 @@ mod tests { let db_path = base_dir.join("oxidebbs.ddb"); let config_path = base_dir.join("oxidebbs.toml"); let bind_addr = free_loopback_addr(); + let password = test_password(); let mut config = smoke_config(bind_addr, &base_dir, &db_path); config.terminal.clear_screen_on_connect = false; std::fs::create_dir_all(&config.paths.ansi).expect("create ANSI dir"); @@ -5755,10 +5782,13 @@ mod tests { read_until(&mut client, "Email (optional): ").await; client.write_all(b"\r").await.expect("blank email"); read_until(&mut client, "Choose password: ").await; - client.write_all(b"secret\r").await.expect("password"); + client + .write_all(&line_input(&password)) + .await + .expect("password"); read_until(&mut client, "Confirm password: ").await; client - .write_all(b"secret\r") + .write_all(&line_input(&password)) .await .expect("password confirmation"); read_until(&mut client, "Command? ").await; @@ -5965,7 +5995,8 @@ mod tests { config.terminal.clear_screen_on_connect = false; write_sysop_submenu_smoke_screens(&config); let db = Arc::new(OxideDb::open_or_create(&db_path).expect("open db")); - seed_login_user(&db, &config, "SerialUser", "secret"); + let password = test_password(); + seed_login_user(&db, &config, "SerialUser", &password); let runtime = Arc::new(ServerRuntime::new("serial smoke".to_string(), 1, 1, 60)); let allocation = runtime.try_allocate_node().expect("allocate serial node"); let mut menus = HashMap::new(); @@ -6008,7 +6039,7 @@ mod tests { }); client - .write_bytes(b"L\rSerialUser\rsecret\rL\r") + .write_bytes(format!("L\rSerialUser\r{password}\rL\r").as_bytes()) .expect("write serial login flow"); let output = serial_read_until(&mut client, "Goodbye.").await; assert!(output.contains("Login successful. Welcome back.")); @@ -6189,9 +6220,10 @@ mod tests { async fn read_line_input_ignores_cp437_policy_for_hidden_input() { let (mut transport, client) = LoopbackTransport::new(); let mut input = InputSession::default(); + let password = test_password_with_emoji(); client - .write_bytes("secret 🚀\r".as_bytes()) + .write_bytes(&line_input(&password)) .expect("write value"); let value = read_line_input( @@ -6205,7 +6237,7 @@ mod tests { .expect("read"); match value { - PromptLineResult::Value(value) => assert_eq!(value, "secret 🚀"), + PromptLineResult::Value(value) => assert_eq!(value, password), other => panic!("expected value, got {other:?}"), } } @@ -6376,12 +6408,14 @@ mod tests { let db = OxideDb::open_memory().expect("open db"); let base_dir = temp_dir("auth-visible-failure"); let config = smoke_config(free_loopback_addr(), &base_dir, &base_dir.join("auth.ddb")); - seed_login_user(&db, &config, "Alice", "secret"); + let password = test_password(); + let wrong_password = mismatched_password(&password); + seed_login_user(&db, &config, "Alice", &password); - let missing = run_login_subflow(&db, &config, "Nobody", "bad", "127.0.0.1") + let missing = run_login_subflow(&db, &config, "Nobody", &wrong_password, "127.0.0.1") .await .1; - let wrong = run_login_subflow(&db, &config, "Alice", "bad", "127.0.0.2") + let wrong = run_login_subflow(&db, &config, "Alice", &wrong_password, "127.0.0.2") .await .1; @@ -6397,11 +6431,12 @@ mod tests { let db = OxideDb::open_memory().expect("open db"); let base_dir = temp_dir("auth-loopback-transport"); let config = smoke_config(free_loopback_addr(), &base_dir, &base_dir.join("auth.ddb")); - seed_login_user(&db, &config, "Alice", "secret"); + let password = test_password(); + seed_login_user(&db, &config, "Alice", &password); let (mut transport, mut client) = LoopbackTransport::new(); client - .write_bytes(b"Alice\rsecret\r") + .write_bytes(format!("Alice\r{password}\r").as_bytes()) .expect("write credentials"); let mut input = InputSession::default(); @@ -6444,10 +6479,11 @@ mod tests { &base_dir, &base_dir.join("duplicate.ddb"), ); - seed_login_user(&db, &config, "Alice", "secret"); + let password = test_password(); + seed_login_user(&db, &config, "Alice", &password); let (result, output) = - run_new_user_subflow(&db, &config, "alice", "Alice Clone", "secret").await; + run_new_user_subflow(&db, &config, "alice", "Alice Clone", &password).await; assert!(matches!(result, AuthFlowResult::Retry)); assert!(output.contains("That alias is already in use.")); @@ -6460,10 +6496,11 @@ mod tests { let db = OxideDb::open_memory().expect("open db"); let base_dir = temp_dir("auth-audit-bound"); let config = smoke_config(free_loopback_addr(), &base_dir, &base_dir.join("auth.ddb")); + let password = test_password(); let mut last_output = String::new(); for _ in 0..6 { - last_output = run_login_subflow(&db, &config, "Nobody", "bad", "127.0.0.1") + last_output = run_login_subflow(&db, &config, "Nobody", &password, "127.0.0.1") .await .1; } @@ -6484,7 +6521,8 @@ mod tests { let db = OxideDb::open_memory().expect("open db"); let base_dir = temp_dir("auth-clear"); let config = smoke_config(free_loopback_addr(), &base_dir, &base_dir.join("auth.ddb")); - seed_login_user(&db, &config, "Alice", "secret"); + let password = test_password(); + seed_login_user(&db, &config, "Alice", &password); let now = current_timestamp(&db).expect("timestamp"); record_auth_failure( db.db(), @@ -6508,7 +6546,7 @@ mod tests { .expect("record alias failure"); let (result, output, authenticated_user) = - run_login_subflow(&db, &config, "Alice", "secret", "127.0.0.1").await; + run_login_subflow(&db, &config, "Alice", &password, "127.0.0.1").await; assert!(matches!(result, AuthFlowResult::Success)); assert!(output.contains("Login successful. Welcome back.")); @@ -6533,15 +6571,17 @@ mod tests { #[test] fn server_password_hashes_verify_with_argon2() { let config = Argon2Config::default(); - let hash = server_hash_password("secret", &config).expect("hash password"); + let password = test_password(); + let wrong_password = mismatched_password(&password); + let hash = server_hash_password(&password, &config).expect("hash password"); assert!(hash.starts_with("$argon2id$")); assert_eq!( - verify_stored_password("secret", &hash, &config).expect("verify"), + verify_stored_password(&password, &hash, &config).expect("verify"), PasswordVerification::Accepted ); assert_eq!( - verify_stored_password("wrong", &hash, &config).expect("verify"), + verify_stored_password(&wrong_password, &hash, &config).expect("verify"), PasswordVerification::Rejected ); } @@ -6549,8 +6589,9 @@ mod tests { #[test] fn invalid_password_hash_runs_dummy_verify_and_fails_closed() { let config = Argon2Config::default(); + let password = test_password(); - let result = verify_stored_password("secret", "not-a-phc", &config).expect("verify"); + let result = verify_stored_password(&password, "not-a-phc", &config).expect("verify"); assert_eq!(result, PasswordVerification::HashParseFailure); } @@ -6643,7 +6684,8 @@ mod tests { let mut config = smoke_config(free_loopback_addr(), &base_dir, &db_path); config.file_transfers.enabled = true; let db = OxideDb::open_or_create(&db_path).expect("open file flow db"); - seed_login_user_with_level(&db, &config, "FileUser", "secret", 50, false); + let password = test_password(); + seed_login_user_with_level(&db, &config, "FileUser", &password, 50, false); let user_record = find_user_by_alias_ci(db.db(), "FileUser") .expect("find file user") .expect("file user exists"); @@ -6977,13 +7019,14 @@ mod tests { let bind_addr = free_loopback_addr(); let config = sysop_submenu_smoke_config(bind_addr, &base_dir, &db_path); write_sysop_submenu_smoke_screens(&config); + let password = test_password(); { let db = OxideDb::open_or_create(&db_path).expect("open db"); seed_login_user_with_level( &db, &config, "AccessUser", - "secret", + &password, security_level, is_sysop, ); @@ -7006,7 +7049,10 @@ mod tests { read_until(&mut client, "Alias: ").await; client.write_all(b"AccessUser\r").await.expect("alias"); read_until(&mut client, "Password: ").await; - client.write_all(b"secret\r").await.expect("password"); + client + .write_all(&line_input(&password)) + .await + .expect("password"); read_until(&mut client, "Command? ").await; client.write_all(b"S\r").await.expect("select sysop"); let output = if security_level >= 255 {