Packages

Elixir bindings to macOS Keychain Services via Security.framework. Store, retrieve, and manage passwords and secrets in the system keychain.

Current section

Files

Jump to
ex_keychain lib ex_keychain.ex
Raw

lib/ex_keychain.ex

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