jefffhaynes / BinarySerializer

A declarative serialization framework for controlling formatting of data at the byte and bit level using field bindings, converters, and code.
MIT License
289 stars 62 forks source link

How to access parent BinarySerializer inside IBinarySerializable? #158

Open sanek2k6 opened 3 years ago

sanek2k6 commented 3 years ago

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

Thank you!

jefffhaynes commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

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 commented 1 year ago

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);
   }
}