defmodule ExKeychain do @moduledoc """ Elixir bindings to macOS Keychain Services via Security.framework. Provides access to the macOS Keychain for storing and retrieving passwords, certificates, and cryptographic keys. Uses Rustler NIFs backed by the `security-framework` Rust crate. ## Generic Passwords Generic passwords are identified by a service name and account name. This is the most common keychain item type, used for application passwords, API tokens, and similar credentials. # Store a password :ok = ExKeychain.set("my-app", "user@example.com", "s3cret") # Retrieve it {:ok, "s3cret"} = ExKeychain.get("my-app", "user@example.com") # Delete it :ok = ExKeychain.delete("my-app", "user@example.com") ## Internet Passwords Internet passwords include server, protocol, and path information in addition to the account name. Used for website credentials, FTP logins, etc. :ok = ExKeychain.set_internet("example.com", :https, "admin", "p4ss", port: 443, path: "/api") {:ok, "p4ss"} = ExKeychain.get_internet("example.com", :https, "admin") ## Platform Requirements This library only works on macOS. Attempting to load it on other platforms will result in a NIF load error. Some operations (especially those involving access control lists or the system keychain) may require code signing or specific entitlements. """ use Rustler, otp_app: :ex_keychain, crate: "ex_keychain" # ── Generic Passwords ─────────────────────────────────────────────── @doc """ Store a generic password in the default keychain. ## Options * `:label` - Human-readable label shown in Keychain Access.app * `:comment` - Comment string stored with the item ## Examples :ok = ExKeychain.set("my-app", "user@example.com", "s3cret") :ok = ExKeychain.set("my-app", "admin", "hunter2", label: "Admin Login") """ @spec set(String.t(), String.t(), String.t(), keyword()) :: :ok | {:error, String.t()} def set(_service, _account, _password, _opts \\ []), do: :erlang.nif_error(:nif_not_loaded) @doc """ Retrieve a generic password from the keychain. Returns `{:ok, password}` if found, or `{:error, reason}` if not. The most common error is `"The specified item could not be found in the keychain."` ## Examples {:ok, "s3cret"} = ExKeychain.get("my-app", "user@example.com") {:error, _reason} = ExKeychain.get("my-app", "nonexistent") """ @spec get(String.t(), String.t()) :: {:ok, String.t()} | {:error, String.t()} def get(_service, _account), do: :erlang.nif_error(:nif_not_loaded) @doc """ Delete a generic password from the keychain. Returns `:ok` if deleted, or `{:error, reason}` if not found. ## Examples :ok = ExKeychain.delete("my-app", "user@example.com") """ @spec delete(String.t(), String.t()) :: :ok | {:error, String.t()} def delete(_service, _account), do: :erlang.nif_error(:nif_not_loaded) @doc """ Check if a generic password exists without retrieving it. This avoids triggering any access control dialogs that `get/2` might cause. ## Examples true = ExKeychain.exists?("my-app", "user@example.com") false = ExKeychain.exists?("my-app", "nonexistent") """ @spec exists?(String.t(), String.t()) :: boolean() def exists?(_service, _account), do: :erlang.nif_error(:nif_not_loaded) @doc """ Update the password for an existing generic keychain item. The item must already exist. Use `set/4` to create-or-update. ## Examples :ok = ExKeychain.update("my-app", "user@example.com", "new_password") """ @spec update(String.t(), String.t(), String.t()) :: :ok | {:error, String.t()} def update(_service, _account, _new_password), do: :erlang.nif_error(:nif_not_loaded) @doc """ List all accounts stored under a given service name. Returns a list of account name strings. ## Examples ["admin", "user@example.com"] = ExKeychain.list("my-app") [] = ExKeychain.list("unknown-service") """ @spec list(String.t()) :: [String.t()] def list(_service), do: :erlang.nif_error(:nif_not_loaded) # ── Internet Passwords ────────────────────────────────────────────── @doc """ Store an internet password in the default keychain. Protocol should be one of: `:https`, `:http`, `:ftp`, `:ftps`, `:smb`, `:afp`, `:ldap`, `:ldaps`. ## Options * `:port` - Server port number * `:path` - URL path * `:label` - Human-readable label ## Examples :ok = ExKeychain.set_internet("example.com", :https, "admin", "p4ss") :ok = ExKeychain.set_internet("ftp.example.com", :ftp, "deploy", "key", port: 2222, path: "/uploads") """ @spec set_internet(String.t(), atom(), String.t(), String.t(), keyword()) :: :ok | {:error, String.t()} def set_internet(_server, _protocol, _account, _password, _opts \\ []), do: :erlang.nif_error(:nif_not_loaded) @doc """ Retrieve an internet password from the keychain. ## Examples {:ok, "p4ss"} = ExKeychain.get_internet("example.com", :https, "admin") """ @spec get_internet(String.t(), atom(), String.t()) :: {:ok, String.t()} | {:error, String.t()} def get_internet(_server, _protocol, _account), do: :erlang.nif_error(:nif_not_loaded) @doc """ Delete an internet password from the keychain. """ @spec delete_internet(String.t(), atom(), String.t()) :: :ok | {:error, String.t()} def delete_internet(_server, _protocol, _account), do: :erlang.nif_error(:nif_not_loaded) end