Module: LocalVault::SessionCache
- Defined in:
- lib/localvault/session_cache.rb
Overview
Caches derived master keys to avoid re-prompting passphrase on every command.
On macOS, uses the system Keychain (service “localvault”, account = vault name). Falls back to file-based cache at ~/.localvault/.sessions/ (mode 0600) when Keychain is unavailable (CI, sandboxed, Linux).
Entries expire after DEFAULT_TTL_HOURS (8 hours). Expired entries are cleaned up on read.
Stored payload format: “<base64_key>|<expiry_unix_ts>”
Constant Summary collapse
- DEFAULT_TTL_HOURS =
8- KEYCHAIN_SERVICE =
"localvault"
Class Method Summary collapse
-
.clear(vault_name) ⇒ void
Remove the cached master key for a single vault.
-
.clear_all ⇒ void
Remove cached master keys for all known vaults.
-
.get(vault_name) ⇒ String?
Retrieve a cached master key for the given vault.
- .keychain_delete(vault_name) ⇒ Object
- .keychain_get(vault_name) ⇒ Object
- .keychain_set(vault_name, payload) ⇒ Object
- .macos? ⇒ Boolean
- .session_file(vault_name) ⇒ Object
- .sessions_dir ⇒ Object
-
.set(vault_name, master_key, ttl_hours: DEFAULT_TTL_HOURS) ⇒ void
Cache a master key for the given vault with a time-to-live.
Class Method Details
.clear(vault_name) ⇒ void
This method returns an undefined value.
Remove the cached master key for a single vault.
66 67 68 |
# File 'lib/localvault/session_cache.rb', line 66 def self.clear(vault_name) keychain_delete(vault_name) end |
.clear_all ⇒ void
This method returns an undefined value.
Remove cached master keys for all known vaults.
73 74 75 |
# File 'lib/localvault/session_cache.rb', line 73 def self.clear_all Store.list_vaults.each { |name| clear(name) } end |
.get(vault_name) ⇒ String?
Retrieve a cached master key for the given vault.
Returns nil if no entry exists or the entry has expired. Expired entries are automatically cleaned up.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/localvault/session_cache.rb', line 32 def self.get(vault_name) payload = keychain_get(vault_name) return nil unless payload key_b64, expiry_str = payload.split("|", 2) return nil unless key_b64 && expiry_str expiry = expiry_str.to_i if Time.now.to_i >= expiry clear(vault_name) # clean up expired entry return nil end Base64.strict_decode64(key_b64) rescue ArgumentError nil end |
.keychain_delete(vault_name) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/localvault/session_cache.rb', line 133 def self.keychain_delete(vault_name) if macos? system( "security", "delete-generic-password", "-a", vault_name, "-s", KEYCHAIN_SERVICE, out: File::NULL, err: File::NULL ) end # Always clean file fallback FileUtils.rm_f(session_file(vault_name)) end |
.keychain_get(vault_name) ⇒ Object
96 97 98 99 100 101 102 103 104 |
# File 'lib/localvault/session_cache.rb', line 96 def self.keychain_get(vault_name) if macos? out = `security find-generic-password -a #{Shellwords.escape(vault_name)} -s #{Shellwords.escape(KEYCHAIN_SERVICE)} -w 2>/dev/null`.chomp return out if $?.success? && !out.empty? end # File fallback (Linux, or macOS when Keychain unavailable) file = session_file(vault_name) File.exist?(file) ? File.read(file).strip : nil end |
.keychain_set(vault_name, payload) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/localvault/session_cache.rb', line 106 def self.keychain_set(vault_name, payload) if macos? keychain_delete(vault_name) success = system( "security", "add-generic-password", "-a", vault_name, "-s", KEYCHAIN_SERVICE, "-w", payload, out: File::NULL, err: File::NULL ) # Fall back to file store if Keychain fails (e.g., in CI or sandboxed env) unless success file = session_file(vault_name) File.write(file, payload) # chmod unconditionally: File.write's perm: only applies on creation, # so an existing looser-mode file would otherwise keep its perms and # leak the master key. File.chmod(0o600, file) end else keychain_delete(vault_name) file = session_file(vault_name) File.write(file, payload) File.chmod(0o600, file) end end |
.macos? ⇒ Boolean
79 80 81 |
# File 'lib/localvault/session_cache.rb', line 79 def self.macos? RUBY_PLATFORM.include?("darwin") end |
.session_file(vault_name) ⇒ Object
92 93 94 |
# File 'lib/localvault/session_cache.rb', line 92 def self.session_file(vault_name) File.join(sessions_dir, vault_name.gsub(/[^a-zA-Z0-9_\-]/, "_")) end |
.sessions_dir ⇒ Object
83 84 85 86 87 88 89 90 |
# File 'lib/localvault/session_cache.rb', line 83 def self.sessions_dir dir = File.join( ENV.fetch("LOCALVAULT_HOME", File.("~/.localvault")), ".sessions" ) FileUtils.mkdir_p(dir, mode: 0o700) dir end |
.set(vault_name, master_key, ttl_hours: DEFAULT_TTL_HOURS) ⇒ void
This method returns an undefined value.
Cache a master key for the given vault with a time-to-live.
56 57 58 59 60 |
# File 'lib/localvault/session_cache.rb', line 56 def self.set(vault_name, master_key, ttl_hours: DEFAULT_TTL_HOURS) expiry = Time.now.to_i + (ttl_hours * 3600).to_i payload = "#{Base64.strict_encode64(master_key)}|#{expiry}" keychain_set(vault_name, payload) end |