11cb0ef41Sopenharmony_ci'use strict'; 21cb0ef41Sopenharmony_ciconst common = require('../common'); 31cb0ef41Sopenharmony_ciif (!common.hasCrypto) 41cb0ef41Sopenharmony_ci common.skip('missing crypto'); 51cb0ef41Sopenharmony_ci 61cb0ef41Sopenharmony_ciif (!common.enoughTestMem) 71cb0ef41Sopenharmony_ci common.skip('memory-intensive test'); 81cb0ef41Sopenharmony_ci 91cb0ef41Sopenharmony_ciconst assert = require('assert'); 101cb0ef41Sopenharmony_ciconst crypto = require('crypto'); 111cb0ef41Sopenharmony_ci 121cb0ef41Sopenharmony_cifunction runOneBenchmark(compareFunc, firstBufFill, secondBufFill, bufSize) { 131cb0ef41Sopenharmony_ci return eval(` 141cb0ef41Sopenharmony_ci const firstBuffer = Buffer.alloc(bufSize, firstBufFill); 151cb0ef41Sopenharmony_ci const secondBuffer = Buffer.alloc(bufSize, secondBufFill); 161cb0ef41Sopenharmony_ci 171cb0ef41Sopenharmony_ci const startTime = process.hrtime(); 181cb0ef41Sopenharmony_ci const result = compareFunc(firstBuffer, secondBuffer); 191cb0ef41Sopenharmony_ci const endTime = process.hrtime(startTime); 201cb0ef41Sopenharmony_ci 211cb0ef41Sopenharmony_ci // Ensure that the result of the function call gets used, so it doesn't 221cb0ef41Sopenharmony_ci // get discarded due to engine optimizations. 231cb0ef41Sopenharmony_ci assert.strictEqual(result, firstBufFill === secondBufFill); 241cb0ef41Sopenharmony_ci 251cb0ef41Sopenharmony_ci endTime[0] * 1e9 + endTime[1]; 261cb0ef41Sopenharmony_ci `); 271cb0ef41Sopenharmony_ci} 281cb0ef41Sopenharmony_ci 291cb0ef41Sopenharmony_cifunction getTValue(compareFunc) { 301cb0ef41Sopenharmony_ci const numTrials = 1e5; 311cb0ef41Sopenharmony_ci const bufSize = 10000; 321cb0ef41Sopenharmony_ci // Perform benchmarks to verify that timingSafeEqual is actually timing-safe. 331cb0ef41Sopenharmony_ci 341cb0ef41Sopenharmony_ci const rawEqualBenches = Array(numTrials); 351cb0ef41Sopenharmony_ci const rawUnequalBenches = Array(numTrials); 361cb0ef41Sopenharmony_ci 371cb0ef41Sopenharmony_ci for (let i = 0; i < numTrials; i++) { 381cb0ef41Sopenharmony_ci if (Math.random() < 0.5) { 391cb0ef41Sopenharmony_ci // First benchmark: comparing two equal buffers 401cb0ef41Sopenharmony_ci rawEqualBenches[i] = runOneBenchmark(compareFunc, 'A', 'A', bufSize); 411cb0ef41Sopenharmony_ci // Second benchmark: comparing two unequal buffers 421cb0ef41Sopenharmony_ci rawUnequalBenches[i] = runOneBenchmark(compareFunc, 'B', 'C', bufSize); 431cb0ef41Sopenharmony_ci } else { 441cb0ef41Sopenharmony_ci // Flip the order of the benchmarks half of the time. 451cb0ef41Sopenharmony_ci rawUnequalBenches[i] = runOneBenchmark(compareFunc, 'B', 'C', bufSize); 461cb0ef41Sopenharmony_ci rawEqualBenches[i] = runOneBenchmark(compareFunc, 'A', 'A', bufSize); 471cb0ef41Sopenharmony_ci } 481cb0ef41Sopenharmony_ci } 491cb0ef41Sopenharmony_ci 501cb0ef41Sopenharmony_ci const equalBenches = filterOutliers(rawEqualBenches); 511cb0ef41Sopenharmony_ci const unequalBenches = filterOutliers(rawUnequalBenches); 521cb0ef41Sopenharmony_ci 531cb0ef41Sopenharmony_ci // Use a two-sample t-test to determine whether the timing difference between 541cb0ef41Sopenharmony_ci // the benchmarks is statistically significant. 551cb0ef41Sopenharmony_ci // https://wikipedia.org/wiki/Student%27s_t-test#Independent_two-sample_t-test 561cb0ef41Sopenharmony_ci 571cb0ef41Sopenharmony_ci const equalMean = mean(equalBenches); 581cb0ef41Sopenharmony_ci const unequalMean = mean(unequalBenches); 591cb0ef41Sopenharmony_ci 601cb0ef41Sopenharmony_ci const equalLen = equalBenches.length; 611cb0ef41Sopenharmony_ci const unequalLen = unequalBenches.length; 621cb0ef41Sopenharmony_ci 631cb0ef41Sopenharmony_ci const combinedStd = combinedStandardDeviation(equalBenches, unequalBenches); 641cb0ef41Sopenharmony_ci const standardErr = combinedStd * Math.sqrt(1 / equalLen + 1 / unequalLen); 651cb0ef41Sopenharmony_ci 661cb0ef41Sopenharmony_ci return (equalMean - unequalMean) / standardErr; 671cb0ef41Sopenharmony_ci} 681cb0ef41Sopenharmony_ci 691cb0ef41Sopenharmony_ci// Returns the mean of an array 701cb0ef41Sopenharmony_cifunction mean(array) { 711cb0ef41Sopenharmony_ci return array.reduce((sum, val) => sum + val, 0) / array.length; 721cb0ef41Sopenharmony_ci} 731cb0ef41Sopenharmony_ci 741cb0ef41Sopenharmony_ci// Returns the sample standard deviation of an array 751cb0ef41Sopenharmony_cifunction standardDeviation(array) { 761cb0ef41Sopenharmony_ci const arrMean = mean(array); 771cb0ef41Sopenharmony_ci const total = array.reduce((sum, val) => sum + Math.pow(val - arrMean, 2), 0); 781cb0ef41Sopenharmony_ci return Math.sqrt(total / (array.length - 1)); 791cb0ef41Sopenharmony_ci} 801cb0ef41Sopenharmony_ci 811cb0ef41Sopenharmony_ci// Returns the common standard deviation of two arrays 821cb0ef41Sopenharmony_cifunction combinedStandardDeviation(array1, array2) { 831cb0ef41Sopenharmony_ci const sum1 = Math.pow(standardDeviation(array1), 2) * (array1.length - 1); 841cb0ef41Sopenharmony_ci const sum2 = Math.pow(standardDeviation(array2), 2) * (array2.length - 1); 851cb0ef41Sopenharmony_ci return Math.sqrt((sum1 + sum2) / (array1.length + array2.length - 2)); 861cb0ef41Sopenharmony_ci} 871cb0ef41Sopenharmony_ci 881cb0ef41Sopenharmony_ci// Filter large outliers from an array. A 'large outlier' is a value that is at 891cb0ef41Sopenharmony_ci// least 50 times larger than the mean. This prevents the tests from failing 901cb0ef41Sopenharmony_ci// due to the standard deviation increase when a function unexpectedly takes 911cb0ef41Sopenharmony_ci// a very long time to execute. 921cb0ef41Sopenharmony_cifunction filterOutliers(array) { 931cb0ef41Sopenharmony_ci const arrMean = mean(array); 941cb0ef41Sopenharmony_ci return array.filter((value) => value / arrMean < 50); 951cb0ef41Sopenharmony_ci} 961cb0ef41Sopenharmony_ci 971cb0ef41Sopenharmony_ci// t_(0.99995, ∞) 981cb0ef41Sopenharmony_ci// i.e. If a given comparison function is indeed timing-safe, the t-test result 991cb0ef41Sopenharmony_ci// has a 99.99% chance to be below this threshold. Unfortunately, this means 1001cb0ef41Sopenharmony_ci// that this test will be a bit flakey and will fail 0.01% of the time even if 1011cb0ef41Sopenharmony_ci// crypto.timingSafeEqual is working properly. 1021cb0ef41Sopenharmony_ci// t-table ref: http://www.sjsu.edu/faculty/gerstman/StatPrimer/t-table.pdf 1031cb0ef41Sopenharmony_ci// Note that in reality there are roughly `2 * numTrials - 2` degrees of 1041cb0ef41Sopenharmony_ci// freedom, not ∞. However, assuming `numTrials` is large, this doesn't 1051cb0ef41Sopenharmony_ci// significantly affect the threshold. 1061cb0ef41Sopenharmony_ciconst T_THRESHOLD = 3.892; 1071cb0ef41Sopenharmony_ci 1081cb0ef41Sopenharmony_ciconst t = getTValue(crypto.timingSafeEqual); 1091cb0ef41Sopenharmony_ciassert( 1101cb0ef41Sopenharmony_ci Math.abs(t) < T_THRESHOLD, 1111cb0ef41Sopenharmony_ci `timingSafeEqual should not leak information from its execution time (t=${t})`, 1121cb0ef41Sopenharmony_ci); 1131cb0ef41Sopenharmony_ci 1141cb0ef41Sopenharmony_ci// As a coherence check to make sure the statistical tests are working, run the 1151cb0ef41Sopenharmony_ci// same benchmarks again, this time with an unsafe comparison function. In this 1161cb0ef41Sopenharmony_ci// case the t-value should be above the threshold. 1171cb0ef41Sopenharmony_ciconst unsafeCompare = (bufA, bufB) => bufA.equals(bufB); 1181cb0ef41Sopenharmony_ciconst t2 = getTValue(unsafeCompare); 1191cb0ef41Sopenharmony_ciassert( 1201cb0ef41Sopenharmony_ci Math.abs(t2) > T_THRESHOLD, 1211cb0ef41Sopenharmony_ci `Buffer#equals should leak information from its execution time (t=${t2})`, 1221cb0ef41Sopenharmony_ci); 123