keyring-manager 0.9.0

Cross-platform library for managing passwords
Documentation
#[cfg(all(target_os = "android", feature = "keyring_manager_android_tests"))]
mod android;
#[cfg(all(target_os = "ios", feature = "keyring_manager_ios_tests"))]
mod ios;

use super::*;

////////////////////////////////////////////////////////////////////
// Common test functions

pub static TEST_APPLICATION: &str = "rust-keyring-test";
pub static TEST_SERVICE: &str = "test.keychain-rs\tio";
pub static TEST_SERVICE2: &str = "test.keychain-rs\tio2";
pub static TEST_USER: &str = "user@keychain-rs.io";
pub static TEST_USER2: &str = "foo@bar\tcom";
pub static TEST_ASCII_PASSWORD: &str = "my_password";
pub static TEST_NON_ASCII_PASSWORD: &str = "大根";

fn do_roundtrip_and_delete(manager: &KeyringManager, value: &str) -> String {
    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.set_value(value))
        .unwrap();
    let out = manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.get_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .unwrap();
    out
}

pub fn exec_test_escaped_password_input(manager: &KeyringManager) {
    let pass = "";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\0";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\t";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\n";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\r";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = " ";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\\n ";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\\n\r ";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = ";";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "$";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "\\";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(out, pass);

    let pass = "*";
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(pass, out);
}

pub fn exec_test_add_ascii_password(manager: &KeyringManager) {
    let pass = TEST_ASCII_PASSWORD;
    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.set_value(pass))
        .unwrap();
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(pass, out);
}

pub fn exec_test_round_trip_ascii_password(manager: &KeyringManager) {
    let pass = TEST_ASCII_PASSWORD;
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(pass, out);
    assert!(manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .is_err());
}

pub fn exec_test_add_non_ascii_password(manager: &KeyringManager) {
    let pass = TEST_NON_ASCII_PASSWORD;
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(pass, out);
}

pub fn exec_test_round_trip_non_ascii_password(manager: &KeyringManager) {
    let pass = TEST_NON_ASCII_PASSWORD;
    let out = do_roundtrip_and_delete(manager, pass);
    assert_eq!(pass, out);
    assert!(manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .is_err());
}

pub fn exec_test_multiple(manager: &KeyringManager) {
    let password_1 = "大根";
    let password_2 = "0xE5A4A7E6A0B9"; // Above in hex string

    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.set_value(password_1))
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.set_value(password_2))
        .unwrap();
    let out1 = manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.get_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .unwrap();
    let out2 = manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.get_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.delete_value())
        .unwrap();

    assert_eq!(password_1, out1);
    assert_eq!(password_2, out2);

    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.set_value(password_1))
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.set_value(password_2))
        .unwrap();
    let out2 = manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.get_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.delete_value())
        .unwrap();
    let out1 = manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.get_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .unwrap();

    assert_eq!(password_1, out1);
    assert_eq!(password_2, out2);
}

pub fn exec_test_list_keys(manager: &KeyringManager) {
    let no_keys: Vec<String> = Vec::new();

    // An unknown service is an empty list, not an error.
    assert_eq!(
        manager.list_keys("service-that-does-not-exist").unwrap(),
        no_keys
    );

    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.set_value("one"))
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.set_value("two"))
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE2, TEST_USER, |kr| kr.set_value("three"))
        .unwrap();

    // Keys come back unescaped: TEST_USER2 holds a tab, which every backend escapes on the way in.
    let keys = manager.list_keys(TEST_SERVICE).unwrap();
    assert!(keys.contains(&TEST_USER.to_owned()), "keys: {:?}", keys);
    assert!(keys.contains(&TEST_USER2.to_owned()), "keys: {:?}", keys);

    // A service only lists its own keys.
    let keys2 = manager.list_keys(TEST_SERVICE2).unwrap();
    assert_eq!(keys2, vec![TEST_USER.to_owned()], "keys2: {:?}", keys2);

    manager
        .with_keyring(TEST_SERVICE, TEST_USER, |kr| kr.delete_value())
        .unwrap();

    let keys = manager.list_keys(TEST_SERVICE).unwrap();
    assert!(!keys.contains(&TEST_USER.to_owned()), "keys: {:?}", keys);
    assert!(keys.contains(&TEST_USER2.to_owned()), "keys: {:?}", keys);

    manager
        .with_keyring(TEST_SERVICE, TEST_USER2, |kr| kr.delete_value())
        .unwrap();
    manager
        .with_keyring(TEST_SERVICE2, TEST_USER, |kr| kr.delete_value())
        .unwrap();

    assert_eq!(manager.list_keys(TEST_SERVICE).unwrap(), no_keys);
    assert_eq!(manager.list_keys(TEST_SERVICE2).unwrap(), no_keys);
}

/////
cfg_if! {
    if #[cfg(all(not(target_os = "android"), not(target_os = "ios"), test))] {
        // Native, non-mobile test harness for PlatformKeyringManager

        use serial_test::serial;
        use directories::ProjectDirs;
        use std::path::PathBuf;
        use std::sync::LazyLock;

        fn get_cache_dir() -> PathBuf {
            if let Some(my_proj_dirs) = ProjectDirs::from("org", "Keyring", "KeyRingTests") {
                PathBuf::from(my_proj_dirs.cache_dir())
            } else {
                PathBuf::from("./")
            }
        }

        static PLATFORM_KEYRING_MANAGER: LazyLock<KeyringManager> =
            LazyLock::new(new_test_platform_keyring_manager);

        static INSECURE_KEYRING_MANAGER: LazyLock<KeyringManager> =
            LazyLock::new(new_test_insecure_keyring_manager);

        cfg_if! {
            if #[cfg(all(target_os = "macos", feature = "macos-specify-keychain"))] {
                // Keep tests out of the login keychain: it pollutes the developer's real keychain,
                // raises permission dialogs, and CI has no unlocked login keychain at all.
                static TEST_KEYCHAIN_DIR: LazyLock<tempfile::TempDir> =
                    LazyLock::new(|| tempfile::tempdir().unwrap());

                fn new_test_platform_keyring_manager() -> KeyringManager {
                    use security_framework::os::macos::keychain::CreateOptions;

                    let path = TEST_KEYCHAIN_DIR.path().join("test.keychain");
                    CreateOptions::new()
                        .password("keyring-manager-test")
                        .create(&path)
                        .unwrap();
                    KeyringManager::new_secure_with_path(TEST_APPLICATION, &path).unwrap()
                }
            } else {
                fn new_test_platform_keyring_manager() -> KeyringManager {
                    KeyringManager::new_secure(TEST_APPLICATION).unwrap()
                }
            }
        }

        fn new_test_insecure_keyring_manager() -> KeyringManager {
            KeyringManager::new_insecure(
                TEST_APPLICATION,
                &get_cache_dir().join("test_insecure_keyring"),
            )
            .unwrap()
        }

        cfg_if! {
            if #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] {
                #[test]
                #[serial]
                fn test_platform_escaped_password_input() {
                    exec_test_escaped_password_input(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_add_ascii_password() {
                    exec_test_add_ascii_password(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_round_trip_ascii_password() {
                    exec_test_round_trip_ascii_password(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_add_non_ascii_password() {
                    exec_test_add_non_ascii_password(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_round_trip_non_ascii_password() {
                    exec_test_round_trip_non_ascii_password(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_multiple() {
                    exec_test_multiple(&PLATFORM_KEYRING_MANAGER);
                }

                #[test]
                #[serial]
                fn test_platform_list_keys() {
                    exec_test_list_keys(&PLATFORM_KEYRING_MANAGER);
                }
            } else {
                #[test]
                #[serial]
                fn test_platform_no_secure() {
                    assert!(KeyringManager::new_secure(TEST_APPLICATION).is_err());
                }
            }
        }

        #[test]
        #[serial]
        fn test_insecure_escaped_password_input() {
            exec_test_escaped_password_input(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_add_ascii_password() {
            exec_test_add_ascii_password(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_round_trip_ascii_password() {
            exec_test_round_trip_ascii_password(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_add_non_ascii_password() {
            exec_test_add_non_ascii_password(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_round_trip_non_ascii_password() {
            exec_test_round_trip_non_ascii_password(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_multiple() {
            exec_test_multiple(&INSECURE_KEYRING_MANAGER);
        }

        #[test]
        #[serial]
        fn test_insecure_list_keys() {
            exec_test_list_keys(&INSECURE_KEYRING_MANAGER);
        }
    }
}