• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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