localvault 1.0.5 → 1.1.1
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 +66 -2
- data/lib/localvault/cli/team.rb +183 -0
- data/lib/localvault/key_slot.rb +46 -0
- data/lib/localvault/store.rb +1 -0
- data/lib/localvault/sync_bundle.rb +13 -8
- data/lib/localvault/version.rb +1 -1
- data/lib/localvault.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7a7a9c4bee5e2c661610323979b91de29205e4316213bde33496d7785f09d8e
|
|
4
|
+
data.tar.gz: aa0a9d9c4e1387cecc27f7f5de8b67ac7d843f603d3940bd3b5d726f95d2f433
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c39e166b61c0021798884390112dddd051b23c5569ab0d41ec0dc420d0d0e472bff487e8fda5f9dd6bb47c7c1b2de99644fd3463cf9d334634b78dfc4f0f95f3
|
|
7
|
+
data.tar.gz: e3f4423241183217c1a08ea2ee5eae7aeccb167f698b94aedfbe730551b02a1567ed864f0406a84ea425cfc8233dde59221a4380f63d318512e2c1449f294c9d
|
data/lib/localvault/cli/sync.rb
CHANGED
|
@@ -16,7 +16,10 @@ module LocalVault
|
|
|
16
16
|
return
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
key_slots = load_existing_key_slots(vault_name)
|
|
20
|
+
key_slots = bootstrap_owner_slot(key_slots, store)
|
|
21
|
+
|
|
22
|
+
blob = SyncBundle.pack(store, key_slots: key_slots)
|
|
20
23
|
client = ApiClient.new(token: Config.token)
|
|
21
24
|
client.push_vault(vault_name, blob)
|
|
22
25
|
|
|
@@ -52,7 +55,12 @@ module LocalVault
|
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
$stdout.puts "Pulled vault '#{vault_name}'."
|
|
55
|
-
|
|
58
|
+
|
|
59
|
+
if try_unlock_via_key_slot(vault_name, data[:key_slots])
|
|
60
|
+
$stdout.puts "Unlocked via your identity key."
|
|
61
|
+
else
|
|
62
|
+
$stdout.puts "Unlock it with: localvault unlock -v #{vault_name}"
|
|
63
|
+
end
|
|
56
64
|
rescue SyncBundle::UnpackError => e
|
|
57
65
|
$stderr.puts "Error: #{e.message}"
|
|
58
66
|
rescue ApiClient::ApiError => e
|
|
@@ -107,12 +115,68 @@ module LocalVault
|
|
|
107
115
|
|
|
108
116
|
private
|
|
109
117
|
|
|
118
|
+
# Try to decrypt the master key from a key slot matching the current identity.
|
|
119
|
+
# On success, caches the master key in SessionCache. Returns true/false.
|
|
120
|
+
def try_unlock_via_key_slot(vault_name, key_slots)
|
|
121
|
+
return false unless key_slots.is_a?(Hash) && !key_slots.empty?
|
|
122
|
+
return false unless Identity.exists?
|
|
123
|
+
|
|
124
|
+
handle = Config.inventlist_handle
|
|
125
|
+
return false unless handle
|
|
126
|
+
|
|
127
|
+
slot = key_slots[handle]
|
|
128
|
+
return false unless slot.is_a?(Hash) && slot["enc_key"].is_a?(String)
|
|
129
|
+
|
|
130
|
+
master_key = KeySlot.decrypt(slot["enc_key"], Identity.private_key_bytes)
|
|
131
|
+
|
|
132
|
+
# Verify the key actually works by trying to decrypt
|
|
133
|
+
vault = Vault.new(name: vault_name, master_key: master_key)
|
|
134
|
+
vault.all
|
|
135
|
+
|
|
136
|
+
SessionCache.set(vault_name, master_key)
|
|
137
|
+
true
|
|
138
|
+
rescue KeySlot::DecryptionError, Crypto::DecryptionError
|
|
139
|
+
false
|
|
140
|
+
end
|
|
141
|
+
|
|
110
142
|
def logged_in?
|
|
111
143
|
return true if Config.token
|
|
112
144
|
|
|
113
145
|
$stderr.puts "Error: Not logged in. Run: localvault login TOKEN"
|
|
114
146
|
false
|
|
115
147
|
end
|
|
148
|
+
|
|
149
|
+
# Load key_slots from the last pushed blob (if any).
|
|
150
|
+
# Returns {} if no remote blob or if it's a v1 bundle.
|
|
151
|
+
def load_existing_key_slots(vault_name)
|
|
152
|
+
client = ApiClient.new(token: Config.token)
|
|
153
|
+
blob = client.pull_vault(vault_name)
|
|
154
|
+
return {} unless blob.is_a?(String) && !blob.empty?
|
|
155
|
+
data = SyncBundle.unpack(blob)
|
|
156
|
+
data[:key_slots] || {}
|
|
157
|
+
rescue ApiClient::ApiError, SyncBundle::UnpackError
|
|
158
|
+
{}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Add the owner's key slot if identity exists and no slot is present.
|
|
162
|
+
# Requires the vault to be unlockable (needs master key for encryption).
|
|
163
|
+
def bootstrap_owner_slot(key_slots, store)
|
|
164
|
+
return key_slots unless Identity.exists?
|
|
165
|
+
handle = Config.inventlist_handle
|
|
166
|
+
return key_slots unless handle
|
|
167
|
+
|
|
168
|
+
# Already has owner slot — don't churn
|
|
169
|
+
return key_slots if key_slots.key?(handle)
|
|
170
|
+
|
|
171
|
+
# Need the master key to create the slot — try SessionCache
|
|
172
|
+
master_key = SessionCache.get(store.vault_name)
|
|
173
|
+
return key_slots unless master_key
|
|
174
|
+
|
|
175
|
+
pub_b64 = Identity.public_key
|
|
176
|
+
enc_key = KeySlot.create(master_key, pub_b64)
|
|
177
|
+
key_slots[handle] = { "pub" => pub_b64, "enc_key" => enc_key }
|
|
178
|
+
key_slots
|
|
179
|
+
end
|
|
116
180
|
end
|
|
117
181
|
end
|
|
118
182
|
end
|
data/lib/localvault/cli/team.rb
CHANGED
|
@@ -13,6 +13,15 @@ module LocalVault
|
|
|
13
13
|
|
|
14
14
|
vault_name ||= options[:vault] || Config.default_vault
|
|
15
15
|
client = ApiClient.new(token: Config.token)
|
|
16
|
+
|
|
17
|
+
# Try sync-based key slots first
|
|
18
|
+
key_slots = load_key_slots(client, vault_name)
|
|
19
|
+
if key_slots && !key_slots.empty?
|
|
20
|
+
list_key_slots(vault_name, key_slots)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Fall back to direct shares
|
|
16
25
|
result = client.sent_shares(vault_name: vault_name)
|
|
17
26
|
shares = (result["shares"] || []).reject { |s| s["status"] == "revoked" }
|
|
18
27
|
|
|
@@ -34,8 +43,83 @@ module LocalVault
|
|
|
34
43
|
$stderr.puts "Error: #{e.message}"
|
|
35
44
|
end
|
|
36
45
|
|
|
46
|
+
desc "add HANDLE", "Add a teammate to a synced vault via key slot"
|
|
47
|
+
method_option :vault, type: :string, aliases: "-v"
|
|
48
|
+
def add(handle)
|
|
49
|
+
unless Config.token
|
|
50
|
+
$stderr.puts "Error: Not logged in. Run: localvault login TOKEN"
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
unless Identity.exists?
|
|
55
|
+
$stderr.puts "Error: No keypair found. Run: localvault keygen"
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
handle = handle.delete_prefix("@")
|
|
60
|
+
vault_name = options[:vault] || Config.default_vault
|
|
61
|
+
|
|
62
|
+
# Need master key from session
|
|
63
|
+
master_key = SessionCache.get(vault_name)
|
|
64
|
+
unless master_key
|
|
65
|
+
$stderr.puts "Error: Vault '#{vault_name}' is not unlocked. Run: localvault show -v #{vault_name}"
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Fetch recipient's public key
|
|
70
|
+
client = ApiClient.new(token: Config.token)
|
|
71
|
+
result = client.get_public_key(handle)
|
|
72
|
+
pub_key = result["public_key"]
|
|
73
|
+
|
|
74
|
+
unless pub_key && !pub_key.empty?
|
|
75
|
+
$stderr.puts "Error: @#{handle} has no public key published."
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Load existing key slots from remote
|
|
80
|
+
existing_blob = client.pull_vault(vault_name) rescue nil
|
|
81
|
+
key_slots = if existing_blob.is_a?(String) && !existing_blob.empty?
|
|
82
|
+
data = SyncBundle.unpack(existing_blob)
|
|
83
|
+
data[:key_slots].is_a?(Hash) ? data[:key_slots] : {}
|
|
84
|
+
else
|
|
85
|
+
{}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Create key slot for recipient
|
|
89
|
+
begin
|
|
90
|
+
enc_key = KeySlot.create(master_key, pub_key)
|
|
91
|
+
rescue ArgumentError, KeySlot::DecryptionError => e
|
|
92
|
+
$stderr.puts "Error: @#{handle}'s public key is invalid: #{e.message}"
|
|
93
|
+
return
|
|
94
|
+
end
|
|
95
|
+
key_slots[handle] = { "pub" => pub_key, "enc_key" => enc_key }
|
|
96
|
+
|
|
97
|
+
# Ensure owner slot exists too
|
|
98
|
+
owner_handle = Config.inventlist_handle
|
|
99
|
+
unless key_slots.key?(owner_handle)
|
|
100
|
+
owner_pub = Identity.public_key
|
|
101
|
+
key_slots[owner_handle] = { "pub" => owner_pub, "enc_key" => KeySlot.create(master_key, owner_pub) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Pack and push
|
|
105
|
+
store = Store.new(vault_name)
|
|
106
|
+
blob = SyncBundle.pack(store, key_slots: key_slots)
|
|
107
|
+
client.push_vault(vault_name, blob)
|
|
108
|
+
|
|
109
|
+
$stdout.puts "Added @#{handle} to vault '#{vault_name}'."
|
|
110
|
+
rescue ApiClient::ApiError => e
|
|
111
|
+
if e.status == 404
|
|
112
|
+
$stderr.puts "Error: @#{handle} not found or has no public key."
|
|
113
|
+
else
|
|
114
|
+
$stderr.puts "Error: #{e.message}"
|
|
115
|
+
end
|
|
116
|
+
rescue SyncBundle::UnpackError => e
|
|
117
|
+
$stderr.puts "Error: #{e.message}"
|
|
118
|
+
end
|
|
119
|
+
|
|
37
120
|
desc "remove HANDLE", "Remove a person's access to a vault"
|
|
38
121
|
method_option :vault, type: :string, aliases: "-v"
|
|
122
|
+
method_option :rotate, type: :boolean, default: false, desc: "Re-encrypt vault with new master key (full revocation)"
|
|
39
123
|
def remove(handle)
|
|
40
124
|
unless Config.token
|
|
41
125
|
$stderr.puts "Error: Not connected. Run: localvault connect --token TOKEN --handle HANDLE"
|
|
@@ -46,6 +130,14 @@ module LocalVault
|
|
|
46
130
|
vault_name = options[:vault] || Config.default_vault
|
|
47
131
|
client = ApiClient.new(token: Config.token)
|
|
48
132
|
|
|
133
|
+
# Try sync-based key slot removal first
|
|
134
|
+
key_slots = load_key_slots(client, vault_name)
|
|
135
|
+
if key_slots && !key_slots.empty?
|
|
136
|
+
remove_key_slot(handle, vault_name, key_slots, client, rotate: options[:rotate])
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Fall back to direct share revocation
|
|
49
141
|
result = client.sent_shares(vault_name: vault_name)
|
|
50
142
|
share = (result["shares"] || []).find do |s|
|
|
51
143
|
s["recipient_handle"] == handle && s["status"] != "revoked"
|
|
@@ -61,6 +153,97 @@ module LocalVault
|
|
|
61
153
|
rescue ApiClient::ApiError => e
|
|
62
154
|
$stderr.puts "Error: #{e.message}"
|
|
63
155
|
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def load_key_slots(client, vault_name)
|
|
160
|
+
return nil unless client.respond_to?(:pull_vault)
|
|
161
|
+
blob = client.pull_vault(vault_name)
|
|
162
|
+
return nil unless blob.is_a?(String) && !blob.empty?
|
|
163
|
+
data = SyncBundle.unpack(blob)
|
|
164
|
+
slots = data[:key_slots]
|
|
165
|
+
slots.is_a?(Hash) ? slots : nil
|
|
166
|
+
rescue ApiClient::ApiError, SyncBundle::UnpackError, NoMethodError
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def remove_key_slot(handle, vault_name, key_slots, client, rotate: false)
|
|
171
|
+
unless key_slots.key?(handle)
|
|
172
|
+
$stderr.puts "Error: @#{handle} has no slot in vault '#{vault_name}'."
|
|
173
|
+
return
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
valid_slots = key_slots.select { |_, v| v.is_a?(Hash) && v["pub"].is_a?(String) }
|
|
177
|
+
if handle == Config.inventlist_handle && valid_slots.size <= 1
|
|
178
|
+
$stderr.puts "Error: Cannot remove yourself — you are the only member."
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
key_slots.delete(handle)
|
|
183
|
+
store = Store.new(vault_name)
|
|
184
|
+
|
|
185
|
+
if rotate
|
|
186
|
+
master_key = SessionCache.get(vault_name)
|
|
187
|
+
unless master_key
|
|
188
|
+
$stderr.puts "Error: Vault '#{vault_name}' is not unlocked. Run: localvault show -v #{vault_name}"
|
|
189
|
+
return
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Decrypt current secrets, generate new master key, re-encrypt
|
|
193
|
+
vault = Vault.new(name: vault_name, master_key: master_key)
|
|
194
|
+
secrets = vault.all
|
|
195
|
+
|
|
196
|
+
new_salt = Crypto.generate_salt
|
|
197
|
+
new_master_key = Crypto.derive_master_key(SecureRandom.hex(32), new_salt)
|
|
198
|
+
|
|
199
|
+
# Re-encrypt secrets with new master key
|
|
200
|
+
new_json = JSON.generate(secrets)
|
|
201
|
+
new_encrypted = Crypto.encrypt(new_json, new_master_key)
|
|
202
|
+
store.write_encrypted(new_encrypted)
|
|
203
|
+
store.create_meta!(salt: new_salt)
|
|
204
|
+
|
|
205
|
+
# Re-create key slots for remaining members with new master key
|
|
206
|
+
new_slots = {}
|
|
207
|
+
key_slots.each do |h, slot|
|
|
208
|
+
next unless slot.is_a?(Hash) && slot["pub"].is_a?(String)
|
|
209
|
+
new_slots[h] = { "pub" => slot["pub"], "enc_key" => KeySlot.create(new_master_key, slot["pub"]) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
blob = SyncBundle.pack(store, key_slots: new_slots)
|
|
213
|
+
client.push_vault(vault_name, blob)
|
|
214
|
+
|
|
215
|
+
# Only cache new master key if the caller is still a member
|
|
216
|
+
if new_slots.key?(Config.inventlist_handle)
|
|
217
|
+
SessionCache.set(vault_name, new_master_key)
|
|
218
|
+
else
|
|
219
|
+
SessionCache.clear(vault_name)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
$stdout.puts "Removed @#{handle} from vault '#{vault_name}'."
|
|
223
|
+
$stdout.puts "Vault re-encrypted with new master key (rotated)."
|
|
224
|
+
else
|
|
225
|
+
blob = SyncBundle.pack(store, key_slots: key_slots)
|
|
226
|
+
client.push_vault(vault_name, blob)
|
|
227
|
+
$stdout.puts "Removed @#{handle} from vault '#{vault_name}'."
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def list_key_slots(vault_name, key_slots)
|
|
232
|
+
my_handle = Config.inventlist_handle
|
|
233
|
+
valid = key_slots.select { |_, v| v.is_a?(Hash) && v["pub"].is_a?(String) }
|
|
234
|
+
|
|
235
|
+
if valid.empty?
|
|
236
|
+
$stdout.puts "No key slots for vault '#{vault_name}'."
|
|
237
|
+
return
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
$stdout.puts "Vault: #{vault_name} — #{valid.size} member(s)"
|
|
241
|
+
$stdout.puts
|
|
242
|
+
valid.sort.each do |handle, slot|
|
|
243
|
+
marker = handle == my_handle ? " (you)" : ""
|
|
244
|
+
$stdout.puts " @#{handle}#{marker}"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
64
247
|
end
|
|
65
248
|
end
|
|
66
249
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require "rbnacl"
|
|
2
|
+
require "base64"
|
|
3
|
+
|
|
4
|
+
module LocalVault
|
|
5
|
+
module KeySlot
|
|
6
|
+
class DecryptionError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Encrypt a master key for a recipient's X25519 public key.
|
|
9
|
+
# Returns a base64-encoded ciphertext string.
|
|
10
|
+
# Uses an ephemeral sender keypair (same Box construction as ShareCrypto).
|
|
11
|
+
def self.create(master_key, recipient_pub_key_b64)
|
|
12
|
+
recipient_pub = RbNaCl::PublicKey.new(Base64.strict_decode64(recipient_pub_key_b64))
|
|
13
|
+
ephemeral_sk = RbNaCl::PrivateKey.generate
|
|
14
|
+
box = RbNaCl::Box.new(recipient_pub, ephemeral_sk)
|
|
15
|
+
nonce = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
|
|
16
|
+
ciphertext = box.box(nonce, master_key)
|
|
17
|
+
|
|
18
|
+
payload = {
|
|
19
|
+
"v" => 1,
|
|
20
|
+
"sender_pub" => Base64.strict_encode64(ephemeral_sk.public_key.to_bytes),
|
|
21
|
+
"nonce" => Base64.strict_encode64(nonce),
|
|
22
|
+
"ciphertext" => Base64.strict_encode64(ciphertext)
|
|
23
|
+
}
|
|
24
|
+
Base64.strict_encode64(JSON.generate(payload))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decrypt a key slot using the recipient's private key.
|
|
28
|
+
# Returns the raw master key bytes.
|
|
29
|
+
def self.decrypt(slot_b64, my_private_key_bytes)
|
|
30
|
+
raw = Base64.strict_decode64(slot_b64)
|
|
31
|
+
payload = JSON.parse(raw)
|
|
32
|
+
sender_pub = RbNaCl::PublicKey.new(Base64.strict_decode64(payload.fetch("sender_pub")))
|
|
33
|
+
my_sk = RbNaCl::PrivateKey.new(my_private_key_bytes)
|
|
34
|
+
box = RbNaCl::Box.new(sender_pub, my_sk)
|
|
35
|
+
nonce = Base64.strict_decode64(payload.fetch("nonce"))
|
|
36
|
+
ciphertext = Base64.strict_decode64(payload.fetch("ciphertext"))
|
|
37
|
+
box.open(nonce, ciphertext)
|
|
38
|
+
rescue RbNaCl::CryptoError => e
|
|
39
|
+
raise DecryptionError, "Failed to decrypt key slot: #{e.message}"
|
|
40
|
+
rescue JSON::ParserError, KeyError => e
|
|
41
|
+
raise DecryptionError, "Invalid key slot format: #{e.message}"
|
|
42
|
+
rescue ArgumentError => e
|
|
43
|
+
raise DecryptionError, "Invalid key slot encoding: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/localvault/store.rb
CHANGED
|
@@ -6,29 +6,34 @@ module LocalVault
|
|
|
6
6
|
module SyncBundle
|
|
7
7
|
class UnpackError < StandardError; end
|
|
8
8
|
|
|
9
|
-
VERSION =
|
|
9
|
+
VERSION = 2
|
|
10
|
+
SUPPORTED_VERSIONS = [1, 2].freeze
|
|
10
11
|
|
|
11
12
|
# Pack a vault's meta.yml + secrets.enc into a single JSON blob.
|
|
12
13
|
# The secrets.enc is already encrypted — this bundle is opaque to the server.
|
|
13
|
-
|
|
14
|
+
# key_slots: optional hash of { "handle" => { "pub" => b64, "enc_key" => b64 }, ... }
|
|
15
|
+
def self.pack(store, key_slots: {})
|
|
14
16
|
meta_content = File.read(store.meta_path)
|
|
15
17
|
secrets_content = store.read_encrypted || ""
|
|
16
18
|
JSON.generate(
|
|
17
|
-
"version"
|
|
18
|
-
"meta"
|
|
19
|
-
"secrets"
|
|
19
|
+
"version" => VERSION,
|
|
20
|
+
"meta" => Base64.strict_encode64(meta_content),
|
|
21
|
+
"secrets" => Base64.strict_encode64(secrets_content),
|
|
22
|
+
"key_slots" => key_slots
|
|
20
23
|
)
|
|
21
24
|
end
|
|
22
25
|
|
|
23
|
-
# Unpack a blob back into {meta:, secrets:}
|
|
26
|
+
# Unpack a blob back into {meta:, secrets:, key_slots:}.
|
|
24
27
|
# Pass expected_name: to validate the meta.yml name matches the vault being pulled.
|
|
28
|
+
# v1 bundles return empty key_slots for backward compatibility.
|
|
25
29
|
def self.unpack(blob, expected_name: nil)
|
|
26
30
|
data = JSON.parse(blob)
|
|
27
31
|
version = data["version"]
|
|
28
|
-
raise UnpackError, "Unsupported bundle version: #{version}" if version && version
|
|
32
|
+
raise UnpackError, "Unsupported bundle version: #{version}" if version && !SUPPORTED_VERSIONS.include?(version)
|
|
29
33
|
|
|
30
34
|
meta_raw = Base64.strict_decode64(data.fetch("meta"))
|
|
31
35
|
secrets_raw = Base64.strict_decode64(data.fetch("secrets"))
|
|
36
|
+
key_slots = data["key_slots"].is_a?(Hash) ? data["key_slots"] : {}
|
|
32
37
|
|
|
33
38
|
if expected_name
|
|
34
39
|
meta_parsed = YAML.safe_load(meta_raw)
|
|
@@ -38,7 +43,7 @@ module LocalVault
|
|
|
38
43
|
end
|
|
39
44
|
end
|
|
40
45
|
|
|
41
|
-
{ meta: meta_raw, secrets: secrets_raw }
|
|
46
|
+
{ meta: meta_raw, secrets: secrets_raw, key_slots: key_slots }
|
|
42
47
|
rescue JSON::ParserError => e
|
|
43
48
|
raise UnpackError, "Invalid sync bundle format: #{e.message}"
|
|
44
49
|
rescue KeyError => e
|
data/lib/localvault/version.rb
CHANGED
data/lib/localvault.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "localvault/store"
|
|
|
5
5
|
require_relative "localvault/vault"
|
|
6
6
|
require_relative "localvault/identity"
|
|
7
7
|
require_relative "localvault/share_crypto"
|
|
8
|
+
require_relative "localvault/key_slot"
|
|
8
9
|
require_relative "localvault/api_client"
|
|
9
10
|
require_relative "localvault/sync_bundle"
|
|
10
11
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: localvault
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nauman Tariq
|
|
@@ -114,6 +114,7 @@ files:
|
|
|
114
114
|
- lib/localvault/config.rb
|
|
115
115
|
- lib/localvault/crypto.rb
|
|
116
116
|
- lib/localvault/identity.rb
|
|
117
|
+
- lib/localvault/key_slot.rb
|
|
117
118
|
- lib/localvault/mcp/server.rb
|
|
118
119
|
- lib/localvault/mcp/tools.rb
|
|
119
120
|
- lib/localvault/session_cache.rb
|