1'use strict'; 2const common = require('../common'); 3if (!common.hasCrypto) 4 common.skip('missing crypto'); 5 6if (!common.enoughTestMem) 7 common.skip('memory-intensive test'); 8 9const assert = require('assert'); 10const crypto = require('crypto'); 11 12function runOneBenchmark(compareFunc, firstBufFill, secondBufFill, bufSize) { 13 return eval(` 14 const firstBuffer = Buffer.alloc(bufSize, firstBufFill); 15 const secondBuffer = Buffer.alloc(bufSize, secondBufFill); 16 17 const startTime = process.hrtime(); 18 const result = compareFunc(firstBuffer, secondBuffer); 19 const endTime = process.hrtime(startTime); 20 21 // Ensure that the result of the function call gets used, so it doesn't 22 // get discarded due to engine optimizations. 23 assert.strictEqual(result, firstBufFill === secondBufFill); 24 25 endTime[0] * 1e9 + endTime[1]; 26 `); 27} 28 29function getTValue(compareFunc) { 30 const numTrials = 1e5; 31 const bufSize = 10000; 32 // Perform benchmarks to verify that timingSafeEqual is actually timing-safe. 33 34 const rawEqualBenches = Array(numTrials); 35 const rawUnequalBenches = Array(numTrials); 36 37 for (let i = 0; i < numTrials; i++) { 38 if (Math.random() < 0.5) { 39 // First benchmark: comparing two equal buffers 40 rawEqualBenches[i] = runOneBenchmark(compareFunc, 'A', 'A', bufSize); 41 // Second benchmark: comparing two unequal buffers 42 rawUnequalBenches[i] = runOneBenchmark(compareFunc, 'B', 'C', bufSize); 43 } else { 44 // Flip the order of the benchmarks half of the time. 45 rawUnequalBenches[i] = runOneBenchmark(compareFunc, 'B', 'C', bufSize); 46 rawEqualBenches[i] = runOneBenchmark(compareFunc, 'A', 'A', bufSize); 47 } 48 } 49 50 const equalBenches = filterOutliers(rawEqualBenches); 51 const unequalBenches = filterOutliers(rawUnequalBenches); 52 53 // Use a two-sample t-test to determine whether the timing difference between 54 // the benchmarks is statistically significant. 55 // https://wikipedia.org/wiki/Student%27s_t-test#Independent_two-sample_t-test 56 57 const equalMean = mean(equalBenches); 58 const unequalMean = mean(unequalBenches); 59 60 const equalLen = equalBenches.length; 61 const unequalLen = unequalBenches.length; 62 63 const combinedStd = combinedStandardDeviation(equalBenches, unequalBenches); 64 const standardErr = combinedStd * Math.sqrt(1 / equalLen + 1 / unequalLen); 65 66 return (equalMean - unequalMean) / standardErr; 67} 68 69// Returns the mean of an array 70function mean(array) { 71 return array.reduce((sum, val) => sum + val, 0) / array.length; 72} 73 74// Returns the sample standard deviation of an array 75function standardDeviation(array) { 76 const arrMean = mean(array); 77 const total = array.reduce((sum, val) => sum + Math.pow(val - arrMean, 2), 0); 78 return Math.sqrt(total / (array.length - 1)); 79} 80 81// Returns the common standard deviation of two arrays 82function combinedStandardDeviation(array1, array2) { 83 const sum1 = Math.pow(standardDeviation(array1), 2) * (array1.length - 1); 84 const sum2 = Math.pow(standardDeviation(array2), 2) * (array2.length - 1); 85 return Math.sqrt((sum1 + sum2) / (array1.length + array2.length - 2)); 86} 87 88// Filter large outliers from an array. A 'large outlier' is a value that is at 89// least 50 times larger than the mean. This prevents the tests from failing 90// due to the standard deviation increase when a function unexpectedly takes 91// a very long time to execute. 92function filterOutliers(array) { 93 const arrMean = mean(array); 94 return array.filter((value) => value / arrMean < 50); 95} 96 97// t_(0.99995, ∞) 98// i.e. If a given comparison function is indeed timing-safe, the t-test result 99// has a 99.99% chance to be below this threshold. Unfortunately, this means 100// that this test will be a bit flakey and will fail 0.01% of the time even if 101// crypto.timingSafeEqual is working properly. 102// t-table ref: http://www.sjsu.edu/faculty/gerstman/StatPrimer/t-table.pdf 103// Note that in reality there are roughly `2 * numTrials - 2` degrees of 104// freedom, not ∞. However, assuming `numTrials` is large, this doesn't 105// significantly affect the threshold. 106const T_THRESHOLD = 3.892; 107 108const t = getTValue(crypto.timingSafeEqual); 109assert( 110 Math.abs(t) < T_THRESHOLD, 111 `timingSafeEqual should not leak information from its execution time (t=${t})` 112); 113 114// As a sanity check to make sure the statistical tests are working, run the 115// same benchmarks again, this time with an unsafe comparison function. In this 116// case the t-value should be above the threshold. 117const unsafeCompare = (bufA, bufB) => bufA.equals(bufB); 118const t2 = getTValue(unsafeCompare); 119assert( 120 Math.abs(t2) > T_THRESHOLD, 121 `Buffer#equals should leak information from its execution time (t=${t2})` 122); 123