Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to access parent BinarySerializer inside IBinarySerializable? #158

Open
sanek2k6 opened this issue Sep 29, 2020 · 7 comments
Open

How to access parent BinarySerializer inside IBinarySerializable? #158

sanek2k6 opened this issue Sep 29, 2020 · 7 comments

Comments

@sanek2k6
Copy link

sanek2k6 commented Sep 29, 2020

Hello!

We receive messages where message headers are not encrypted and the payload is optionally encrypted. Whether the message is encrypted is indicated in the message header:

public class MessageHeader
{
        [FieldOrder(0)]
        public bool IsEncrypted { get; set; }

        // ...
}

public class SomeMessageContents : IMessageContents
{
        // ...
}

In order to be able to get access to the raw stream of data, the message payload is defined as an IBinarySerializable:

public class Message : IBinarySerializable
{
        [Ignore]
        public MessageHeader Header { get; set; }

        [Ignore]
        public IMessageContents Contents { get; set; }

        public void Serialize(Stream stream, Endianness endianness, BinarySerializationContext serializationContext)
        {
        
        }
        
        public void Deserialize(Stream stream, Endianness endianness, BinarySerializationContext serializationContext)
        {
                Header = serializer.Deserialize<MessageHeader>(stream);
        
                if (Header.IsEncrypted)
                {
                        using var decryptedStream = new MemoryStream();
                        Decrypt(stream, decryptedStream);
                        decryptedStream.Seek(0, SeekOrigin.Begin);
                        Contents = serializer.Deserialize<SomeMessageContents>(decryptedStream);
                }
                else
                {                       
                        Contents = serializer.Deserialize<SomeMessageContents>(stream);
                }
        }

        private IBinarySerializer serializer = //???
}

After we decrypt the raw stream, we want to use BinarySerializer to parse the decrypted contents, but we dont want to define a new BinarySerializer at this level and would rather make use of the one that initiated the deserialization operation at top-level:

        var serializer = new BinarySerializer { Endianness = Endianness.Big };        
        var message = serializer.Deserialize<Message>(messageBytes);
  • Is this the best approach in this scenario?
  • Is there any way to get access to the serializer doing the (de)serialize in an IBinarySerializable?
  • Is it possible to pass a custom context and access it inside Serialize()/Deserialize()? I saw a context being used in one of the tests, but its used through an attribute, so a bit different.

Thank you!

@sanek2k6 sanek2k6 changed the title How to access serializer inside IBinarySerializable? How to access parent BinarySerializer inside IBinarySerializable? Sep 29, 2020
@jefffhaynes
Copy link
Owner

I would recommend treating the encrypted payload as another "layer" in your protocol and using a second binary serializer. It's not really meaningful to get a reference to the parent inside a custom object since the object graph in that parent doesn't include the encrypted layer, if that makes sense. I spent a fair amount of time actually trying to incorporate the concept of layers into the serializer for use cases like this and concluded that it's really more trouble than it's worth. Out of curiosity, why don't you want to define a new serializer?

@sanek2k6
Copy link
Author

sanek2k6 commented Sep 30, 2020

I'm not sure I understand - the packet definition remains the same (and cannot change as we are implementing a spec), however the bytes for the Contents are optionally encrypted, as specified in the header. The decrypting operation itself requires a series of client-specific keys that also needs to be passed in to the Decode() function somehow - not sure how to do that here (initially hoped to do that using a custom Context, but not sure how to do that with a IBinarySerialiable).

We use Dependency Injection to inject an instance of the serializer into any code that needs it, however DI wouldnt be able to set a value inside the object being serialized. It would not be ideal to have to define it in multiple places and yes - we can use a factory instead, but the above problem with keys still exists.

Right now, the best idea we came up with is to have the Message object implement a custom interface (i.e. "IPostProcessingSerializable") - something like this using the example above:

public interface IPostProcessingSerializable
{
    void PostSerialize(object state);
    void PostDeserialize(object state);
}

public class Message : IPostProcessingSerializable
{
    [FieldOrder(0)]
    public MessageHeader Header { get; set; }

    [FieldOrder(1)]
    public Stream ContentsStream { get; set; }

    [Ignore]
    public IMessageContents Contents { get; set; }

    void PostSerialize(object state)
    {
        //...
    }

    void PostDeserialize(object state)
    {
        // state could have the serializer or use it another way
        // state could have the keys

        if (Header.IsEncrypted)
        {
            using var decryptedStream = new MemoryStream();
            Decrypt(ContentsStream, decryptedStream, keys);
            decryptedStream.Seek(0, SeekOrigin.Begin);
            Contents = serializer.Deserialize<SomeMessageContents>(decryptedStream);
        }
        else
        {                       
            Contents = serializer.Deserialize<SomeMessageContents>(ContentsStream);
        }
    }
}

We use the BinarySerializer through a wrapper class BinaryMessageSerializer that could check if the type implements the IPostProcessingSerializable interface and call PostSerialize()/PostDeserialize() to trigger processing of the stream.

@jefffhaynes
Copy link
Owner

Yeah, I'm not suggesting changing the protocol, but treating the encrypted part as a "payload" byte[] field. Then take that field, decrypt it, and process it with a serializer.

@sanek2k6
Copy link
Author

sanek2k6 commented Oct 5, 2020

Thank you for the suggestions. In the end, used a wrapper for the serializer and broke the message down in pieces, decrypting the Contents piece when necessary.

I think one helpful feature that would have made all of this much easier is to support passing in the instance of the object to set the values of, instead of creating it.

So instead of

var message = serializer.Deserialize<Message>(stream);
var message = new Message();
serializer.Deserialize(stream, message);

This would allow you to create the object in any way you liked, passing in whatever else was necessary, so serialization could make use of this, as necessary.

@jefffhaynes
Copy link
Owner

I'm not sure I understand. If you already have the message, what would be the utility in deserializing it? How would the deserializer know which fields are meant to be kept?

@sanek2k6
Copy link
Author

sanek2k6 commented Oct 6, 2020

The object can be created to contain just about anything else you like and the serializer can have the following options:

  • Ignore all fields marked with the [Ignore] attribute and handle everything else.
  • Only handle fields annotated with [FieldOrder()] attribute.
  • For IBinarySerializable, just let the object handle the serializing.

Here is an example, although a bit silly one, but just to demonstrate this:

[Flags]
public enum Color
{
	Red = 1,
	Green = 2,
	Blue = 4,
	Yellow = Red | Green,
	Purple = Red | Blue,
	Cyan = Green | Blue		
}

public abstract class Shape
{
	[Ignore]
	public Color Color { get; }

	protected Shape (Color color)
	{
		this.Color = color;
	}
}

public Circle : Shape
{
	[FieldOrder(0)]
	[SerializeAs(SerializedType.UInt1)]
	public uint Radius { get; set; }

	public Circle (Color color)
		: base(color)
	{
		
	}
}

public Rectangle : Shape, IBinarySerializable
{
	[Ignore]
	public uint Length { get; set; }

	[Ignore]
	public uint Width { get; set; }

	public Rectangle  (Color color)
		: base(color)
	{
		
	}

	public void Deserialize(Stream stream, Endianness endianness, BinarySerializationContext context)
	{
		Length = stream.ReadByte();
		Width = stream.ReadByte();
	}

	public void Serialize(Stream stream, Endianness endianness, BinarySerializationContext context)
	{
		stream.WriteByte((byte)Length);
		stream.WriteByte((byte)Width);
	}
}

And usage:

var circle = new Circle(Color.Green);
var square = new Rectangle(Color.Red);

var circleBytes = new byte[] { 0x05 };
var squareBytes = new byte[] { 0x03, 0x03 };

var serializer = new BinarySerializer();

serializer.Deserialize(circleBytes, circle);
serializer.Deserialize(squareBytes, square);

Obviously the objects can contain any information that does not get serialized like factories that can tell an IBinarySerializable how to deserialize something (i.e. if data is encrypted).

@bevanweiss
Copy link
Contributor

bevanweiss commented May 20, 2023

I think this might have been nicer to do if you used some of the SubType handling...

public class Message
{
        [FieldOrder(0)]
        public MessageHeader Header { get; set; }

        [FieldOrder(1)]
        [SubType(nameof("Header")+"."+nameof(MessageHeader.IsEncrypted), false, typeof(MessageContent))]
        [SubType(nameof("Header")+"."+nameof(MessageHeader.IsEncrypted), true, typeof(EncryptedMessageContent))]
        public IMessageContents Contents { get; set; }
        ...
}       

and then you'd have something like

public class EncryptedMessageContent : MessageContent, IBinarySerializable
{
    void Deserialize(Stream stream,....)
    {
       // do the reading in of all the encrypted bytes.. and write out the decrypted bytes to a
      // Memory stream, you can then use an instance of the BinarySerializer to deserialize the memory stream
      // back to an instance of a MessageContent class, and use a mapper (AutoMapper, or similar) to apply it back to the 
      // EncryptedMessage fields (which it inherited as a child of MessageContent)...
     
     var memStream = DecryptMessageContent(stream);
     messageContent = serializer.Deserialize(memStream);
     if (messageContent is MessageContent)
     {
       this = messageContent;
       // we might be able to reach up through the context to access the parent and set the value in the header
      //  to IsEncrypted = false now... since we've decrypted it.
     }
     else
        throw new Exception("Decryption failed");
    }

   void Serialize(Stream stream...)
   {
     // we don't really want to re-encrypt
      var messageContent = this as MessageContent;
      serializer.Serialize(stream, messageContent);
   }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants