Class: Familia::Encryption::EncryptedData

Inherits:
Data
  • Object
show all
Defined in:
lib/familia/encryption/encrypted_data.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(algorithm:, nonce:, ciphertext:, auth_tag:, key_version:, encoding: nil, envelope_version: nil, aad_fields: nil, key_material_fields: nil) ⇒ EncryptedData

Returns a new instance of EncryptedData.



21
22
23
24
# File 'lib/familia/encryption/encrypted_data.rb', line 21

def initialize(algorithm:, nonce:, ciphertext:, auth_tag:, key_version:, encoding: nil, envelope_version: nil,
               aad_fields: nil, key_material_fields: nil)
  super
end

Instance Attribute Details

#aad_fieldsObject (readonly)

Returns the value of attribute aad_fields

Returns:

  • (Object)

    the current value of aad_fields



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def aad_fields
  @aad_fields
end

#algorithmObject (readonly)

Returns the value of attribute algorithm

Returns:

  • (Object)

    the current value of algorithm



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def algorithm
  @algorithm
end

#auth_tagObject (readonly)

Returns the value of attribute auth_tag

Returns:

  • (Object)

    the current value of auth_tag



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def auth_tag
  @auth_tag
end

#ciphertextObject (readonly)

Returns the value of attribute ciphertext

Returns:

  • (Object)

    the current value of ciphertext



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def ciphertext
  @ciphertext
end

#encodingObject (readonly)

Returns the value of attribute encoding

Returns:

  • (Object)

    the current value of encoding



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def encoding
  @encoding
end

#envelope_versionObject (readonly)

Returns the value of attribute envelope_version

Returns:

  • (Object)

    the current value of envelope_version



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def envelope_version
  @envelope_version
end

#key_material_fieldsObject (readonly)

Returns the value of attribute key_material_fields

Returns:

  • (Object)

    the current value of key_material_fields



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def key_material_fields
  @key_material_fields
end

#key_versionObject (readonly)

Returns the value of attribute key_version

Returns:

  • (Object)

    the current value of key_version



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def key_version
  @key_version
end

#nonceObject (readonly)

Returns the value of attribute nonce

Returns:

  • (Object)

    the current value of nonce



19
20
21
# File 'lib/familia/encryption/encrypted_data.rb', line 19

def nonce
  @nonce
end

Class Method Details

.from_json(json_string_or_hash) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/familia/encryption/encrypted_data.rb', line 97

def self.from_json(json_string_or_hash)
  # Support both JSON strings (legacy) and already-parsed Hashes (v2.0 deserialization)
  if json_string_or_hash.is_a?(Hash)
    # Already parsed - use directly
    parsed = json_string_or_hash
    # Symbolize keys if they're strings
    parsed = parsed.transform_keys(&:to_sym) if parsed.keys.first.is_a?(String)
    new(**parsed.slice(*members))
  else
    # JSON string - validate and parse
    validate!(json_string_or_hash)
  end
end

.valid?(json_string) ⇒ Boolean

Class methods for parsing and validation

Returns:

  • (Boolean)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/familia/encryption/encrypted_data.rb', line 57

def self.valid?(json_string)
  return true if json_string.nil? # Allow nil values
  return false unless json_string.is_a?(::String)

  begin
    parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
    return false unless parsed.is_a?(Hash)

    # Check for required fields
    required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
    result = required_fields.all? { |field| parsed.key?(field) }
    Familia.debug "[valid?] result: #{result}, missing: #{(required_fields - parsed.keys).inspect}"
    result
  rescue Familia::SerializerError => e
    Familia.debug "[valid?] JSON error: #{e.class}"
    false
  end
end

.validate!(json_string) ⇒ Object

Raises:



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/familia/encryption/encrypted_data.rb', line 76

def self.validate!(json_string)
  return nil if json_string.nil?

  raise EncryptionError, "Expected JSON string, got #{json_string.class}" unless json_string.is_a?(::String)

  begin
    parsed = Familia::JsonSerializer.parse(json_string, symbolize_names: true)
  rescue Familia::SerializerError => e
    raise EncryptionError, "Invalid JSON structure: #{e.class}"
  end

  raise EncryptionError, "Expected JSON object, got #{parsed.class}" unless parsed.is_a?(Hash)

  required_fields = %i[algorithm nonce ciphertext auth_tag key_version]
  missing_fields = required_fields.reject { |field| parsed.key?(field) }

  raise EncryptionError, "Missing required fields: #{missing_fields.join(', ')}" unless missing_fields.empty?

  new(**parsed.slice(*members))
end

Instance Method Details

#decryptable?Boolean

Instance methods for decryptability validation

Returns:

  • (Boolean)


112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/familia/encryption/encrypted_data.rb', line 112

def decryptable?
  return false unless algorithm && nonce && ciphertext && auth_tag && key_version

  # Ensure Registry is set up before checking algorithms
  Registry.setup! if Registry.providers.empty?

  # Check if algorithm is supported
  return false unless Registry.providers.key?(algorithm)

  return false unless encoding_valid?

  # Validate Base64 encoding of binary fields
  begin
    Base64.strict_decode64(nonce)
    Base64.strict_decode64(ciphertext)
    Base64.strict_decode64(auth_tag)
  rescue ArgumentError
    return false
  end

  true
end

#encoding_valid?Boolean

The encoding field is optional (pre-#229 envelopes omit it and decrypt as UTF-8). When present it must be a String naming an encoding this Ruby knows, see ENVELOPE_ENCODINGS.

Returns:

  • (Boolean)


192
193
194
# File 'lib/familia/encryption/encrypted_data.rb', line 192

def encoding_valid?
  encoding.nil? || (encoding.is_a?(::String) && ENVELOPE_ENCODINGS.include?(encoding))
end

#has_key_material?Boolean

Returns:

  • (Boolean)


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

def has_key_material?
  !key_material_fields.nil? && !key_material_fields.empty?
end

#stored_aad_fieldsObject



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

def stored_aad_fields
  aad_fields&.map(&:to_sym)
end

#to_hObject

Omit nil-valued keys from the hash representation so that the encrypted envelope stays backward-compatible (no :encoding key unless explicitly set).



29
30
31
# File 'lib/familia/encryption/encrypted_data.rb', line 29

def to_h
  super.compact
end

#to_json(*_args) ⇒ Object



33
34
35
# File 'lib/familia/encryption/encrypted_data.rb', line 33

def to_json(*_args)
  Familia::JsonSerializer.dump(to_h)
end

#validate_decryptable!Object

Raises:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/familia/encryption/encrypted_data.rb', line 135

def validate_decryptable!
  raise EncryptionError, 'Missing algorithm field' unless algorithm

  # Ensure Registry is set up before checking algorithms
  Registry.setup! if Registry.providers.empty?

  raise EncryptionError, "Unsupported algorithm: #{algorithm}" unless Registry.providers.key?(algorithm)

  unless nonce && ciphertext && auth_tag && key_version
    missing = []
    missing << 'nonce' unless nonce
    missing << 'ciphertext' unless ciphertext
    missing << 'auth_tag' unless auth_tag
    missing << 'key_version' unless key_version
    raise EncryptionError, "Missing required fields: #{missing.join(', ')}"
  end

  # Get the provider for size validation
  provider = Registry.providers[algorithm]

  # Validate Base64 encoding and sizes
  begin
    decoded_nonce = Base64.strict_decode64(nonce)
    if decoded_nonce.bytesize != provider.nonce_size
      raise EncryptionError, "Invalid nonce size: expected #{provider.nonce_size}, got #{decoded_nonce.bytesize}"
    end
  rescue ArgumentError
    raise EncryptionError, 'Invalid Base64 encoding in nonce field'
  end

  begin
    Base64.strict_decode64(ciphertext) # ciphertext can be variable size
  rescue ArgumentError
    raise EncryptionError, 'Invalid Base64 encoding in ciphertext field'
  end

  begin
    decoded_auth_tag = Base64.strict_decode64(auth_tag)
    if decoded_auth_tag.bytesize != provider.auth_tag_size
      raise EncryptionError,
            "Invalid auth_tag size: expected #{provider.auth_tag_size}, got #{decoded_auth_tag.bytesize}"
    end
  rescue ArgumentError
    raise EncryptionError, 'Invalid Base64 encoding in auth_tag field'
  end

  # Validate that the key version exists
  unless Familia.config.encryption_keys&.key?(key_version.to_sym)
    raise EncryptionError, "No key for version: #{key_version}"
  end

  validate_encoding!
end

#validate_encoding!Object

Raises:

  • (EncryptionError)

    when the encoding field is present but not a known encoding name. Raised as a defined error so it can never be mistaken for corrupted ciphertext (#408).



199
200
201
202
203
# File 'lib/familia/encryption/encrypted_data.rb', line 199

def validate_encoding!
  return self if encoding_valid?

  raise EncryptionError, "Unsupported encoding: #{encoding.inspect}"
end

#with_metadata(envelope_version: self.envelope_version, aad_fields: self.aad_fields, key_material_fields: self.key_material_fields) ⇒ Object



37
38
39
40
41
42
43
44
45
46
# File 'lib/familia/encryption/encrypted_data.rb', line 37

def (envelope_version: self.envelope_version, aad_fields: self.aad_fields,
                   key_material_fields: self.key_material_fields)
  # EncryptedData is a Data.define, so #with copies all other members
  # for us; only the envelope metadata is overridden here.
  with(
    envelope_version: envelope_version,
    aad_fields: aad_fields,
    key_material_fields: key_material_fields
  )
end