1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
|
# frozen_string_literal: true
require "base64"
module ActiveRecord
module Encryption
# A message serializer that serializes +Messages+ with JSON.
#
# The generated structure is pretty simple:
#
# {
# p: <payload>,
# h: {
# header1: value1,
# header2: value2,
# ...
# }
# }
#
# Both the payload and the header values are encoded with Base64
# to prevent JSON parsing errors and encoding issues when
# storing the resulting serialized data.
class MessageSerializer
def load(serialized_content)
data = JSON.parse(serialized_content)
parse_message(data, 1)
rescue JSON::ParserError
raise ActiveRecord::Encryption::Errors::Encoding
end
def dump(message)
raise ActiveRecord::Encryption::Errors::ForbiddenClass unless message.is_a?(ActiveRecord::Encryption::Message)
JSON.dump message_to_json(message)
end
def binary?
false
end
private
def parse_message(data, level)
validate_message_data_format(data, level)
ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
end
def validate_message_data_format(data, level)
if level > 2
raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported"
end
unless data.is_a?(Hash) && data.has_key?("p")
raise ActiveRecord::Encryption::Errors::Decryption, "Invalid data format: hash without payload"
end
end
def parse_properties(headers, level)
ActiveRecord::Encryption::Properties.new.tap do |properties|
headers&.each do |key, value|
properties[key] = value.is_a?(Hash) ? parse_message(value, level + 1) : decode_if_needed(value)
end
end
end
def message_to_json(message)
{
p: encode_if_needed(message.payload),
h: headers_to_json(message.headers)
}
end
def headers_to_json(headers)
headers.transform_values do |value|
value.is_a?(ActiveRecord::Encryption::Message) ? message_to_json(value) : encode_if_needed(value)
end
end
def encode_if_needed(value)
if value.is_a?(String)
::Base64.strict_encode64 value
else
value
end
end
def decode_if_needed(value)
if value.is_a?(String)
::Base64.strict_decode64(value)
else
value
end
rescue ArgumentError, TypeError
raise Errors::Encoding
end
end
end
end
|