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