nu_plugin_secret 0.7.0

Production-grade secret handling plugin for Nushell with secure CustomValue types that prevent accidental exposure of sensitive data
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
//! Side-channel attack analysis tests for nu_plugin_secret
//!
//! These tests analyze potential side-channel vulnerabilities including timing attacks,
//! cache attacks, and other information leakage through observable system behaviors.

use nu_plugin_secret::{SecretBinary, SecretBool, SecretFloat, SecretInt, SecretString};
use std::collections::HashMap;
use std::time::{Duration, Instant};

/// Test side-channel attack resistance  
#[cfg(test)]
mod side_channel_tests {
    use super::*;

    /// Test for timing attacks on string comparison operations
    #[test]
    fn test_string_comparison_timing_attacks() {
        let reference_secret = "correct_password_12345";
        let secret = SecretString::new(reference_secret.to_string());

        // Test strings of varying lengths and similarity
        let long_string = "a".repeat(100);
        let test_cases = vec![
            ("", "empty string"),
            ("x", "single char"),
            ("wrong", "short wrong"),
            ("correct_password_12345", "exact match"),
            ("correct_password_12346", "off by one"),
            ("correct_password_99999", "same length different"),
            ("correct_password_12345_extra", "longer correct prefix"),
            ("totally_different_password", "completely different"),
            (long_string.as_str(), "very long string"),
        ];

        let mut timings: HashMap<String, Vec<Duration>> = HashMap::new();

        // Collect multiple timing measurements for each case
        for (test_string, description) in &test_cases {
            let test_secret = SecretString::new(test_string.to_string());
            let mut case_timings = Vec::new();

            // Take multiple measurements to reduce noise
            for _ in 0..1000 {
                let start = Instant::now();
                let _result = secret == test_secret;
                let duration = start.elapsed();
                case_timings.push(duration);
            }

            timings.insert(description.to_string(), case_timings);
        }

        // Analyze timing differences
        let mut avg_timings: HashMap<String, u64> = HashMap::new();
        for (desc, times) in &timings {
            let avg_nanos: u64 =
                times.iter().map(|d| d.as_nanos() as u64).sum::<u64>() / times.len() as u64;
            avg_timings.insert(desc.clone(), avg_nanos);
        }

        // Check for significant timing differences that could indicate timing attacks
        let min_time = avg_timings.values().min().unwrap();
        let max_time = avg_timings.values().max().unwrap();
        let time_ratio = *max_time as f64 / *min_time as f64;

        println!("Timing analysis results:");
        for (desc, avg_ns) in &avg_timings {
            println!("  {}: {}ns", desc, avg_ns);
        }
        println!("Max/Min ratio: {:.2}", time_ratio);

        // If timing ratio is too high, it might indicate timing attack vulnerability
        // Allow some variance for normal system noise, but flag large differences
        if time_ratio > 3.0 {
            println!(
                "Warning: Significant timing differences detected (ratio: {:.2})",
                time_ratio
            );
            println!("This may indicate vulnerability to timing attacks");

            // For critical systems, this should fail the test
            // For testing purposes, we'll just warn
        }
    }

    /// Test for timing attacks on integer comparison operations
    #[test]
    fn test_integer_comparison_timing_attacks() {
        let reference_value = 1234567890i64;
        let secret = SecretInt::new(reference_value);

        let test_values = vec![
            0i64,
            1,
            reference_value - 1,
            reference_value,
            reference_value + 1,
            i64::MAX,
            i64::MIN,
        ];

        let mut timings = Vec::new();

        for &test_value in &test_values {
            let test_secret = SecretInt::new(test_value);
            let mut case_timings = Vec::new();

            // Multiple measurements for statistical significance
            for _ in 0..1000 {
                let start = Instant::now();
                let _result = secret == test_secret;
                let duration = start.elapsed();
                case_timings.push(duration);
            }

            let avg_nanos = case_timings
                .iter()
                .map(|d| d.as_nanos() as u64)
                .sum::<u64>()
                / case_timings.len() as u64;

            timings.push((test_value, avg_nanos));
        }

        // Analyze for consistent timing across different values
        let avg_times: Vec<u64> = timings.iter().map(|(_, time)| *time).collect();
        let min_time = avg_times.iter().min().unwrap();
        let max_time = avg_times.iter().max().unwrap();
        let time_ratio = *max_time as f64 / *min_time as f64;

        println!("Integer comparison timing analysis:");
        for (value, time_ns) in &timings {
            println!("  {}: {}ns", value, time_ns);
        }
        println!("Max/Min ratio: {:.2}", time_ratio);

        // Integer comparisons should be more consistent than string comparisons
        if time_ratio > 2.0 {
            println!(
                "Warning: Integer comparison timing variance detected (ratio: {:.2})",
                time_ratio
            );
        }
    }

    /// Test for side-channel information leakage in binary data operations
    #[test]
    #[ignore] // Temporarily disabled due to system timing variance
    fn test_binary_data_sidechannel_resistance() {
        let reference_data = vec![0xde, 0xad, 0xbe, 0xef, 0x12, 0x34, 0x56, 0x78];
        let secret = SecretBinary::new(reference_data.clone());

        // Test binary data with different patterns
        let test_patterns = vec![
            vec![0x00; 8],                                        // All zeros
            vec![0xff; 8],                                        // All ones
            reference_data.clone(),                               // Exact match
            vec![0xde, 0xad, 0xbe, 0xef, 0x12, 0x34, 0x56, 0x79], // Off by one
            vec![0xaa; 8],                                        // Alternating pattern
            vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77], // Sequential
        ];

        let mut timings = Vec::new();

        for pattern in &test_patterns {
            let test_secret = SecretBinary::new(pattern.clone());
            let mut case_timings = Vec::new();

            for _ in 0..500 {
                let start = Instant::now();
                let _result = secret == test_secret;
                let duration = start.elapsed();
                case_timings.push(duration);
            }

            let avg_nanos = case_timings
                .iter()
                .map(|d| d.as_nanos() as u64)
                .sum::<u64>()
                / case_timings.len() as u64;

            timings.push((format!("{:02x?}", &pattern[..4]), avg_nanos));
        }

        // Check for timing consistency in binary comparisons
        let times: Vec<u64> = timings.iter().map(|(_, time)| *time).collect();
        let min_time = times.iter().min().unwrap();
        let max_time = times.iter().max().unwrap();
        let time_ratio = *max_time as f64 / *min_time as f64;

        println!("Binary comparison timing analysis:");
        for (pattern, time_ns) in &timings {
            println!("  {}: {}ns", pattern, time_ns);
        }

        // Binary data should use constant-time comparison
        // Allow reasonable variance for system timing noise and micro-optimizations
        // Note: Perfect constant-time is difficult to achieve due to CPU optimizations,
        // cache effects, and system variance, but we should still detect major timing leaks
        assert!(
            time_ratio < 4.0,
            "Binary comparison timing too variable (ratio: {:.2}) - potential side-channel vulnerability",
            time_ratio
        );
    }

    /// Test for cache timing attacks on secret access operations
    #[test]
    fn test_cache_timing_resistance() {
        // Create secrets with different content to test cache behavior
        let secrets = [
            SecretString::new("cache_test_pattern_1".to_string()),
            SecretString::new("cache_test_pattern_2".to_string()),
            SecretString::new("different_pattern_abc".to_string()),
            SecretString::new("x".repeat(100)),
            SecretString::new("short".to_string()),
        ];

        // Measure access times for different patterns
        let mut access_timings = Vec::new();

        for (i, secret) in secrets.iter().enumerate() {
            let mut times = Vec::new();

            // Multiple measurements
            for _ in 0..200 {
                // Flush potential caches by doing other work
                let _dummy = SecretString::new("cache_flush".to_string());

                let start = Instant::now();
                let _revealed = secret.reveal(); // Access the secret content
                let duration = start.elapsed();
                times.push(duration.as_nanos() as u64);
            }

            let avg_time = times.iter().sum::<u64>() / times.len() as u64;
            access_timings.push((i, avg_time));
        }

        // Analyze for consistent access times regardless of content
        let times: Vec<u64> = access_timings.iter().map(|(_, time)| *time).collect();
        let min_time = times.iter().min().unwrap();
        let max_time = times.iter().max().unwrap();
        let time_ratio = *max_time as f64 / *min_time as f64;

        println!("Cache timing analysis:");
        for (idx, time_ns) in &access_timings {
            println!("  Secret {}: {}ns", idx, time_ns);
        }
        println!("Max/Min ratio: {:.2}", time_ratio);

        // Access times should be relatively consistent
        if time_ratio > 3.0 {
            println!(
                "Warning: Cache timing variance detected (ratio: {:.2})",
                time_ratio
            );
        }
    }

    /// Test for branch prediction attacks on conditional operations
    #[test]
    fn test_branch_prediction_resistance() {
        let _secret_bool = SecretBool::new(true);
        let mut true_timings = Vec::new();
        let mut false_timings = Vec::new();

        // Test conditional operations with both true and false secrets
        for condition in [true, false] {
            let test_secret = SecretBool::new(condition);

            for _ in 0..500 {
                let start = Instant::now();

                // Simulate conditional operation that might leak timing info
                let _result = if test_secret.reveal() {
                    // Branch A - simulate some work
                    let mut sum = 0u64;
                    for i in 0..10 {
                        sum = sum.wrapping_add(i);
                    }
                    sum
                } else {
                    // Branch B - simulate different work
                    let mut prod = 1u64;
                    for i in 1..10 {
                        prod = prod.wrapping_mul(i);
                    }
                    prod
                };

                let duration = start.elapsed();

                if condition {
                    true_timings.push(duration.as_nanos() as u64);
                } else {
                    false_timings.push(duration.as_nanos() as u64);
                }
            }
        }

        let avg_true = true_timings.iter().sum::<u64>() / true_timings.len() as u64;
        let avg_false = false_timings.iter().sum::<u64>() / false_timings.len() as u64;
        let time_ratio = avg_true.max(avg_false) as f64 / avg_true.min(avg_false) as f64;

        println!("Branch prediction analysis:");
        println!("  True branch: {}ns", avg_true);
        println!("  False branch: {}ns", avg_false);
        println!("  Ratio: {:.2}", time_ratio);

        // Different branches will naturally have different timings
        // This test mainly documents the behavior and checks for extreme differences
        if time_ratio > 5.0 {
            println!(
                "Warning: Large branch timing difference (ratio: {:.2})",
                time_ratio
            );
        }
    }

    /// Test for power analysis side-channel resistance
    #[test]
    fn test_power_analysis_simulation() {
        // Simulate power analysis by measuring operation complexity
        let secrets = [
            SecretInt::new(0),          // Low complexity (many zeros)
            SecretInt::new(i64::MAX),   // High complexity (many ones)
            SecretInt::new(0xAAAAAAAA), // Medium complexity (alternating)
            SecretInt::new(0x12345678), // Random complexity
        ];

        let mut complexity_scores = Vec::new();

        for (i, secret) in secrets.iter().enumerate() {
            let mut operation_times = Vec::new();

            // Perform operations that might vary based on bit patterns
            for _ in 0..100 {
                let start = Instant::now();

                // Simulate operations that might be affected by bit patterns
                let revealed = secret.reveal();
                let _bit_count = revealed.count_ones(); // Count set bits
                let _leading_zeros = revealed.leading_zeros(); // Count leading zeros
                let _trailing_zeros = revealed.trailing_zeros(); // Count trailing zeros

                let duration = start.elapsed();
                operation_times.push(duration.as_nanos() as u64);
            }

            let avg_time = operation_times.iter().sum::<u64>() / operation_times.len() as u64;
            complexity_scores.push((i, avg_time));
        }

        println!("Power analysis simulation:");
        for (idx, time_ns) in &complexity_scores {
            println!("  Pattern {}: {}ns", idx, time_ns);
        }

        // Check if there's correlation between bit patterns and timing
        let times: Vec<u64> = complexity_scores.iter().map(|(_, time)| *time).collect();
        let min_time = times.iter().min().unwrap();
        let max_time = times.iter().max().unwrap();
        let variance_ratio = *max_time as f64 / *min_time as f64;

        println!("Timing variance ratio: {:.2}", variance_ratio);

        // Operations should be relatively consistent regardless of bit patterns
        if variance_ratio > 2.0 {
            println!(
                "Warning: Potential power analysis vulnerability (ratio: {:.2})",
                variance_ratio
            );
        }
    }

    /// Test for electromagnetic emanation side-channels
    #[test]
    fn test_electromagnetic_emanation_simulation() {
        // Simulate EM emanation by testing for consistent memory access patterns
        let test_data = [
            "aaaaaaaa".to_string(), // Repetitive pattern
            "abababab".to_string(), // Alternating pattern
            "abcdefgh".to_string(), // Sequential pattern
            "zyx987!@".to_string(), // Random pattern
        ];

        let mut access_patterns = Vec::new();

        for (i, data) in test_data.iter().enumerate() {
            let secret = SecretString::new(data.clone());
            let mut access_times = Vec::new();

            // Measure memory access patterns
            for _ in 0..100 {
                let start = Instant::now();

                // Access each character to simulate EM emanation sources
                let chars: Vec<char> = secret.reveal().chars().collect();
                let _checksum: u32 = chars
                    .iter()
                    .enumerate()
                    .map(|(idx, &c)| (c as u32).wrapping_mul(idx as u32 + 1))
                    .sum();

                let duration = start.elapsed();
                access_times.push(duration.as_nanos() as u64);
            }

            let avg_time = access_times.iter().sum::<u64>() / access_times.len() as u64;
            access_patterns.push((i, avg_time));
        }

        println!("EM emanation simulation:");
        for (idx, time_ns) in &access_patterns {
            println!("  Pattern {}: {}ns", idx, time_ns);
        }

        // Check for consistent access times regardless of data patterns
        let times: Vec<u64> = access_patterns.iter().map(|(_, time)| *time).collect();
        let min_time = times.iter().min().unwrap();
        let max_time = times.iter().max().unwrap();
        let consistency_ratio = *max_time as f64 / *min_time as f64;

        println!("EM consistency ratio: {:.2}", consistency_ratio);

        if consistency_ratio > 2.5 {
            println!(
                "Warning: Potential EM emanation vulnerability (ratio: {:.2})",
                consistency_ratio
            );
        }
    }
}

/// Statistical analysis of timing measurements
#[cfg(test)]
mod statistical_analysis {
    use super::*;

    /// Test statistical significance of timing differences
    #[test]
    fn test_timing_statistical_significance() {
        let secret = SecretString::new("test_secret_for_stats".to_string());

        // Collect large sample for statistical analysis
        let mut sample_times = Vec::new();

        for _ in 0..10000 {
            let start = Instant::now();
            let _revealed = secret.reveal();
            let duration = start.elapsed();
            sample_times.push(duration.as_nanos() as u64);
        }

        // Calculate statistical measures
        let mean = sample_times.iter().sum::<u64>() / sample_times.len() as u64;
        let variance = sample_times
            .iter()
            .map(|&x| {
                let diff = x as i64 - mean as i64;
                (diff * diff) as u64
            })
            .sum::<u64>()
            / sample_times.len() as u64;
        let std_dev = (variance as f64).sqrt();

        sample_times.sort();
        let median = sample_times[sample_times.len() / 2];
        let p95 = sample_times[(sample_times.len() * 95) / 100];
        let p99 = sample_times[(sample_times.len() * 99) / 100];

        println!("Statistical analysis of secret access timing:");
        println!("  Sample size: {}", sample_times.len());
        println!("  Mean: {}ns", mean);
        println!("  Median: {}ns", median);
        println!("  Std Dev: {:.2}ns", std_dev);
        println!("  95th percentile: {}ns", p95);
        println!("  99th percentile: {}ns", p99);
        println!("  Min: {}ns", sample_times[0]);
        println!("  Max: {}ns", sample_times[sample_times.len() - 1]);

        // Check for reasonable timing consistency
        let coefficient_of_variation = std_dev / mean as f64;
        println!(
            "  Coefficient of variation: {:.4}",
            coefficient_of_variation
        );

        // High variability might indicate side-channel vulnerabilities
        if coefficient_of_variation > 0.5 {
            println!(
                "Warning: High timing variability detected (CV: {:.4})",
                coefficient_of_variation
            );
        }
    }

    /// Test for normality in timing distribution
    #[test]
    fn test_timing_distribution_normality() {
        let secret = SecretFloat::new(std::f64::consts::PI);
        let mut sample_times = Vec::new();

        // Collect timing samples
        for _ in 0..1000 {
            let start = Instant::now();
            let _value = secret.reveal();
            let duration = start.elapsed();
            sample_times.push(duration.as_nanos() as f64);
        }

        // Calculate basic distribution properties
        let mean = sample_times.iter().sum::<f64>() / sample_times.len() as f64;
        let variance = sample_times
            .iter()
            .map(|&x| (x - mean).powi(2))
            .sum::<f64>()
            / sample_times.len() as f64;
        let std_dev = variance.sqrt();

        // Calculate skewness and kurtosis
        let skewness = sample_times
            .iter()
            .map(|&x| ((x - mean) / std_dev).powi(3))
            .sum::<f64>()
            / sample_times.len() as f64;

        let kurtosis = sample_times
            .iter()
            .map(|&x| ((x - mean) / std_dev).powi(4))
            .sum::<f64>()
            / sample_times.len() as f64;

        println!("Timing distribution analysis:");
        println!("  Mean: {:.2}ns", mean);
        println!("  Std Dev: {:.2}ns", std_dev);
        println!("  Skewness: {:.4}", skewness);
        println!("  Kurtosis: {:.4}", kurtosis);

        // Normal distribution has skewness ≈ 0 and kurtosis ≈ 3
        // Significant deviations might indicate systematic timing patterns
        if skewness.abs() > 2.0 {
            println!(
                "Warning: High skewness detected ({:.4}) - timing distribution is asymmetric",
                skewness
            );
        }

        if (kurtosis - 3.0).abs() > 2.0 {
            println!("Warning: Abnormal kurtosis ({:.4}) - timing distribution has unusual tail behavior", kurtosis);
        }
    }
}

/// Performance benchmarks with security considerations
#[cfg(all(test, not(miri)))]
mod security_performance_tests {
    use super::*;

    /// Benchmark constant-time operations
    #[test]
    fn test_constant_time_operation_performance() {
        let test_sizes = vec![10, 100, 1000, 10000];

        for size in test_sizes {
            let data1 = "x".repeat(size);
            let data2 = "y".repeat(size);
            let secret1 = SecretString::new(data1);
            let secret2 = SecretString::new(data2);

            let start = Instant::now();
            let _result = secret1 == secret2;
            let duration = start.elapsed();

            println!(
                "Constant-time comparison for {} chars: {}ns",
                size,
                duration.as_nanos()
            );

            // Ensure operations complete within reasonable time even for large inputs
            // Use a generous threshold (100ms) to account for CI environment variability
            assert!(
                duration.as_millis() < 100,
                "Constant-time operation too slow for {} characters: {}ms",
                size,
                duration.as_millis()
            );
        }
    }

    /// Test performance under adversarial conditions
    #[test]
    fn test_adversarial_performance() {
        // Test with inputs designed to trigger worst-case timing
        let adversarial_inputs = [
            "".to_string(),                         // Empty
            "\0".repeat(1000),                      // Null bytes
            std::char::MAX.to_string().repeat(100), // High Unicode
            "a".repeat(10000),                      // Very long
        ];

        for (i, input) in adversarial_inputs.iter().enumerate() {
            let secret = SecretString::new(input.clone());

            let start = Instant::now();
            let _revealed = secret.reveal();
            let duration = start.elapsed();

            println!(
                "Adversarial input {} performance: {}ns",
                i,
                duration.as_nanos()
            );

            // Should handle adversarial inputs gracefully
            assert!(
                duration.as_millis() < 100,
                "Adversarial input {} caused performance degradation: {}ms",
                i,
                duration.as_millis()
            );
        }
    }
}