hardbyte/Paillier.jl

Encoding should be a parametric type to allow multiple dispatch

hardbyte opened this issue · 0 comments

I've been thinking about how Paillier.jl composes with other parts of the Julia ecosystem, in particular the fact that users should be able to encode and encrypt/decrypt user defined types easily.

Currently using the library with Measurement.jl numbers is messy. One method would be to create an array of both the value and the error from the measurement object:

> encoding = Encoding(Float64, publickey)
> Paillier.encode_and_encrypt(plaintext::Measurement, encoding::Encoding) = Paillier.encode_and_encrypt([plaintext.val, plaintext.err], encoding)
> encrypted_g_with_uncertainty = encode_and_encrypt(9.79 ± 0.02, encoding);
> decrypt_and_decode(privatekey, encrypted_g_with_uncertainty)
2-element Array{Float16,1}:
 9.79
 0.02

However as the encoding doesn't include the Measurement type we can't nicely reconstruct a Measurement object after decryption and decoding.

Creating an encoding of a Measurement doesn't work either:

> measurementencoding = Encoding(Measurement{Float64}, publickey)
> encrypted_g_2 = encode_and_encrypt(9.79 ± 0.02, measurementencoding)
MethodError: no method matching precision(::Type{Measurement{Float64}})
Closest candidates are:
  precision(!Matched::BigFloat) at mpfr.jl:805
  precision(!Matched::Type{Float16}) at float.jl:579
  precision(!Matched::Type{Float32}) at float.jl:580
  ...

Stacktrace:
 [1] encode(::Float64, ::Encoding) at src\Encoding.jl:200

As Julia tries to ascertain the precision of a Measurement within the encode method.

Now while we could probably hack the precision method to unwrap the measurement object, there is still a question of how to encode the measurement into one or more integers suitable for encryption. A better strategy for handling encoding of arbitrary user defined types might be to have users override encode for their user defined Encoding type.

So instead of Encoding being defined as:

struct Encoding
    datatype::DataType
    public_key::PublicKey
    base::Int64
end

Perhaps it should be a parametric type:

struct Encoding{T}
    public_key::PublicKey
    base::Int64
end

# Example use:
encoding = Encoding{Measurement{Float64}}(publickey, 16)

# Then a user can specialize the `encode` and `decode` methods for their data type. E.g.:
Paillier.encode(scalar::Measurement{Float64}, encoding::Encoding{Measurement{T}}) where T = Paillier.encode(
    [scalar.val, scalar.err], 
    Encoding{T}(encoding.public_key, encoding.base)
)

# decode would need more work as decrypt_and_decode calls decode for each element
# of an encrypted array and not with an array of encodings. But that refactoring seems sensible 
# at first glance. Which for our example of encoding a Measurement as an array of encrypted numbers give something like this for the decode method:
decode(encoded::Array{BigInt}, exponent, encoding::Encoding{Measurement{T}}) where T = 
    measurement(decode(encoded[1], exponent, T), decode(encoded[2], exponent, T))