Class: Familia::Encryption::Providers::XChaCha20Poly1305Provider

Inherits:
Familia::Encryption::Provider show all
Includes:
Blake2bPersonalization
Defined in:
lib/familia/encryption/providers/xchacha20_poly1305_provider.rb

Overview

XChaCha20-Poly1305 AEAD provider backed by RbNaCl/libsodium. Derives per-context keys via BLAKE2b keyed hashing with a rotatable personalization string (see Blake2bPersonalization, #333).

Constant Summary collapse

ALGORITHM =
'xchacha20poly1305'
NONCE_SIZE =
24
AUTH_TAG_SIZE =
16
KEY_SIZE =
32

Constants included from Blake2bPersonalization

Blake2bPersonalization::LEGACY_PERSONALIZATION

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Blake2bPersonalization

#current_personalization, #personalizations

Constructor Details

This class inherits a constructor from Familia::Encryption::Provider

Class Method Details

.auth_tag_sizeObject



136
137
138
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 136

def self.auth_tag_size
  AUTH_TAG_SIZE
end

.available?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 48

def self.available?
  !!defined?(RbNaCl)
end

.dependency_hintObject



52
53
54
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 52

def self.dependency_hint
  'rbnacl/libsodium'
end

.nonce_sizeObject



132
133
134
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 132

def self.nonce_size
  NONCE_SIZE
end

.priorityObject



56
57
58
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 56

def self.priority
  100 # Highest priority - best in class
end

Instance Method Details

#algorithmObject



148
149
150
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 148

def algorithm
  ALGORITHM
end

#auth_tag_sizeObject



144
145
146
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 144

def auth_tag_size
  AUTH_TAG_SIZE
end

#decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 75

def decrypt(ciphertext, key, nonce, auth_tag, additional_data = nil)
  validate_key_length!(key)
  box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)

  ciphertext_with_tag = ciphertext + auth_tag
  aad = additional_data.to_s

  box.decrypt(nonce, ciphertext_with_tag, aad)
rescue RbNaCl::CryptoError
  raise EncryptionError, 'Decryption failed - invalid key or corrupted data'
end

#derive_key(master_key, context, personal: nil) ⇒ String

Derives a context-specific encryption key using BLAKE2b.

The personalization parameter provides cryptographic domain separation, ensuring that derived keys are unique per application even when using identical master keys and contexts. This prevents key reuse across different applications or library versions.

personal defaults to the current configured personalization (Blake2bPersonalization#current_personalization), which fails closed on a nil/blank config. The decrypt path passes explicit candidates from #personalizations so ciphertext written under a previous personalization stays decryptable after a rotation (#333).

Parameters:

  • master_key (String)

    The master key (must be >= 32 bytes)

  • context (String)

    Context string for key derivation

  • personal (String, nil) (defaults to: nil)

    Optional personalization override

Returns:

  • (String)

    32-byte derived key



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 108

def derive_key(master_key, context, personal: nil)
  validate_key_length!(master_key)
  # validate_personalization! guards explicitly-passed candidates with
  # the same fail-closed checks as the config path (defense in depth --
  # #personalizations already filters the decrypt walk).
  raw_personal = personal ? validate_personalization!(personal) : current_personalization
  personal_string = raw_personal.ljust(16, "\0")

  RbNaCl::Hash.blake2b(
    # to_s before .b: tolerate non-String contexts (Symbol, nil) and
    # always operate on a fresh BINARY copy (never mutate a frozen
    # caller string -- see issue #250 / FrozenError in benchmark).
    context.to_s.b,
    key: master_key,
    digest_size: KEY_SIZE,
    personal: personal_string,
  )
end

#encrypt(plaintext, key, additional_data = nil) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 60

def encrypt(plaintext, key, additional_data = nil)
  validate_key_length!(key)
  nonce = generate_nonce
  box = RbNaCl::AEAD::XChaCha20Poly1305IETF.new(key)

  aad = additional_data.to_s
  ciphertext_with_tag = box.encrypt(nonce, plaintext.to_s, aad)

  {
    ciphertext: ciphertext_with_tag[0...-16],
    auth_tag: ciphertext_with_tag[-16..],
    nonce: nonce,
  }
end

#generate_nonceObject



87
88
89
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 87

def generate_nonce
  RbNaCl::Random.random_bytes(NONCE_SIZE)
end

#nonce_sizeObject



140
141
142
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 140

def nonce_size
  NONCE_SIZE
end

#secure_wipe(key) ⇒ Object

Clear key from memory (no security guarantees in Ruby)



128
129
130
# File 'lib/familia/encryption/providers/xchacha20_poly1305_provider.rb', line 128

def secure_wipe(key)
  key&.clear
end