1 /// Wrappers for thread and side-channel [mitigation] behavior.
2 module tern.typecons.security;
3 
4 public import core.atomic;
5 import tern.traits;
6 import tern.meta;
7 import tern.typecons;
8 import core.sync.mutex;
9 import core.thread;
10 import core.builtins;
11 import std.conv;
12 import std.array;
13 
14 public alias a8 = shared Atomic!ubyte;
15 public alias a16 = shared Atomic!ushort;
16 public alias a32 = shared Atomic!uint;
17 public alias a64 = shared Atomic!ulong;
18 public alias af16 = shared Atomic!float;
19 public alias af32 = shared Atomic!double;
20 
21 public alias b8 = Blind!ubyte;
22 public alias b16 = Blind!ushort;
23 public alias b32 = Blind!uint;
24 public alias b64 = Blind!ulong;
25 
26 /**
27  * Wraps `T` to make every operation atomic, if possible.
28  *
29  * Remarks:
30  *  `opOpAssign` is not supported for fields of `T`.
31  */
32  // There are some shenanigans with operator overloading that seems to cause a segfault,
33  // not sure how to make a good check for this, so for safety mutexes are sometimes used instead of
34  // `core.atomic` if `T` isn't a scalar.
35 public struct Atomic(T, MemoryOrder M = MemoryOrder.seq)
36 {
37     shared T value;
38     alias value this;
39 
40 public:
41 final:
42     shared Mutex mutex;
43 
44     this(T val)
45     {
46         value = cast(shared(T))val;
47     }
48 
49     auto opAssign(A)(A ahs) shared
50     {
51         static if (isScalar!T)
52             value.atomicStore!M(ahs);
53         else
54         {
55             mixin("if (mutex is null)
56                     mutex = new shared Mutex();
57                 mutex.lock();
58                 scope (exit) mutex.unlock();
59                 value = cast(shared(T))ahs;");
60         }
61         return this;
62     }
63 
64     A opCast(A)() const shared
65     {
66         return Atomic!(A, M)(cast(shared(A))value);
67     }
68 
69     auto opUnary(string op)() shared
70     {
71         static if (op == "--" || op == "++")
72             return opOpAssign!(op[0..1])(1);
73         else static if (op == "*")
74         {
75             if (mutex is null)
76                 mutex = new shared Mutex();
77             mutex.lock();
78             scope (exit) mutex.unlock();
79             return *value;
80         }
81         else static if (isScalar!T)
82             return mixin("Atomic!(T, M)("~op~"value.atomicLoad!M())");
83         else
84         {
85             mixin("if (mutex is null)
86                     mutex = new shared Mutex();
87                 mutex.lock();
88                 scope (exit) mutex.unlock();
89                 return Atomic!(T, M)(cast(shared(T))("~op~"value));");
90         }
91     }
92 
93     static if (isScalar!T)
94     auto opEquals(A)(A ahs) const
95     {
96         return mixin("value.atomicLoad!M() == ahs");
97     }
98 
99     static if (isScalar!T)
100     auto opEquals(A)(A ahs) const shared
101     {
102         return mixin("value.atomicLoad!M() == ahs");
103     }
104 
105     static if (!isScalar!T)
106     auto opEquals(A)(A ahs)
107     {
108         if (mutex is null)
109             mutex = new shared Mutex();
110 
111         mutex.lock();
112         scope (exit) mutex.unlock();
113         return value == ahs;
114     }
115 
116     static if (!isScalar!T)
117     auto opEquals(A)(A ahs) shared
118     {
119         if (mutex is null)
120             mutex = new shared Mutex();
121             
122         mutex.lock();
123         scope (exit) mutex.unlock();
124         return value == ahs;
125     }
126 
127     static if (isScalar!T)
128     int opCmp(R)(const R other) const
129     {
130         return cast(int)(value.atomicLoad() - other);
131     }
132 
133     static if (isScalar!T)
134     int opCmp(R)(const R other) const shared
135     {
136         return cast(int)(value.atomicLoad() - other);
137     }
138 
139     static if (!isScalar!T)
140     int opCmp(A)(const A ahs)
141     {
142         if (mutex is null)
143             mutex = new shared Mutex();
144             
145         mutex.lock();
146         scope (exit) mutex.unlock();
147         return value.opCmp(ahs);
148     }
149 
150     static if (!isScalar!T)
151     int opCmp(A)(const A ahs) shared
152     {
153         if (mutex is null)
154             mutex = new shared Mutex();
155 
156         mutex.lock();
157         scope (exit) mutex.unlock();
158         return value.opCmp(ahs);
159     }
160 
161     public auto opOpAssign(string op, R)(R rhs) shared
162     {
163         static if (isScalar!T)
164             return value.atomicOp!(op~'=')(cast(shared(T))rhs);
165         else static if (op == "~")
166         {
167             if (mutex is null)
168                     mutex = new shared Mutex();
169                     
170             mutex.lock();
171             scope (exit) mutex.unlock();
172             return value ~= rhs;
173         }
174         else
175         {
176             mixin("if (mutex is null)
177                     mutex = new shared Mutex();
178                 mutex.lock();
179                 scope (exit) mutex.unlock();
180                 return Atomic!(T, M)(cast(shared(T))(value "~op~" rhs));");
181         }
182     }
183 
184     auto opBinary(string op, R)(const R rhs) shared
185     {
186         static if (isScalar!T)
187             return mixin("Atomic!(T, M)(value.atomicLoad!M() "~op~" rhs)");
188         else
189         {
190             mixin("if (mutex is null)
191                     mutex = new shared Mutex();
192                 mutex.lock();
193                 scope (exit) mutex.unlock();
194                 return Atomic!(T, M)(cast(shared(T))(value "~op~" rhs));");
195         }
196     }
197 
198     auto opBinaryRight(string op, L)(const L lhs) shared
199     {
200         static if (isScalar!T)
201             return mixin("Atomic!(T, M)(cast(shared(T))(lhs "~op~" value.atomicLoad!M()))");
202         else
203         {
204             mixin("if (mutex is null)
205                     mutex = new shared Mutex();
206                 mutex.lock();
207                 scope (exit) mutex.unlock();
208                 return Atomic!(T, M)(cast(shared(T))(lhs "~op~" value));");
209         }
210     }
211 
212     static if (__traits(compiles, { return value[index]; }))
213     ref auto opIndex(size_t index) shared
214     {
215         mixin("if (mutex is null)
216                 mutex = new shared Mutex();
217             mutex.lock();
218             scope (exit) mutex.unlock();
219             return value[index];");
220     }
221 
222     static if (__traits(compiles, { return value[index]; }))
223     auto opIndexAssign(A)(A ahs, size_t index) shared
224     {
225         mixin("if (mutex is null)
226                 mutex = new shared Mutex();
227             mutex.lock();
228             scope (exit) mutex.unlock();
229             return value[index] = ahs;");
230     }
231 
232     static if (__traits(compiles, { return value[index]; }))
233     auto opIndexOpAssign(string op, A)(A ahs, size_t index) shared
234     {
235         mixin("if (mutex is null)
236                 mutex = new shared Mutex();
237             mutex.lock();
238             scope (exit) mutex.unlock();
239             return value[index] "~op~"= ahs;");
240     }
241 
242     static if (__traits(compiles, { return value[index]; }))
243     auto opIndexUnary(string op)(size_t index) shared
244     {
245         mixin("if (mutex is null)
246                 mutex = new shared Mutex();
247             mutex.lock();
248             scope (exit) mutex.unlock();
249             return "~op~"value[index];");
250     }
251 
252     static if (__traits(compiles, { return value[index]; }))
253     auto opSlice(size_t start, size_t end) shared
254     {
255         mixin("if (mutex is null)
256                 mutex = new shared Mutex();
257             mutex.lock();
258             scope (exit) mutex.unlock();
259             return value[start..end];");
260     }
261 
262     static if (__traits(compiles, { return value[index]; }))
263     auto opSliceAssign(A)(A ahs, size_t start, size_t end) shared
264     {
265         mixin("if (mutex is null)
266                 mutex = new shared Mutex();
267             mutex.lock();
268             scope (exit) mutex.unlock();
269             return value[start..end] = ahs;");
270     }
271 
272     static if (__traits(compiles, { return value[index]; }))
273     auto opSlice(size_t DIM : 0)(size_t start, size_t end) shared
274     {
275         mixin("if (mutex is null)
276                 mutex = new shared Mutex();
277             mutex.lock();
278             scope (exit) mutex.unlock();
279             return value[DIM][start..end];");
280     }
281 
282     static if (__traits(compiles, { return value[index]; }))
283     auto opSliceOpAssign(string op, A)(A ahs, size_t start, size_t end) shared
284     {
285         mixin("if (mutex is null)
286                 mutex = new shared Mutex();
287             mutex.lock();
288             scope (exit) mutex.unlock();
289             return value[start..end] "~op~"= ahs;");
290     }
291 
292     static if (__traits(compiles, { return value[index]; }))
293     auto opSliceUnary(string op)(size_t start, size_t end) shared
294     {
295         mixin("if (mutex is null)
296                 mutex = new shared Mutex();
297             mutex.lock();
298             scope (exit) mutex.unlock();
299             return "~op~"value[start..end];");
300     }
301 
302     static if (__traits(compiles, { return value[index]; }))
303     size_t opDollar() shared
304     {
305         mixin("if (mutex is null)
306                 mutex = new shared Mutex();
307             mutex.lock();
308             scope (exit) mutex.unlock();
309             return value.length;");
310     }
311 
312     static if (__traits(compiles, { return value[index]; }))
313     size_t opDollar(size_t DIM : 0)() 
314     {
315         mixin("if (mutex is null)
316                 mutex = new shared Mutex();
317             mutex.lock();
318             scope (exit) mutex.unlock();
319             return value[DIM].length;");
320     }
321 
322     template opDispatch(string member) 
323     {
324         template opDispatch(TARGS...) 
325         {
326             auto opDispatch(ARGS...)(ARGS args) shared
327             {
328                 static if (seqContains!(member, Functions!T) || 
329                     __traits(compiles, { mixin("return value.atomicLoad!M()."~member~"!TARGS(args);"); }) ||
330                     (__traits(compiles, { mixin("return value.atomicLoad!M()."~member~';'); }) && ARGS.length == 0) ||
331                     !__traits(compiles, { mixin("return value."~member~" = args[0];"); }))
332                 {
333                     static if (TARGS.length == 0 && ARGS.length == 0)
334                     {
335                         mixin("if (mutex is null)
336                                 mutex = new shared Mutex();
337                             mutex.lock();
338                             scope (exit) mutex.unlock();
339                             return value."~member~";");
340                     }
341                     else
342                     {
343                         mixin("if (mutex is null)
344                                 mutex = new shared Mutex();
345                             mutex.lock();
346                             scope (exit) mutex.unlock();
347                             return value."~member~"!TARGS(args);");
348                     }
349                 }
350                 else
351                 {
352                     mixin("if (mutex is null)
353                             mutex = new shared Mutex();
354                         mutex.lock();
355                         scope (exit) mutex.unlock();
356                         return value."~member~" = args[0];");
357                 }
358             }
359         }
360     }
361 
362     string toString() const shared
363     {
364         return to!string(value);
365     }
366 }
367 
368 /// Helper function for creating an atomic
369 shared(Atomic!T) atomic(T)(T val)
370 {
371     return Atomic!T(val);
372 }
373 
374 /**
375  * Prevents timing and power side channel attacks by obfuscating the processing of `T`.
376  *
377  * Remarks:
378  *  - This obviously has performance impacts and is designed to be used in cryptography.
379  *  - Only supports integral types for simplicity.
380  */
381 public struct Blind(T)
382     if (isIntegral!T)
383 {
384     T value;
385     alias value this;
386 
387 public:
388 final:
389     ulong state = uint.max;
390 
391     this(T val)
392     {
393         value = val;
394     }
395 
396     ulong numNextOps()
397     {
398         ulong t = state;
399         ulong ops = 4;
400         while (t % 32 != 0)
401         {
402             ops++;
403             t -= 3;
404         }
405         return ops;
406     }
407 
408     void obscure()
409     {
410         while (state % 32 != 0)
411             state -= 3;
412 
413         state ^= state >> 12;
414         state ^= state << 25;
415         state ^= state >> 27;
416         state *= (0x2545F4914F6CDD1D ^ value);
417     }
418 
419     auto opAssign(A)(A ahs)
420     {
421         scope (exit) obscure();
422         value = ahs;
423         return this;
424     }
425 
426     auto opAssign(A)(A ahs) shared
427     {
428         scope (exit) obscure();
429         value = ahs;
430         return this;
431     }
432 
433     auto opOpAssign(string op, A)(A ahs)
434     {
435         scope (exit) obscure();
436         mixin("value "~op~"= ahs;");
437         return this;
438     }
439 
440     auto opBinary(string op, R)(const R rhs)
441     {
442         scope (exit) obscure();
443         return mixin("Blind!T(value "~op~" rhs)");
444     }
445 
446     auto opBinary(string op, R)(const R rhs) shared
447     {
448         scope (exit) obscure();
449         return mixin("Blind!T(value "~op~" rhs)");
450     }
451 
452     auto opBinaryRight(string op, L)(const L lhs)
453     {
454         scope (exit) obscure();
455         return mixin("Blind!T(lhs "~op~" value)");
456     }
457 
458     auto opBinaryRight(string op, L)(const L lhs) shared
459     {
460         scope (exit) obscure();
461         return mixin("Blind!T(lhs "~op~" value)");
462     }
463 
464     auto opUnary(string op)() 
465     {
466         scope (exit) obscure();
467         return mixin("Blind!T("~op~"value)");
468     }
469 
470     auto opUnary(string op)() shared
471     {
472         obscure();
473         return mixin("Blind!T("~op~"value)");
474     }
475 
476     bool opEquals(A)(const A ahs)
477     {
478         obscure();
479         return value == ahs;
480     }
481 
482     bool opEquals(A)(const A ahs) shared
483     {
484         obscure();
485         return value == ahs;
486     }
487 
488     int opCmp(A)(const A ahs)
489     {
490         obscure();
491         return cast(int)(value - ahs);
492     }
493 
494     int opCmp(A)(const A ahs) shared
495     {
496         obscure();
497         return cast(int)(value - ahs);
498     }
499 
500     string toString() const
501     {
502         return to!string(value);
503     }
504 
505     string toString() const shared
506     {
507         return to!string(value);
508     }
509 }
510 
511 /// Helper function for creating a blind.
512 Blind!T blind(T)(T val)
513 {
514     return Blind!T(val);
515 }