Module: LocalVault::Crypto

Defined in:
lib/localvault/crypto.rb

Overview

Cryptographic primitives for vault encryption and key derivation.

Uses libsodium (via RbNaCl) exclusively:

  • Argon2id for passphrase → master key derivation (memory-hard KDF)

  • XSalsa20-Poly1305 for authenticated symmetric encryption

  • X25519 for asymmetric keypair generation (used by Identity + KeySlot)

Examples:

Derive a master key and encrypt

salt = Crypto.generate_salt
key  = Crypto.derive_master_key("my passphrase", salt)
ct   = Crypto.encrypt("secret data", key)
Crypto.decrypt(ct, key)  # => "secret data"

Defined Under Namespace

Classes: DecryptionError

Constant Summary collapse

SALT_BYTES =
16
NONCE_BYTES =

24

RbNaCl::SecretBoxes::XSalsa20Poly1305.nonce_bytes
KEY_BYTES =

32

RbNaCl::SecretBoxes::XSalsa20Poly1305.key_bytes
ARGON2_OPSLIMIT =

Argon2id parameters (moderate — fast enough for CLI, strong enough for secrets)

2
ARGON2_MEMLIMIT =

64 MB

67_108_864

Class Method Summary collapse

Class Method Details

.decrypt(ciphertext_with_nonce, key) ⇒ String

Decrypt ciphertext produced by encrypt. Expects nonce prepended.

Parameters:

  • ciphertext_with_nonce (String)

    nonce (24 bytes) + ciphertext

  • key (String)

    32-byte symmetric key

Returns:

  • (String)

    decrypted plaintext

Raises:



89
90
91
92
93
94
95
96
# File 'lib/localvault/crypto.rb', line 89

def self.decrypt(ciphertext_with_nonce, key)
  box = RbNaCl::SecretBox.new(key)
  nonce = ciphertext_with_nonce[0, NONCE_BYTES]
  ciphertext = ciphertext_with_nonce[NONCE_BYTES..]
  box.decrypt(nonce, ciphertext)
rescue RbNaCl::CryptoError => e
  raise DecryptionError, "Decryption failed: #{e.message}"
end

.decrypt_private_key(encrypted_bytes, master_key) ⇒ String

Decrypt a private key with a master key (convenience wrapper around decrypt).

Parameters:

  • encrypted_bytes (String)

    nonce + ciphertext from encrypt_private_key

  • master_key (String)

    32-byte symmetric key

Returns:

  • (String)

    raw private key bytes

Raises:



124
125
126
# File 'lib/localvault/crypto.rb', line 124

def self.decrypt_private_key(encrypted_bytes, master_key)
  decrypt(encrypted_bytes, master_key)
end

.derive_master_key(passphrase, salt) ⇒ String

Derive a 32-byte master key from a passphrase using Argon2id.

Parameters:

  • passphrase (String)

    the user’s passphrase

  • salt (String)

    16-byte salt

Returns:

  • (String)

    32-byte derived key



61
62
63
64
65
66
67
68
69
# File 'lib/localvault/crypto.rb', line 61

def self.derive_master_key(passphrase, salt)
  RbNaCl::PasswordHash.argon2id(
    passphrase,
    salt,
    ARGON2_OPSLIMIT,
    ARGON2_MEMLIMIT,
    KEY_BYTES
  )
end

.encrypt(plaintext, key) ⇒ String

Encrypt plaintext with XSalsa20-Poly1305. Prepends a random nonce.

Parameters:

  • plaintext (String)

    data to encrypt

  • key (String)

    32-byte symmetric key

Returns:

  • (String)

    nonce (24 bytes) + ciphertext



76
77
78
79
80
81
# File 'lib/localvault/crypto.rb', line 76

def self.encrypt(plaintext, key)
  box = RbNaCl::SecretBox.new(key)
  nonce = RbNaCl::Random.random_bytes(NONCE_BYTES)
  ciphertext = box.encrypt(nonce, plaintext)
  nonce + ciphertext
end

.encrypt_private_key(private_key_bytes, master_key) ⇒ String

Encrypt a private key with a master key (convenience wrapper around encrypt).

Parameters:

  • private_key_bytes (String)

    raw private key bytes

  • master_key (String)

    32-byte symmetric key

Returns:

  • (String)

    nonce + ciphertext



114
115
116
# File 'lib/localvault/crypto.rb', line 114

def self.encrypt_private_key(private_key_bytes, master_key)
  encrypt(private_key_bytes, master_key)
end

.generate_keypairHash{Symbol => String}

Generate an X25519 keypair for asymmetric encryption.

Returns:

  • (Hash{Symbol => String})

    :public_key and :private_key as raw bytes



101
102
103
104
105
106
107
# File 'lib/localvault/crypto.rb', line 101

def self.generate_keypair
  sk = RbNaCl::PrivateKey.generate
  {
    public_key: sk.public_key.to_bytes,
    private_key: sk.to_bytes
  }
end

.generate_saltString

Generate a random salt for key derivation.

Returns:

  • (String)

    16 random bytes



52
53
54
# File 'lib/localvault/crypto.rb', line 52

def self.generate_salt
  RbNaCl::Random.random_bytes(SALT_BYTES)
end