localvault 1.3.3 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/localvault/cli/sync.rb +26 -7
- data/lib/localvault/cli/team_helpers.rb +31 -17
- data/lib/localvault/cli.rb +5 -2
- data/lib/localvault/vault.rb +20 -2
- data/lib/localvault/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e57b23276f02c28222edddf4d555066764b38b091b32e5c107c033c447218e2
|
|
4
|
+
data.tar.gz: c5b00e5cb5935555e093ab9df9fa0d1f740beb0986c5935818a4ee163d1c3777
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f28817a5b6c310436977e07f4a51b8ce4373212d9b65b2560576819d43229b5d137053b0aee00efcfe5f8eaa1016372d1ceb7a113d9d372208da8fef803a5d49
|
|
7
|
+
data.tar.gz: 6a2326ed0fde101e65b5433ac2e2a211a8c8067f2aeb8726d6fe640b5ce14956e45b2cf135526e33e8595f8bf1667b06f88483e7e0ff2dac2a950db36f124370
|
data/lib/localvault/cli/sync.rb
CHANGED
|
@@ -22,8 +22,16 @@ module LocalVault
|
|
|
22
22
|
return
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# Load remote state to determine vault mode
|
|
26
|
-
|
|
25
|
+
# Load remote state to determine vault mode. MUST distinguish a
|
|
26
|
+
# genuinely-absent remote (404) from a transient API failure: treating
|
|
27
|
+
# a 5xx as "no remote" would silently downgrade a team vault to a
|
|
28
|
+
# personal v1 bundle on the next push.
|
|
29
|
+
remote_data, load_error = load_remote_bundle_data(vault_name)
|
|
30
|
+
if load_error
|
|
31
|
+
$stderr.puts "Error: #{load_error}"
|
|
32
|
+
$stderr.puts "Refusing to push — cannot verify vault mode. Retry when the server is reachable."
|
|
33
|
+
return
|
|
34
|
+
end
|
|
27
35
|
handle = Config.inventlist_handle
|
|
28
36
|
|
|
29
37
|
if remote_data && remote_data[:owner]
|
|
@@ -214,14 +222,25 @@ module LocalVault
|
|
|
214
222
|
end
|
|
215
223
|
|
|
216
224
|
# Load the full unpacked remote bundle data (owner, key_slots, etc).
|
|
217
|
-
# Returns
|
|
225
|
+
# Returns [data, error_message]:
|
|
226
|
+
# - [hash, nil] on success
|
|
227
|
+
# - [nil, nil] when there genuinely is no remote bundle (404 / empty)
|
|
228
|
+
# - [nil, msg] on any other failure (transient network, 5xx, bad bundle)
|
|
229
|
+
#
|
|
230
|
+
# The caller MUST distinguish these cases — treating a transient error
|
|
231
|
+
# as "no remote" is a data-corruption bug: sync push would then re-upload
|
|
232
|
+
# the vault as a v1 personal bundle, silently downgrading a team vault
|
|
233
|
+
# and wiping its owner + key_slots.
|
|
218
234
|
def load_remote_bundle_data(vault_name)
|
|
219
235
|
client = ApiClient.new(token: Config.token)
|
|
220
236
|
blob = client.pull_vault(vault_name)
|
|
221
|
-
return nil unless blob.is_a?(String) && !blob.empty?
|
|
222
|
-
SyncBundle.unpack(blob)
|
|
223
|
-
rescue ApiClient::ApiError
|
|
224
|
-
nil
|
|
237
|
+
return [nil, nil] unless blob.is_a?(String) && !blob.empty?
|
|
238
|
+
[SyncBundle.unpack(blob), nil]
|
|
239
|
+
rescue ApiClient::ApiError => e
|
|
240
|
+
return [nil, nil] if e.status == 404
|
|
241
|
+
[nil, "Could not load remote bundle for '#{vault_name}': #{e.message}"]
|
|
242
|
+
rescue SyncBundle::UnpackError => e
|
|
243
|
+
[nil, "Could not parse remote bundle for '#{vault_name}': #{e.message}"]
|
|
225
244
|
end
|
|
226
245
|
|
|
227
246
|
# Load key_slots from the last pushed blob (if any).
|
|
@@ -98,28 +98,42 @@ module LocalVault
|
|
|
98
98
|
store = Store.new(vault_name)
|
|
99
99
|
|
|
100
100
|
# Partial scope removal
|
|
101
|
-
if remove_scopes
|
|
102
|
-
|
|
101
|
+
if remove_scopes
|
|
102
|
+
slot = key_slots[handle]
|
|
103
|
+
slot_scopes = slot.is_a?(Hash) ? slot["scopes"] : nil
|
|
104
|
+
|
|
105
|
+
unless slot_scopes.is_a?(Array)
|
|
106
|
+
# Member has full access (nil scopes) — --scope is a user error.
|
|
107
|
+
# Falling through to full removal here used to silently delete
|
|
108
|
+
# the member, which is surprising and destructive.
|
|
109
|
+
$stderr.puts "Error: @#{handle} has full access to '#{vault_name}', not scoped. " \
|
|
110
|
+
"Use `localvault remove @#{handle}` without --scope to revoke access."
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
remaining = slot_scopes - remove_scopes
|
|
103
115
|
if remaining.empty?
|
|
104
116
|
# Last scope removed — remove member entirely
|
|
105
117
|
key_slots.delete(handle)
|
|
106
118
|
$stdout.puts "Removed @#{handle} from vault '#{vault_name}' (last scope removed)."
|
|
107
119
|
else
|
|
108
|
-
# Rebuild blob with remaining scopes
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
# Rebuild blob with remaining scopes. Requires the vault to be
|
|
121
|
+
# unlocked — previously this would silently no-op (print success,
|
|
122
|
+
# push unchanged slot) when the session cache was empty.
|
|
123
|
+
master_key = ensure_master_key(vault_name)
|
|
124
|
+
return unless master_key
|
|
125
|
+
|
|
126
|
+
vault = Vault.new(name: vault_name, master_key: master_key)
|
|
127
|
+
filtered = vault.filter(remaining)
|
|
128
|
+
member_key = RbNaCl::Random.random_bytes(32)
|
|
129
|
+
encrypted_blob = Crypto.encrypt(JSON.generate(filtered), member_key)
|
|
130
|
+
enc_key = KeySlot.create(member_key, slot["pub"])
|
|
131
|
+
key_slots[handle] = {
|
|
132
|
+
"pub" => slot["pub"],
|
|
133
|
+
"enc_key" => enc_key,
|
|
134
|
+
"scopes" => remaining,
|
|
135
|
+
"blob" => Base64.strict_encode64(encrypted_blob)
|
|
136
|
+
}
|
|
123
137
|
$stdout.puts "Removed scope(s) #{remove_scopes.join(", ")} from @#{handle}. Remaining: #{remaining.join(", ")}"
|
|
124
138
|
end
|
|
125
139
|
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -405,8 +405,9 @@ module LocalVault
|
|
|
405
405
|
return
|
|
406
406
|
end
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
408
|
+
# Gather + validate the new passphrase BEFORE destroying the existing
|
|
409
|
+
# vault. If the user enters empty / mismatched / interrupts, we abort
|
|
410
|
+
# without touching anything on disk.
|
|
410
411
|
passphrase = prompt_passphrase("New passphrase: ")
|
|
411
412
|
if passphrase.empty?
|
|
412
413
|
abort_with "Passphrase cannot be empty"
|
|
@@ -419,6 +420,8 @@ module LocalVault
|
|
|
419
420
|
return
|
|
420
421
|
end
|
|
421
422
|
|
|
423
|
+
# All inputs validated — safe to destroy + recreate.
|
|
424
|
+
store.destroy!
|
|
422
425
|
salt = Crypto.generate_salt
|
|
423
426
|
master_key = Crypto.derive_master_key(passphrase, salt)
|
|
424
427
|
Vault.create!(name: vault_name, master_key: master_key, salt: salt)
|
data/lib/localvault/vault.rb
CHANGED
|
@@ -64,7 +64,8 @@ module LocalVault
|
|
|
64
64
|
# @param value [String] the secret value
|
|
65
65
|
# @return [String] the stored value
|
|
66
66
|
# @raise [InvalidKeyName] when key contains invalid characters
|
|
67
|
-
# @raise [RuntimeError] when a scalar key is used as a group
|
|
67
|
+
# @raise [RuntimeError] when a scalar key is used as a group, or when a
|
|
68
|
+
# scalar is being assigned to an existing group name
|
|
68
69
|
def set(key, value)
|
|
69
70
|
validate_key!(key)
|
|
70
71
|
secrets = all
|
|
@@ -74,6 +75,15 @@ module LocalVault
|
|
|
74
75
|
raise "#{group} is a scalar value, not a group" unless secrets[group].is_a?(Hash)
|
|
75
76
|
secrets[group][subkey] = value
|
|
76
77
|
else
|
|
78
|
+
# Refuse to silently clobber an existing group with a scalar. This
|
|
79
|
+
# used to succeed: set("app", "oops") on a vault containing
|
|
80
|
+
# {"app" => {"DB" => ...}} would replace the whole group and lose
|
|
81
|
+
# every nested secret under it.
|
|
82
|
+
if secrets[key].is_a?(Hash)
|
|
83
|
+
raise "'#{key}' is a group containing #{secrets[key].size} secret(s), " \
|
|
84
|
+
"not a scalar. Use `localvault delete #{key}` first if you " \
|
|
85
|
+
"really want to replace the whole group."
|
|
86
|
+
end
|
|
77
87
|
secrets[key] = value
|
|
78
88
|
end
|
|
79
89
|
write_secrets(secrets)
|
|
@@ -283,7 +293,8 @@ module LocalVault
|
|
|
283
293
|
# @param hash [Hash] key-value pairs to merge into the vault
|
|
284
294
|
# @return [void]
|
|
285
295
|
# @raise [InvalidKeyName] when any key contains invalid characters
|
|
286
|
-
# @raise [RuntimeError] when a scalar key is used as a group
|
|
296
|
+
# @raise [RuntimeError] when a scalar key is used as a group, or when a
|
|
297
|
+
# scalar is being assigned to an existing group name
|
|
287
298
|
def merge(hash)
|
|
288
299
|
secrets = all
|
|
289
300
|
hash.each do |k, v|
|
|
@@ -303,6 +314,13 @@ module LocalVault
|
|
|
303
314
|
raise "#{group} is a scalar value, not a group" unless secrets[group].is_a?(Hash)
|
|
304
315
|
secrets[group][subkey] = v.to_s
|
|
305
316
|
else
|
|
317
|
+
# Same guard as Vault#set: don't silently clobber a group with
|
|
318
|
+
# a scalar. This protects bulk `import` and `receive` flows.
|
|
319
|
+
if secrets[k].is_a?(Hash)
|
|
320
|
+
raise "'#{k}' is a group containing #{secrets[k].size} secret(s), " \
|
|
321
|
+
"not a scalar. Delete the group first if you really want " \
|
|
322
|
+
"to replace it with a scalar."
|
|
323
|
+
end
|
|
306
324
|
secrets[k] = v.to_s
|
|
307
325
|
end
|
|
308
326
|
end
|
data/lib/localvault/version.rb
CHANGED