Keyeio - Secure API Key Storage for Eio Applications#
Keyeio provides secure storage and retrieval of API keys and credentials for Eio-based applications. It uses the XDG Base Directory Specification via the xdge library for consistent, platform-appropriate storage locations.
Features#
- 🔐 Secure Storage: Keys stored in XDG-compliant directories with strict file permissions (0o600)
- 📁 Multiple Profiles: Support multiple configurations per service (default, production, staging, etc.)
- 🎯 Type-Safe API: Abstract types and comprehensive accessors following OCaml best practices
- 🖥️ Cmdliner Integration: Easy command-line argument handling matching xdge patterns
- 🔮 Future-Proof: Designed to support Secret Service API (GNOME Keyring, KWallet) integration
Installation#
opam install keyeio
Quick Start#
Basic Usage#
open Cmdliner
let main (xdg, _) profile =
(* Get credentials from profile *)
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
let base_url = Keyeio.Profile.get profile ~key:"base_url"
|> Option.value ~default:"https://api.example.com" in
(* Use credentials with your API client *)
Printf.printf "Connecting to %s\n" base_url;
(* ... *)
let () =
Eio_main.run @@ fun env ->
(* Create Cmdliner terms *)
let xdg_term = Xdge.Cmd.term "myapp" env#fs () in
let key_term = Keyeio.Cmd.term
~app_name:"myapp"
~fs:env#fs
~service:"immiche" (* Service name *)
() in
let cmd = Cmd.v (Cmd.info "myapp")
Term.(const main $ xdg_term $ key_term) in
exit (Cmd.eval cmd)
Storage Format#
Keys are stored in ~/.config/<appname>/keys/<service>.toml:
[default]
api_key = "abc123..."
base_url = "https://api.example.com"
[production]
api_key = "xyz789..."
base_url = "https://api.prod.example.com"
[staging]
api_key = "def456..."
base_url = "https://api.staging.example.com"
Examples#
The library includes comprehensive examples in example/keyeio_example.ml:
# Build the example
dune build keyeio/example/keyeio_example.exe
# List all configured services
./keyeio_example.exe list
# Show profiles for a service
./keyeio_example.exe profiles immiche
# Use default profile
./keyeio_example.exe basic
# Use a specific profile
./keyeio_example.exe basic --profile production
# Simulate API client
./keyeio_example.exe client --profile staging
Example Output#
$ ./keyeio_example.exe profiles immiche
=== List Profiles Example ===
Service: immiche
Available profiles:
- default (keys: api_key, base_url)
- production (keys: api_key, base_url, extra_field)
- staging (keys: api_key, base_url)
Service details:
Service immiche:
default:
Profile immiche.default:
api_key: test_api***
base_url: https://immich.example.com
production:
Profile immiche.production:
api_key: prod_api***
base_url: https://immich.prod.example.com
extra_field: some_value
API Overview#
Creating a Keyeio Context#
val create : Xdge.t -> t
Creates a keyeio context from an XDG context. Keys are stored in keys/ subdirectory of the config directory.
Loading Services#
val load_service : t -> service:string -> (Service.t, [> `Msg of string]) result
Load all profiles for a service from ~/.config/<appname>/keys/<service>.toml.
val list_services : t -> (string list, [> `Msg of string]) result
List all available services (TOML files in the keys directory).
Working with Profiles#
module Profile : sig
val service : t -> string
val name : t -> string
val get : t -> key:string -> string option
val get_required : t -> key:string -> string (* raises Key_not_found *)
val keys : t -> string list
val pp : Format.formatter -> t -> unit
end
Working with Services#
module Service : sig
val name : t -> string
val profile_names : t -> string list
val get_profile : t -> string -> Profile.t option
val default_profile : t -> Profile.t option
val pp : Format.formatter -> t -> unit
end
Cmdliner Integration#
module Cmd : sig
val term :
app_name:string ->
fs:Eio.Fs.dir_ty Eio.Path.t ->
service:string ->
?profile:string -> (* Default: "default" *)
?key_file:bool -> (* Add --key-file flag, default: true *)
unit ->
Profile.t Cmdliner.Term.t
val env_docs : app_name:string -> service:string -> unit -> string
end
The term function generates:
--profile NAME: Select which profile to use--key-file PATH: Override with direct file path (optional)
Security#
Current Security Model#
- Keys stored as TOML files in
~/.config/<appname>/keys/ - Files created with permissions
0o600(owner read/write only) - Sensitive values masked in pretty-printing output
- Follows standard Unix file permission security model
Future Enhancements#
The library is designed to support future integration with system keychains:
- Linux: freedesktop.org Secret Service API (GNOME Keyring, KWallet, KeePassXC)
- macOS: Keychain Services API
- Windows: Credential Manager API
The abstract backend type allows adding these integrations without breaking the API.
Integration Examples#
With Immiche (Immich API Client)#
let main (xdg, _) profile =
Eio_main.run @@ fun env ->
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
let base_url = Keyeio.Profile.get_required profile ~key:"base_url" in
let client = Immiche.create ~sw ~env ~base_url ~api_key () in
(* Use client... *)
let () =
Eio_main.run @@ fun env ->
let xdg_term = Xdge.Cmd.term "myapp" env#fs () in
let key_term = Keyeio.Cmd.term ~app_name:"myapp" ~fs:env#fs ~service:"immiche" () in
(* ... *)
With Karakeepe (Hoarder API Client)#
let key_term = Keyeio.Cmd.term
~app_name:"myapp"
~fs:env#fs
~service:"karakeepe"
() in
let api_key = Keyeio.Profile.get_required profile ~key:"api_key" in
let base_url = Keyeio.Profile.get_required profile ~key:"base_url" in
let bookmarks = Karakeepe.fetch_all_bookmarks ~sw ~env ~api_key base_url in
(* Process bookmarks... *)
Environment Variables#
The storage location respects XDG Base Directory Specification:
XDG_CONFIG_HOME: Base directory for config files (default:~/.config)<APPNAME>_CONFIG_DIR: Application-specific override (highest priority)
Keys are stored in: $XDG_CONFIG_HOME/<appname>/keys/<service>.toml
Documentation#
Full API documentation is available:
dune build @doc
open _build/default/_doc/_html/keyeio/index.html
License#
ISC License