Module: Pandoru::SecretStore

Defined in:
lib/pandoru/secret_store.rb

Overview

Portable secret storage: keeps the Pandora credential out of any plaintext file by delegating to the host OS’s native secret service. There’s no solid cross-platform Ruby gem for this, so we shell out to each platform’s tool, the way fastlane et al. do:

macOS   → security        (Keychain)
Linux   → secret-tool      (libsecret / Secret Service: GNOME Keyring, KWallet)
Windows → Credential Manager (not yet wired — see Adapters::Windows)

The credential is stored as a single JSON blob { “username”, “password” } under one fixed key, so retrieval is uniform across backends and we never need to enumerate the store to discover the username. Everything degrades gracefully: with no working backend, fetch returns nil and the resolver falls through to the config file.

Defined Under Namespace

Modules: Adapters

Constant Summary collapse

SERVICE =
'pandoru'
DEFAULT_RUNNER =

Shells out, returning [stdout, success?]. Injectable so adapters can be unit-tested without touching a real keychain.

lambda do |cmd, stdin_data|
  opts = { err: File::NULL }
  opts[:stdin_data] = stdin_data if stdin_data
  out, status = Open3.capture2(*cmd, **opts)
  [out, status.success?]
rescue Errno::ENOENT
  ['', false]
end

Class Method Summary collapse

Class Method Details

.adapter(runner: DEFAULT_RUNNER) ⇒ Object

The first available adapter for this host, or a Null adapter.



36
37
38
39
40
# File 'lib/pandoru/secret_store.rb', line 36

def adapter(runner: DEFAULT_RUNNER)
  [Adapters::MacOS, Adapters::SecretTool, Adapters::Windows]
    .map { |klass| klass.new(runner: runner) }
    .find(&:available?) || Adapters::Null.new(runner: runner)
end

.available?(adapter: adapter()) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/pandoru/secret_store.rb', line 42

def available?(adapter: adapter())
  !adapter.is_a?(Adapters::Null)
end

.backend_name(adapter: adapter()) ⇒ Object



46
47
48
# File 'lib/pandoru/secret_store.rb', line 46

def backend_name(adapter: adapter())
  adapter.name
end

.delete(service: SERVICE, adapter: adapter()) ⇒ Object



69
70
71
# File 'lib/pandoru/secret_store.rb', line 69

def delete(service: SERVICE, adapter: adapter())
  adapter.delete(service)
end

.fetch(service: SERVICE, adapter: adapter()) ⇒ Object

username, password

from the store, or nil if absent/unreadable.



51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/pandoru/secret_store.rb', line 51

def fetch(service: SERVICE, adapter: adapter())
  raw = adapter.read(service)
  return nil if raw.nil? || raw.strip.empty?

  data = JSON.parse(raw)
  username = data['username']
  password = data['password']
  return nil unless present?(username) && present?(password)

  [username, password]
rescue JSON::ParserError
  nil
end

.present?(value) ⇒ Boolean

Returns:

  • (Boolean)


73
74
75
# File 'lib/pandoru/secret_store.rb', line 73

def present?(value)
  !value.nil? && !value.to_s.strip.empty?
end

.store(username, password, service: SERVICE, adapter: adapter()) ⇒ Object



65
66
67
# File 'lib/pandoru/secret_store.rb', line 65

def store(username, password, service: SERVICE, adapter: adapter())
  adapter.write(service, JSON.generate('username' => username, 'password' => password))
end