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) ⇒ EncryptedData

Returns a new instance of EncryptedData.



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

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

Instance Attribute Details

#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

#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



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

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)


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/familia/encryption/encrypted_data.rb', line 20

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:



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/familia/encryption/encrypted_data.rb', line 39

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)


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

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

#to_hObject

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



15
16
17
# File 'lib/familia/encryption/encrypted_data.rb', line 15

def to_h
  super.compact
end

#validate_decryptable!Object

Raises:



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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
# File 'lib/familia/encryption/encrypted_data.rb', line 96

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