Module: LocalVault::KeySlot

Defined in:
lib/localvault/key_slot.rb

Overview

Encrypts/decrypts a vault’s master key for a specific user’s X25519 public key.

Key slots enable multi-user vault access via sync. Each authorized user has a slot containing the vault’s master key encrypted to their public key. Uses an ephemeral sender keypair (X25519 Box) — same construction as ShareCrypto.

Examples:

Create and decrypt a key slot

slot = KeySlot.create(master_key, recipient_pub_b64)
recovered = KeySlot.decrypt(slot, recipient_priv_bytes)
recovered == master_key  # => true

Defined Under Namespace

Classes: DecryptionError

Class Method Summary collapse

Class Method Details

.create(master_key, recipient_pub_key_b64) ⇒ String

Encrypt a master key for a recipient’s X25519 public key.

Uses an ephemeral sender keypair so the recipient can decrypt without knowing who sent it.

Parameters:

  • master_key (String)

    raw 32-byte master key to encrypt

  • recipient_pub_key_b64 (String)

    base64-encoded X25519 public key

Returns:

  • (String)

    base64-encoded JSON payload containing the encrypted key slot



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/localvault/key_slot.rb', line 27

def self.create(master_key, recipient_pub_key_b64)
  recipient_pub = RbNaCl::PublicKey.new(Base64.strict_decode64(recipient_pub_key_b64))
  ephemeral_sk  = RbNaCl::PrivateKey.generate
  box           = RbNaCl::Box.new(recipient_pub, ephemeral_sk)
  nonce         = RbNaCl::Random.random_bytes(RbNaCl::Box.nonce_bytes)
  ciphertext    = box.box(nonce, master_key)

  payload = {
    "v"          => 1,
    "sender_pub" => Base64.strict_encode64(ephemeral_sk.public_key.to_bytes),
    "nonce"      => Base64.strict_encode64(nonce),
    "ciphertext" => Base64.strict_encode64(ciphertext)
  }
  Base64.strict_encode64(JSON.generate(payload))
end

.decrypt(slot_b64, my_private_key_bytes) ⇒ String

Decrypt a key slot using the recipient’s private key.

Parameters:

  • slot_b64 (String)

    base64-encoded key slot from create

  • my_private_key_bytes (String)

    raw 32-byte X25519 private key

Returns:

  • (String)

    raw master key bytes

Raises:

  • (DecryptionError)

    when the key is wrong, data is tampered, or format is invalid



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/localvault/key_slot.rb', line 49

def self.decrypt(slot_b64, my_private_key_bytes)
  raw        = Base64.strict_decode64(slot_b64)
  payload    = JSON.parse(raw)
  sender_pub = RbNaCl::PublicKey.new(Base64.strict_decode64(payload.fetch("sender_pub")))
  my_sk      = RbNaCl::PrivateKey.new(my_private_key_bytes)
  box        = RbNaCl::Box.new(sender_pub, my_sk)
  nonce      = Base64.strict_decode64(payload.fetch("nonce"))
  ciphertext = Base64.strict_decode64(payload.fetch("ciphertext"))
  box.open(nonce, ciphertext)
rescue RbNaCl::CryptoError => e
  raise DecryptionError, "Failed to decrypt key slot: #{e.message}"
rescue JSON::ParserError, KeyError => e
  raise DecryptionError, "Invalid key slot format: #{e.message}"
rescue ArgumentError => e
  raise DecryptionError, "Invalid key slot encoding: #{e.message}"
end