Make a secret or black box function?

isn’t this approach too brittle? string(Char.(funvec)...) gives away the function with minor tinkering by a curious student.

1 Like

It’s certainly not the best conceivable algorithm.

Note that with the IRTools or the JLD2 solutions (or really anything that gives you a pure Julia function), they’re a @code_warntype away from seeing (a partially obfuscated version of) the source of the top-level function call:

# with IRTools solution from above
julia> @code_warntype g(nothing,2,1)

Body::Float64
1 ─ %1 = Core.apply_type(Base.Val, 2)::Core.Const(Val{2})
│   %2 = (%1)()::Core.Const(Val{2}())
│   %3 = Base.literal_pow(Main.:^, @_3, %2)::Int64
│   %4 = Main.sin(@_4)::Float64
│   %5 = (%3 + %4)::Float64
└──      return %5

You can fix that by just wrapping the function in a closure, e.g.

julia> f = (x,y) -> (() -> x^2 + sin(y))();

At that point, you could just as well use the built-in Serialization to save the function:

julia> serialize("secret_function.jls", f);

julia> g = deserialize("secret_function.jls");

julia> @code_warntype g(1,2)

Body::Float64
1 ─ %1 = Serialization.__deserialized_types__.:(var"#14#16")::Core.Const(Serialization.__deserialized_types__.var"#14#16")
│   %2 = Core.typeof(x)::Core.Const(Int64)
│   %3 = Core.typeof(y)::Core.Const(Int64)
│   %4 = Core.apply_type(%1, %2, %3)::Core.Const(Serialization.__deserialized_types__.var"#14#16"{Int64, Int64})
│        (#14 = %new(%4, x, y))
│   %6 = #14::Serialization.__deserialized_types__.var"#14#16"{Int64, Int64}
│   %7 = (%6)()::Float64
└──      return %7

julia> g(1,2)
1.9092974268256817

The contents of the file is pretty unreadable (though they may see that e.g. sin is used in someway, but doubt anyone can easily tell how):

7JL
43#13#15"XN�D""MLL�#13#�#NMainD#13aREPL[4]�SN�D3,FF9!#self#xy#14�LeF�63#14#16"}~,"}GF~GF",	,
LLL�#14#�#aNMainD#14aREPL[4]�SN�D3,!a#self#�LeF�V$N�D�%�}V$N�D�$N�DVal�V(�V$N�Dliteral_pow$NMainD^(�(�V$N�D�%�~V$NMainDsin(�V$NMainD�(�(�:(����NF�4LineInfoNodeN�DNMainD#14aREPL[4]����NFNN		��������LLLLN�)V$N�Dtypeof%�V$N�Dtypeof%�V$N�D�(�(�(�Y%��(�%�%�%�V(�:(����NF�4,eNMainD#13aREPL[4]���}~#14�NFNN		��������LLLLN�)

Although even still, the function is technically reconstructable in Julia if they @code_warntype into the deserialized closure, etc… Only way to make this Julia-proof is probably provide a dynamic library for a C compiled function.

8 Likes

AFAIK, Serialisation may not portable enough as the proper recovery cannot be guaranteed from a machine to another right ?

2 Likes

I’m not an expert but machine-to-machine should be OK as long as the student’s Julia version is the same or newer-ish than the version that created the file. I say newer"-ish" bc you’re right its good to keep in mind Serializion has no backwards compatibility guarantees and is definitely not meant for long-term data storage, but maybe is fine here, e.g. just checked the file above created on 1.5 is loadable on 1.6.

This gives me an idea: approximate with (tensored) Chebyshev polynomials, drop high-degree cross terms (eg like Smolyak/sparse bases), then emit code with hardcoded coefficients evaluating a polynomial, summing in random order.

1 Like

@PeterSimon
I really liked your suggestion. Even without the obfuscation, most students won’t know how to get at the function definition.

I tried this approach (without using the obfuscation middle-man function). I just tried saving and then reloading the function. But unfortunately, it didn’t work:

julia> @load "Devious_Function.jld2"
┌ Warning: type Main.#f does not exist in workspace; reconstructing
└ @ JLD2 ~/.julia/packages/JLD2/qncOK/src/data/reconstructing_datatypes.jl:358
1-element Array{Symbol,1}:
 :f

julia> f(rand(14))
ERROR: MethodError: objects of type JLD2.ReconstructedTypes.var"##Main.#f#253" are not callable

I defined the function with a random vector that was previous defined:

f(x)=-exp(-sum(1.0.-abs.(x.-x0))^2/100)

x0 was defined from x0=rand(14) before the function definition. (And subtracting 1 was just a way of obfuscating a bit…) I wondered if defining the function with a predefined constant vector was a problem…but trying another function with an explicit definition (no previously defined constants) didn’t help.

So I guess I don’t know how to save and then recall a function and have it still be callable.

Anyone have any hints?

You can’t save and then recall a function type to a JLD2 file, unfortunately. You can skip the obfuscation part of my procedure, but you will need to put the function definition inside a string, as I did. I tried doing it this way (without the obfuscation part) first, but the string contents were visible when the file was opened in an editor (even after turning on compression for the JLD2 file), hence the need for the obfuscation function.

@marius311 showed a better approach in his post. It’s probably what you’re looking for. If that isn’t clear to you, ask and one of us responders will provide another explicit example.

1 Like

@marius311
I’ve been playing around with your idea a bit. I was under the impression, apparently incorrectly, that I could just read in this secret_function.jls file in another session of julia and that the saved function would still be defined, but it seems that the function that I read in (via deserialization) is only defined in the same session. It is still making a reference to the orginal, somehow. (I was hoping to share the .jls file only with the students and let them load it in…).

julia> using Serialization

julia> f(x) = -exp(-5*sum((x.-1.0.+[0.24197  0.15722  0.20821  0.86427  0.6547  0.85627  0.58583  0.90464  0.23778  0.38811  0.0803  0.31662  0.57585  0.71083][[2;6;13;8;10;11;7;3;4;1;5;12;14;9]]).^2))
f (generic function with 1 method)

julia> serialize("secret_function.jls",f)

julia> g = deserialize("secret_function.jls")
f (generic function with 1 method)

julia> g(rand(14))
-1.6990329706087957e-5

julia> exit()
➜  ~/.julia/dev julia #start new session of julia
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.0 (2021-03-24)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> using Serialization

julia> g = deserialize("secret_function.jls")
ERROR: UndefVarError: #f not defined
Stacktrace:
...

Am I misunderstanding something about your suggestion?

After I posted, I had one more idea—maybe the closure matters? So I tried the closure before serializing and that works! Yah. Thank you.

julia> f = x -> (() -> -exp(-5*sum((x.-1.0.+[0.24197  0.15722  0.20821  0.86427  0.6547  0.85627  0.58583  0.90464  0.23778  0.38811  0.0803  0.31662  0.57585  0.71083][[2;6;13;8;10;11;7;3;4;1;5;12;14;9]]).^2)))()
#5 (generic function with 1 method)

julia> serialize("secret_function.jls",f)

julia> exit()
➜  ~/.julia/dev julia           
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.6.0 (2021-03-24)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> using Serialization

julia> f = deserialize("secret_function.jls")
#5 (generic function with 1 method)

julia> f(rand(14))
-4.564053334390881e-5
3 Likes