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)
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
-
.decrypt(ciphertext_with_nonce, key) ⇒ String
Decrypt ciphertext produced by
encrypt. -
.decrypt_private_key(encrypted_bytes, master_key) ⇒ String
Decrypt a private key with a master key (convenience wrapper around
decrypt). -
.derive_master_key(passphrase, salt) ⇒ String
Derive a 32-byte master key from a passphrase using Argon2id.
-
.encrypt(plaintext, key) ⇒ String
Encrypt plaintext with XSalsa20-Poly1305.
-
.encrypt_private_key(private_key_bytes, master_key) ⇒ String
Encrypt a private key with a master key (convenience wrapper around
encrypt). -
.generate_keypair ⇒ Hash{Symbol => String}
Generate an X25519 keypair for asymmetric encryption.
-
.generate_salt ⇒ String
Generate a random salt for key derivation.
Class Method Details
.decrypt(ciphertext_with_nonce, key) ⇒ String
Decrypt ciphertext produced by encrypt. Expects nonce prepended.
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.}" end |
.decrypt_private_key(encrypted_bytes, master_key) ⇒ String
Decrypt a private key with a master key (convenience wrapper around decrypt).
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.
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.
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).
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_keypair ⇒ Hash{Symbol => String}
Generate an X25519 keypair for asymmetric encryption.
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_salt ⇒ String
Generate a random salt for key derivation.
52 53 54 |
# File 'lib/localvault/crypto.rb', line 52 def self.generate_salt RbNaCl::Random.random_bytes(SALT_BYTES) end |