lib/diaspora_federation/entities/relayable.rb
# frozen_string_literal: true
module DiasporaFederation
module Entities
# This is a module that defines common properties for relayable entities
# which include Like, Comment, Participation, Message, etc. Each relayable
# has a parent, identified by guid. Relayables are also signed and signing/verification
# logic is embedded into Salmon XML processing code.
module Relayable
include Signable
# Additional properties from parsed input object
# @return [Hash] additional elements
attr_reader :additional_data
# On inclusion of this module the required properties for a relayable are added to the object that includes it.
#
# @!attribute [r] author
# The diaspora* ID of the author
# @see Person#author
# @return [String] diaspora* ID
#
# @!attribute [r] guid
# A random string of at least 16 chars
# @see Validation::Rule::Guid
# @return [String] comment guid
#
# @!attribute [r] parent_guid
# @see StatusMessage#guid
# @return [String] parent guid
#
# @!attribute [r] author_signature
# Contains a signature of the entity using the private key of the author of a relayable itself.
# The presence of this signature is mandatory. Without it the entity won't be accepted by
# a target pod.
# @return [String] author signature
#
# @!attribute [r] parent
# Meta information about the parent object
# @return [RelatedEntity] parent entity
#
# @param [Entity] klass the entity in which it is included
def self.included(klass)
klass.class_eval do
property :author, :string
property :guid, :string
property :parent_guid, :string
property :author_signature, :string, default: nil
entity :parent, Entities::RelatedEntity
end
klass.extend Parsing
end
# Initializes a new relayable Entity with order and additional xml elements
#
# @param [Hash] data entity data
# @param [Array] signature_order order for the signature
# @param [Hash] additional_data additional xml elements
# @see DiasporaFederation::Entity#initialize
def initialize(data, signature_order=nil, additional_data={})
self.signature_order = signature_order if signature_order
self.additional_data = additional_data
super(data)
end
# Verifies the +author_signature+ if needed.
# @see DiasporaFederation::Entities::Signable#verify_signature
#
# @raise [SignatureVerificationFailed] if the signature is not valid
# @raise [PublicKeyNotFound] if no public key is found
def verify_signature
super(author, :author_signature) unless author == parent.root.author
end
def sender_valid?(sender)
(sender == author && parent.root.local) || sender == parent.root.author
end
# @return [String] string representation of this object
def to_s
"#{super}#{":#{parent_type}" if respond_to?(:parent_type)}:#{parent_guid}"
end
def to_json(*_args)
super.merge!(property_order: signature_order).tap {|json_hash|
missing_properties = json_hash[:property_order] - json_hash[:entity_data].keys
missing_properties.each {|property|
json_hash[:entity_data][property] = nil
}
}
end
# The order for signing
# @return [Array]
def signature_order
@signature_order || (self.class.class_props.keys.reject {|key|
self.class.optional_props.include?(key) && public_send(key).nil?
} - %i[author_signature parent])
end
private
# Sign with author key
# @raise [AuthorPrivateKeyNotFound] if the author private key is not found
# @return [String] A Base64 encoded signature of #signature_data with key
def sign_with_author
privkey = DiasporaFederation.callbacks.trigger(:fetch_private_key, author)
raise AuthorPrivateKeyNotFound, "author=#{author} obj=#{self}" if privkey.nil?
sign_with_key(privkey).tap do
logger.info "event=sign status=complete signature=author_signature author=#{author} obj=#{self}"
end
end
# Update the signatures with the keys of the author and the parent
# if the signatures are not there yet and if the keys are available.
#
# @return [Hash] properties with updated signatures
def enriched_properties
super.merge(additional_data).tap do |hash|
hash[:author_signature] = author_signature || sign_with_author unless author == parent.root.author
end
end
# Sort all XML elements according to the order used for the signatures.
#
# @return [Hash] sorted xml elements
def xml_elements
data = super
order = signature_order
order += %i[author_signature] unless author == parent.root.author
order.to_h {|element| [element, data[element].to_s] }
end
def signature_order=(order)
prop_names = self.class.class_props.keys.map(&:to_s)
@signature_order = order.grep_v(/signature/)
.map {|name| prop_names.include?(name) ? name.to_sym : name }
end
def additional_data=(additional_data)
@additional_data = additional_data.reject {|name, _| name =~ /signature/ }
end
# @return [String] signature data string
def signature_data
data = normalized_properties.merge(additional_data)
signature_order.map {|name| data[name] }.join(";")
end
# Override class methods from {Entity} to parse serialized data
module Parsing
# Does the same job as Entity.from_hash except of the following differences:
# 1) unknown properties from the properties_hash are stored to additional_data of the relayable instance
# 2) parent entity fetch is attempted
# 3) signatures verification is performed; property_order is used as the order in which properties are composed
# to compute signatures
# 4) unknown properties' keys must be of String type
#
# @see Entity.from_hash
def from_hash(properties_hash, property_order)
# Use all known properties to build the Entity (entity_data). All additional elements
# are respected and attached to a hash as string (additional_data). This is needed
# to support receiving objects from the future versions of diaspora*, where new elements may have been added.
additional_data = properties_hash.reject {|key, _| class_props.has_key?(key) }
fetch_parent(properties_hash)
new(properties_hash, property_order, additional_data).tap(&:verify_signature)
end
private
def fetch_parent(data)
type = data.fetch(:parent_type) {
break self::PARENT_TYPE if const_defined?(:PARENT_TYPE)
raise DiasporaFederation::Entity::ValidationError, error_message_missing_property(data, "parent_type")
}
guid = data.fetch(:parent_guid) {
raise DiasporaFederation::Entity::ValidationError, error_message_missing_property(data, "parent_guid")
}
data[:parent] = RelatedEntity.fetch(data[:author], type, guid)
end
def error_message_missing_property(data, missing_property)
obj_str = "#{class_name}#{":#{data[:guid]}" if data.has_key?(:guid)}" \
"#{" from #{data[:author]}" if data.has_key?(:author)}"
"Invalid #{obj_str}! Missing '#{missing_property}'."
end
def xml_parser_class
DiasporaFederation::Parsers::RelayableXmlParser
end
def json_parser_class
DiasporaFederation::Parsers::RelayableJsonParser
end
end
# Raised, if creating the author_signature fails, because the private key was not found
class AuthorPrivateKeyNotFound < RuntimeError
end
end
end
end