I am having a bit of a problem deserialising JSON that I am sending to an azure function. Firstly I intend to send an array of Ciphertext types with post to azure, deserialise the JSON to restore my Data and then operate on this Data. My class as shown below is called sampleClass
and this has an attribute ciphertext
of type Ciphertext
:
[DataContract]
public class sampleClass
{
[DataMember]
public Ciphertext ciphertext { get; set; }
[JsonConstructor]
public sampleClass() { }
}
This is the class I am trying to Serialise/Deserialise to.
To post the Data I am using HttpClient and I am posting it as JSON as shown:
HttpResponseMessage response = await client.PostAsJsonAsync("api/Function1", cipher);
In my azure function I am trying to read in the Json as a stream and the Deserialise it to sampleClass[], However this is throwing me an error.
//Receive data from The Http PostRequest.
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
//De serialises to an object.
sampleClass[] array = JsonConvert.DeserializeObject<sampleClass[]>(requestBody);
The error that gets thrown is shown below:
Executed 'Function1' (Failed, Id=1be7633e-6b6a-4626-98b7-8fec98eac633) [11/02/2020 15:50:48] System.Private.CoreLib: Exception while executing function: Function1. Newtonsoft.Json: Unable to find a constructor to use for type Microsoft.Research.SEAL.Ciphertext. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path '[0].ciphertext.CoeffModCount', line 1, position 32.
This error is thrown when I attempt to deserialise my JSON, how can I fix this?
You have several problems here. Let's take them in order.
Firstly, the type Microsoft.Research.SEAL.Ciphertext
has neither a parameterless constructor nor single parameterized constructor, as can be seen from the reference source:
public class Ciphertext : NativeObject
{
public Ciphertext(MemoryPoolHandle pool = null)
{
// Contents omitted
}
public Ciphertext(SEALContext context, MemoryPoolHandle pool = null)
{
// Contents omitted
}
// Additional constructors, methods and members omitted.
The first constructor's parameter is optional, but that doesn't mean it's parameterless, it just means that the compiler supplies the value when it's not present in the code. But when the constructor is invoked via reflection (which is what Json.NET does) it's still necessary to provide a value; see Reflection - Call constructor with parameters for details. The lack of a true parameterless constructor for this type is what causes the Newtonsoft.Json: Unable to find a constructor to use for type Microsoft.Research.SEAL.Ciphertext. exception to be thrown.
(In comments it was stated that your problem was that sampleClass
lacked a default constructor, but that comment was wrong.)
Since you can't modify Ciphertext
a standard way to provide your own construction method is to use CustomCreationConverter<T>
like so:
public class CiphertextConverter : CustomCreationConverter<Ciphertext>
{
public override Ciphertext Create(Type objectType)
{
return new Ciphertext(); // Use the default value for the optional parameter
}
}
And then do:
var settings = new JsonSerializerSettings
{
Converters = { new CiphertextConverter() },
};
var array = JsonConvert.DeserializeObject<sampleClass []>(requestBody, settings);
However, this does not work, which is your next problem. Since most of the public properties of Ciphertext
are read-only, the type cannot be deserialized from them.
Failing demo fiddle #1 here.
So, what to do? As it turns out, Ciphertext
has two methods
public long Save(Stream stream, ComprModeType? comprMode = null)
public long Load(SEALContext context, Stream stream)
These would appear to allow us to serialize a Ciphertext
to a MemoryStream
, then subsequently insert the contents into JSON as a Base64 string using a converter such as the following:
public class CiphertextConverter : JsonConverter<Ciphertext>
{
readonly SEALContext context;
public CiphertextConverter(SEALContext context) => this.context = context ?? throw new ArgumentNullException(nameof(context));
public override Ciphertext ReadJson(JsonReader reader, Type objectType, Ciphertext existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var data = serializer.Deserialize<byte []>(reader);
if (data == null)
return null;
var cipherText = new Ciphertext();
using (var stream = new MemoryStream(data))
cipherText.Load(context, stream);
return cipherText;
}
public override void WriteJson(JsonWriter writer, Ciphertext value, JsonSerializer serializer)
{
using (var stream = new MemoryStream())
{
value.Save(stream, ComprModeType.Deflate); // TODO: test to see whether Deflate gives better size vs speed performance in practice.
writer.WriteValue(stream.ToArray());
}
}
}
And then use the converter during serialization and deserialization as follows:
var settings = new JsonSerializerSettings
{
Converters = { new CiphertextConverter(GlobalContext.Context) },
};
var array = JsonConvert.DeserializeObject<sampleClass []>(requestBody, settings);
But wait -- what's this object GlobalContext.Context
? This brings us to your you third problem, namely that you need compatible SEALContext
objects on both the client and server side to pass a Ciphertext
between then via serialization. Now, looking through the Cloud Functions Demo demo app, this seems to be a correct assumption, as this app does have compatible contexts on both the client and server side:
So I'm going to assume that you do also. Given that you do, the converter above should be used for both serialization and deserialization.
For testing purposes I adapted the test method CiphertextTests.SaveLoadTest()
and the class GlobalContext
from https://github.com/microsoft/SEAL/tree/master/dotnet/tests to create the following test harness:
public class TestClass
{
[TestMethod]
public void JsonNetSaveLoadTest()
{
Debug.WriteLine("Testing Json.NET");
Func<Ciphertext, SEALContext, Ciphertext> roundtrip = (cipher, context) =>
{
var clientArray = new [] { new sampleClass { ciphertext = cipher } };
var settings = new JsonSerializerSettings
{
Converters = { new CiphertextConverter(GlobalContext.Context) },
};
var requestBody = JsonConvert.SerializeObject(clientArray, settings);
Debug.Write(" ");
Debug.WriteLine(requestBody);
Debug.WriteLine(" requestBody.Length={0}", requestBody.Length);
var array = JsonConvert.DeserializeObject<sampleClass []>(requestBody, settings);
Assert.IsTrue(array.Length == clientArray.Length);
var reserializedJson = JsonConvert.SerializeObject(array, settings);
Debug.Write(" ");
Debug.WriteLine(reserializedJson);
Assert.IsTrue(requestBody == reserializedJson);
return array[0].ciphertext;
};
SaveLoadTest(roundtrip);
Console.WriteLine(" passed.");
}
// Adapted from https://github.com/microsoft/SEAL/blob/master/dotnet/tests/CiphertextTests.cs#L113
[TestMethod]
public void DirectSaveLoadTest()
{
Debug.WriteLine("Testing direct save and load:");
Func<Ciphertext, SEALContext, Ciphertext> roundtrip = (cipher, context) =>
{
Ciphertext loaded = new Ciphertext();
Assert.AreEqual(0ul, loaded.Size);
Assert.AreEqual(0ul, loaded.PolyModulusDegree);
Assert.AreEqual(0ul, loaded.CoeffModCount);
using (MemoryStream mem = new MemoryStream())
{
cipher.Save(mem);
mem.Seek(offset: 0, loc: SeekOrigin.Begin);
loaded.Load(context, mem);
}
return loaded;
};
SaveLoadTest(roundtrip);
Debug.WriteLine(" passed.");
}
// Adapted from https://github.com/microsoft/SEAL/blob/master/dotnet/tests/CiphertextTests.cs#L113
static void SaveLoadTest(Func<Ciphertext, SEALContext, Ciphertext> roundtrip)
{
SEALContext context = GlobalContext.Context;
KeyGenerator keygen = new KeyGenerator(context);
Encryptor encryptor = new Encryptor(context, keygen.PublicKey);
Plaintext plain = new Plaintext("2x^3 + 4x^2 + 5x^1 + 6");
Ciphertext cipher = new Ciphertext();
encryptor.Encrypt(plain, cipher);
Assert.AreEqual(2ul, cipher.Size);
Assert.AreEqual(8192ul, cipher.PolyModulusDegree);
Assert.AreEqual(4ul, cipher.CoeffModCount);
var loaded = roundtrip(cipher, context);
Assert.AreEqual(2ul, loaded.Size);
Assert.AreEqual(8192ul, loaded.PolyModulusDegree);
Assert.AreEqual(4ul, loaded.CoeffModCount);
Assert.IsTrue(ValCheck.IsValidFor(loaded, context));
ulong ulongCount = cipher.Size * cipher.PolyModulusDegree * cipher.CoeffModCount;
for (ulong i = 0; i < ulongCount; i++)
{
Assert.AreEqual(cipher[i], loaded[i]);
}
}
}
static class GlobalContext
{
// Copied from https://github.com/microsoft/SEAL/blob/master/dotnet/tests/GlobalContext.cs
static GlobalContext()
{
EncryptionParameters encParams = new EncryptionParameters(SchemeType.BFV)
{
PolyModulusDegree = 8192,
CoeffModulus = CoeffModulus.BFVDefault(polyModulusDegree: 8192)
};
encParams.SetPlainModulus(65537ul);
BFVContext = new SEALContext(encParams);
encParams = new EncryptionParameters(SchemeType.CKKS)
{
PolyModulusDegree = 8192,
CoeffModulus = CoeffModulus.BFVDefault(polyModulusDegree: 8192)
};
CKKSContext = new SEALContext(encParams);
}
public static SEALContext BFVContext { get; private set; } = null;
public static SEALContext CKKSContext { get; private set; } = null;
public static SEALContext Context => BFVContext;
}
Working demo fiddle #2 here.
Notes:
As long as it is public, there is no need to mark the parameterless constructor of sampleClass
with [JsonConstructor]
.
From testing, the Base64 strings generated for Ciphertext
seem to be quite long, and roughly .5 MB per Ciphertext
. Because Json.NET fully materializes each string during parsing, it isn't really efficient at handling such huge strings. You will need to re-evaluate your architecture if you exceed the maximum effective string length or experience large object heap fragmentation.
I am not a security professional. I can't tell you whether sending a serialized Ciphertext
over the wire might leak information. Nor can I advise you on how to choose an appropriate SEALContext
for your application -- or even whether having compatible contexts on the client and server side might leak information. This answer only explains how to serialize a specific SEAL object via Json.NET.