Module: Browserctl::EncryptionService

Defined in:
lib/browserctl/encryption_service.rb

Overview

Cryptographic primitives for browserctl-managed payloads.

Raised when decryption fails (tampered ciphertext or wrong key). Callers should catch this rather than ‘OpenSSL::Cipher::CipherError` so they don’t have to take a direct dependency on the underlying cipher library.

Owns AES-256-GCM cipher setup and PBKDF2 key derivation so callers like ‘State::Bundle` don’t reach into ‘OpenSSL::Cipher` directly. The contract is intentionally narrow:

* `derive_keys(passphrase, salt)` returns a `[enc_key, hmac_key]` pair
  derived via PBKDF2-HMAC-SHA256 with `PBKDF2_ITERS` iterations and a
  64-byte output split in half. The first half is the AES-256-GCM key,
  the second is the HMAC-SHA-256 key for the bundle footer.
* `encrypt(plaintext, key)` returns `nonce || ciphertext || tag`.
* `decrypt(blob, key)` reverses it; raises `OpenSSL::Cipher::CipherError`
  on tampered blobs / wrong key.
* `random_salt` / `random_nonce` are exposed so callers can keep bundle
  wire-format assembly in one place without re-deriving sizes.

The constants here are duplicated in ‘State::Bundle` only as named references to the wire format positions; the source of truth lives here.

Defined Under Namespace

Classes: DecryptionError

Constant Summary collapse

SALT_SIZE =
16
NONCE_SIZE =
12
TAG_SIZE =
16
KEY_SIZE =
32
PBKDF2_ITERS =
200_000
DIGEST =
"SHA256"
CIPHER =
"aes-256-gcm"

Class Method Summary collapse

Class Method Details

.decrypt(blob, key) ⇒ Object

Inverse of ‘encrypt`. Raises `DecryptionError` on tampered ciphertext or wrong key so callers don’t need to reach into ‘OpenSSL::Cipher`.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/browserctl/encryption_service.rb', line 61

def decrypt(blob, key)
  nonce      = blob.byteslice(0, NONCE_SIZE)
  tag        = blob.byteslice(-TAG_SIZE, TAG_SIZE)
  ciphertext = blob.byteslice(NONCE_SIZE, blob.bytesize - NONCE_SIZE - TAG_SIZE)

  cipher = OpenSSL::Cipher.new(CIPHER)
  cipher.decrypt
  cipher.key      = key
  cipher.iv       = nonce
  cipher.auth_tag = tag
  cipher.update(ciphertext) + cipher.final
rescue OpenSSL::Cipher::CipherError => e
  raise DecryptionError, e.message
end

.derive_keys(passphrase, salt) ⇒ Object

Returns ‘[enc_key, hmac_key]`, each `KEY_SIZE` bytes.



43
44
45
46
# File 'lib/browserctl/encryption_service.rb', line 43

def derive_keys(passphrase, salt)
  material = OpenSSL::PKCS5.pbkdf2_hmac(passphrase.to_s, salt, PBKDF2_ITERS, KEY_SIZE * 2, DIGEST)
  [material.byteslice(0, KEY_SIZE), material.byteslice(KEY_SIZE, KEY_SIZE)]
end

.encrypt(plaintext, key) ⇒ Object

AES-256-GCM. Returns ‘nonce || ciphertext || tag`.



49
50
51
52
53
54
55
56
57
# File 'lib/browserctl/encryption_service.rb', line 49

def encrypt(plaintext, key)
  cipher = OpenSSL::Cipher.new(CIPHER)
  cipher.encrypt
  cipher.key = key
  nonce = SecureRandom.bytes(NONCE_SIZE)
  cipher.iv = nonce
  ct = cipher.update(plaintext) + cipher.final
  nonce + ct + cipher.auth_tag
end

.random_nonceObject



80
81
82
# File 'lib/browserctl/encryption_service.rb', line 80

def random_nonce
  SecureRandom.bytes(NONCE_SIZE)
end

.random_saltObject



76
77
78
# File 'lib/browserctl/encryption_service.rb', line 76

def random_salt
  SecureRandom.bytes(SALT_SIZE)
end