// file: ks-wallet-demo-scenarios/tests/password_lifecycle.rs // version: 1 //! External ordered password lifecycle scenario. const PASSWORD_A: &str = "ks-wallet-demo-password-A"; const PASSWORD_B: &str = "ks-wallet-demo-password-B"; const CHALLENGE_B: &[u8] = b"ks-wallet-demo-scenarios password-B signing proof"; const CHALLENGE_A_RESTORED: &[u8] = b"ks-wallet-demo-scenarios restored password-A signing proof"; fn make_directory_private(path: &std::path::Path) { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap_or_else( |error| panic!("wallet scenario directory permissions must be set: {error}"), ); } } fn password(value: &str) -> ks_wallet::WalletPassword { return ks_wallet::WalletPassword::new(value.to_owned()) .unwrap_or_else(|error| panic!("scenario password must be accepted: {error}")); } #[tokio::test] async fn password_rotation_runs_a_to_b_rejects_a_then_restores_a() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary scenario directory must exist: {error}")); make_directory_private(directory.path()); let manager = ks_wallet::WalletManager::new(directory.path()) .unwrap_or_else(|error| panic!("wallet manager must be created: {error}")); let alias = ks_wallet::WalletAlias::parse("password-lifecycle") .unwrap_or_else(|error| panic!("scenario alias must be valid: {error}")); let created = manager .create(alias.clone(), password(PASSWORD_A)) .await .unwrap_or_else(|error| panic!("scenario wallet creation must succeed: {error}")); let initial_public_key = created.public_key(); created.lock(); let rotated_to_b = ks_wallet_demo_scenarios::rotate_wallet_password( &manager, &alias, password(PASSWORD_A), password(PASSWORD_B), ) .await .unwrap_or_else(|error| panic!("password A to B rotation must succeed: {error}")); assert_eq!(rotated_to_b.identity.public_key, initial_public_key); let rejected_a = ks_wallet_demo_scenarios::expect_wallet_password_rejected( &manager, &alias, password(PASSWORD_A), ) .await .unwrap_or_else(|error| panic!("old password A must be rejected: {error}")); assert_eq!(rejected_a.identity.public_key, initial_public_key); assert_eq!(rejected_a.rejection_code, "wallet_native_authentication_failed"); let signed_with_b = ks_wallet_demo_scenarios::unlock_and_sign_wallet( &manager, &alias, password(PASSWORD_B), CHALLENGE_B, ) .await .unwrap_or_else(|error| panic!("password B must unlock and sign: {error}")); assert_eq!(signed_with_b.identity.public_key, initial_public_key); assert_eq!(signed_with_b.message_length, CHALLENGE_B.len()); assert!(!signed_with_b.signature.is_empty()); let restored_to_a = ks_wallet_demo_scenarios::rotate_wallet_password( &manager, &alias, password(PASSWORD_B), password(PASSWORD_A), ) .await .unwrap_or_else(|error| panic!("password B to A restoration must succeed: {error}")); assert_eq!(restored_to_a.identity.public_key, initial_public_key); let signed_with_restored_a = ks_wallet_demo_scenarios::unlock_and_sign_wallet( &manager, &alias, password(PASSWORD_A), CHALLENGE_A_RESTORED, ) .await .unwrap_or_else(|error| panic!("restored password A must unlock and sign: {error}")); assert_eq!(signed_with_restored_a.identity.public_key, initial_public_key); assert_eq!(signed_with_restored_a.message_length, CHALLENGE_A_RESTORED.len()); assert!(!signed_with_restored_a.signature.is_empty()); }