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.



9
10
11
12
# File 'lib/familia/encryption/encrypted_data.rb', line 9

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



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def aad_fields
  @aad_fields
end

#algorithmObject (readonly)

Returns the value of attribute algorithm

Returns:

  • (Object)

    the current value of algorithm



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def algorithm
  @algorithm
end

#auth_tagObject (readonly)

Returns the value of attribute auth_tag

Returns:

  • (Object)

    the current value of auth_tag



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def auth_tag
  @auth_tag
end

#ciphertextObject (readonly)

Returns the value of attribute ciphertext

Returns:

  • (Object)

    the current value of ciphertext



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def ciphertext
  @ciphertext
end

#encodingObject (readonly)

Returns the value of attribute encoding

Returns:

  • (Object)

    the current value of encoding



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def encoding
  @encoding
end

#envelope_versionObject (readonly)

Returns the value of attribute envelope_version

Returns:

  • (Object)

    the current value of envelope_version



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

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



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

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



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def key_version
  @key_version
end

#nonceObject (readonly)

Returns the value of attribute nonce

Returns:

  • (Object)

    the current value of nonce



7
8
9
# File 'lib/familia/encryption/encrypted_data.rb', line 7

def nonce
  @nonce
end

Class Method Details

.from_json(json_string_or_hash) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/familia/encryption/encrypted_data.rb', line 85

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)


45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/familia/encryption/encrypted_data.rb', line 45

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}, parsed: #{parsed}, required: #{required_fields}"
    result
  rescue Familia::SerializerError => e
    Familia.debug "[valid?] JSON error: #{e.message}"
    false
  end
end

.validate!(json_string) ⇒ Object

Raises:



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/familia/encryption/encrypted_data.rb', line 64

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.message}"
  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)


100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/familia/encryption/encrypted_data.rb', line 100

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)

  # 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

#has_key_material?Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/familia/encryption/encrypted_data.rb', line 36

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

#stored_aad_fieldsObject



40
41
42
# File 'lib/familia/encryption/encrypted_data.rb', line 40

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).



17
18
19
# File 'lib/familia/encryption/encrypted_data.rb', line 17

def to_h
  super.compact
end

#to_json(*_args) ⇒ Object



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

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

#validate_decryptable!Object

Raises:



121
122
123
124
125
126
127
128
129
130
131
132
133
134
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
# File 'lib/familia/encryption/encrypted_data.rb', line 121

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

  self
end

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



25
26
27
28
29
30
31
32
33
34
# File 'lib/familia/encryption/encrypted_data.rb', line 25

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