Class: LocalVault::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/localvault/store.rb

Overview

File-system storage for a single vault’s encrypted data and metadata.

Each vault lives at ~/.localvault/vaults/<name>/ with two files:

  • meta.yml — salt, creation date, version, secret count

  • secrets.enc — encrypted JSON blob (XSalsa20-Poly1305)

Uses atomic writes (tempfile + rename) to prevent corruption. All directories are created with mode 0700, all files with mode 0600.

Examples:

store = Store.new("production")
store.create!(salt: Crypto.generate_salt)
store.write_encrypted(ciphertext)
store.read_encrypted  # => ciphertext bytes

Defined Under Namespace

Classes: InvalidVaultName

Constant Summary collapse

VAULT_NAME_PATTERN =

Letters, digits, underscore, dash. Must start with alphanumeric.

/\A[a-zA-Z0-9][a-zA-Z0-9_\-]*\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(vault_name) ⇒ Store

Initialize a store for the given vault name.

Parameters:

  • vault_name (String)

    the vault name (alphanumeric, dash, underscore)

Raises:

  • (InvalidVaultName)

    when name is empty, too long, or has invalid characters



35
36
37
38
# File 'lib/localvault/store.rb', line 35

def initialize(vault_name)
  validate_vault_name!(vault_name)
  @vault_name = vault_name
end

Instance Attribute Details

#vault_nameObject (readonly)

Returns the value of attribute vault_name.



29
30
31
# File 'lib/localvault/store.rb', line 29

def vault_name
  @vault_name
end

Class Method Details

.list_vaultsArray<String>

List all vault names found on disk.

Returns:

  • (Array<String>)

    sorted vault names



191
192
193
194
195
196
197
198
# File 'lib/localvault/store.rb', line 191

def self.list_vaults
  vaults_dir = Config.vaults_path
  return [] unless File.directory?(vaults_dir)

  Dir.children(vaults_dir)
    .select { |name| File.directory?(File.join(vaults_dir, name)) }
    .sort
end

Instance Method Details

#countInteger

Number of secrets stored in this vault.

Returns:

  • (Integer)

    the secret count from metadata, defaults to 0



107
108
109
# File 'lib/localvault/store.rb', line 107

def count
  meta&.dig("count") || 0
end

#create!(salt:) ⇒ void

This method returns an undefined value.

Create a new vault on disk with initial metadata.

Parameters:

  • salt (String)

    raw salt bytes for key derivation

Raises:

  • (RuntimeError)

    when the vault already exists



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/localvault/store.rb', line 73

def create!(salt:)
  raise "Vault '#{vault_name}' already exists" if exists?

  FileUtils.mkdir_p(vault_path, mode: 0o700)
  new_meta = {
    "name"       => vault_name,
    "created_at" => Time.now.utc.iso8601,
    "version"    => 1,
    "salt"       => Base64.strict_encode64(salt),
    "count"      => 0
  }
  write_meta(new_meta)
end

#create_meta!(salt:) ⇒ void

This method returns an undefined value.

Create or overwrite metadata with a new salt, preserving created_at if present.

Parameters:

  • salt (String)

    raw salt bytes for key derivation



154
155
156
157
158
159
160
161
162
163
# File 'lib/localvault/store.rb', line 154

def create_meta!(salt:)
  existing = meta
  new_meta = {
    "name"       => vault_name,
    "created_at" => existing&.dig("created_at") || Time.now.utc.iso8601,
    "version"    => 1,
    "salt"       => Base64.strict_encode64(salt)
  }
  write_meta(new_meta)
end

#destroy!void

This method returns an undefined value.

Permanently delete this vault’s directory and all its contents.



168
169
170
# File 'lib/localvault/store.rb', line 168

def destroy!
  FileUtils.rm_rf(vault_path)
end

#exists?Boolean

Check whether this vault exists on disk.

Returns:

  • (Boolean)

    true if the vault directory and meta file exist



64
65
66
# File 'lib/localvault/store.rb', line 64

def exists?
  File.directory?(vault_path) && File.exist?(meta_path)
end

#metaHash?

Read and parse the vault’s metadata.

Returns:

  • (Hash, nil)

    the parsed meta.yml contents, or nil if file is missing



90
91
92
93
# File 'lib/localvault/store.rb', line 90

def meta
  return nil unless File.exist?(meta_path)
  YAML.safe_load_file(meta_path)
end

#meta_pathString

Absolute path to the metadata file.

Returns:

  • (String)

    path to meta.yml



57
58
59
# File 'lib/localvault/store.rb', line 57

def meta_path
  File.join(vault_path, "meta.yml")
end

#read_encryptedString?

Read the encrypted secrets blob from disk.

Returns:

  • (String, nil)

    raw ciphertext bytes, or nil if file is missing



125
126
127
128
# File 'lib/localvault/store.rb', line 125

def read_encrypted
  return nil unless File.exist?(secrets_path)
  File.binread(secrets_path)
end

#saltString?

Read the raw salt bytes from metadata.

Returns:

  • (String, nil)

    decoded salt bytes, or nil if not available



98
99
100
101
102
# File 'lib/localvault/store.rb', line 98

def salt
  m = meta
  return nil unless m && m["salt"]
  Base64.strict_decode64(m["salt"])
end

#secrets_pathString

Absolute path to the encrypted secrets file.

Returns:

  • (String)

    path to secrets.enc



50
51
52
# File 'lib/localvault/store.rb', line 50

def secrets_path
  File.join(vault_path, "secrets.enc")
end

#update_count!(n) ⇒ void

This method returns an undefined value.

Update the secret count in metadata.

Parameters:

  • n (Integer)

    the new count



115
116
117
118
119
120
# File 'lib/localvault/store.rb', line 115

def update_count!(n)
  m = meta
  return unless m
  m["count"] = n
  write_meta(m)
end

#vault_pathString

Absolute path to this vault’s directory.

Returns:

  • (String)

    path to ~/.localvault/vaults/<name>/



43
44
45
# File 'lib/localvault/store.rb', line 43

def vault_path
  File.join(Config.vaults_path, vault_name)
end

#write_encrypted(bytes) ⇒ void

This method returns an undefined value.

Atomically write encrypted bytes to disk using tempfile + rename.

Parameters:

  • bytes (String)

    raw ciphertext bytes to write



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/localvault/store.rb', line 134

def write_encrypted(bytes)
  FileUtils.mkdir_p(vault_path, mode: 0o700)

  # Atomic write: write to temp file, then rename
  tmp = Tempfile.new("localvault", vault_path)
  tmp.binmode
  tmp.write(bytes)
  tmp.close
  File.rename(tmp.path, secrets_path)
  File.chmod(0o600, secrets_path)
rescue StandardError
  tmp&.close
  tmp&.unlink
  raise
end