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 }