Module: LocalVault::SyncBundle
- Defined in:
- lib/localvault/sync_bundle.rb
Overview
Packs and unpacks vault data for cloud sync via InventList + R2.
Bundle versions:
-
v1: personal sync — { version, meta, secrets }
-
v2: legacy team — { version, meta, secrets, key_slots }
-
v3: team with ownership — { version, owner, meta, secrets, key_slots }
The server never sees plaintext — the bundle is opaque ciphertext.
Defined Under Namespace
Classes: UnpackError
Constant Summary collapse
- SUPPORTED_VERSIONS =
[1, 2, 3].freeze
Class Method Summary collapse
-
.pack(store) ⇒ String
Pack a personal vault — v1 format, no key_slots, no owner.
-
.pack_v3(store, owner:, key_slots: {}) ⇒ String
Pack a team vault — v3 format with owner, key_slots, and per-member blobs.
-
.pack_v3_bytes(meta_bytes:, secrets_bytes:, owner:, key_slots: {}) ⇒ String
Pack a v3 team bundle from raw in-memory bytes without touching disk.
-
.unpack(blob, expected_name: nil) ⇒ Hash
Unpack any version bundle into its component parts.
Class Method Details
.pack(store) ⇒ String
Pack a personal vault — v1 format, no key_slots, no owner.
33 34 35 36 37 38 39 40 41 |
# File 'lib/localvault/sync_bundle.rb', line 33 def self.pack(store) = File.read(store.) secrets_content = store.read_encrypted || "" JSON.generate( "version" => 1, "meta" => Base64.strict_encode64(), "secrets" => Base64.strict_encode64(secrets_content) ) end |
.pack_v3(store, owner:, key_slots: {}) ⇒ String
Pack a team vault — v3 format with owner, key_slots, and per-member blobs.
Reads store.meta_path and store.read_encrypted off disk. For flows that need to build a bundle from in-memory bytes without touching disk first (e.g. transactional rotate), use pack_v3_bytes.
53 54 55 56 57 58 59 60 |
# File 'lib/localvault/sync_bundle.rb', line 53 def self.pack_v3(store, owner:, key_slots: {}) pack_v3_bytes( meta_bytes: File.read(store.), secrets_bytes: store.read_encrypted || "", owner: owner, key_slots: key_slots ) end |
.pack_v3_bytes(meta_bytes:, secrets_bytes:, owner:, key_slots: {}) ⇒ String
Pack a v3 team bundle from raw in-memory bytes without touching disk.
Used by transactional rotate flows that need to push the new bundle to the server BEFORE committing the rotated secrets + meta to local disk —if the push fails, nothing on disk changes, so the user can retry without being left with a locally-rotated but remotely-stale vault.
74 75 76 77 78 79 80 81 82 |
# File 'lib/localvault/sync_bundle.rb', line 74 def self.pack_v3_bytes(meta_bytes:, secrets_bytes:, owner:, key_slots: {}) JSON.generate( "version" => 3, "owner" => owner, "meta" => Base64.strict_encode64(), "secrets" => Base64.strict_encode64(secrets_bytes), "key_slots" => key_slots ) end |
.unpack(blob, expected_name: nil) ⇒ Hash
Unpack any version bundle into its component parts.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/localvault/sync_bundle.rb', line 90 def self.unpack(blob, expected_name: nil) data = JSON.parse(blob) version = data["version"] raise UnpackError, "Unsupported bundle version: #{version.inspect}" unless SUPPORTED_VERSIONS.include?(version) = Base64.strict_decode64(data.fetch("meta")) secrets_raw = Base64.strict_decode64(data.fetch("secrets")) key_slots = data["key_slots"].is_a?(Hash) ? data["key_slots"] : {} owner = data["owner"] if expected_name = YAML.safe_load() actual_name = &.dig("name") if actual_name && actual_name != expected_name raise UnpackError, "Bundle meta name '#{actual_name}' does not match expected vault '#{expected_name}'" end end { meta: , secrets: secrets_raw, key_slots: key_slots, owner: owner } rescue JSON::ParserError => e raise UnpackError, "Invalid sync bundle format: #{e.}" rescue KeyError => e raise UnpackError, "Sync bundle missing required field: #{e.}" rescue ArgumentError => e raise UnpackError, "Sync bundle has invalid encoding: #{e.}" end |