blob: 7a398d42b2520c1cba60fd3e635c73ad811898f5 [file] [log] [blame]
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:pool/pool.dart';
import 'package:source_span/source_span.dart';
import '../entrypoint.dart';
import '../ignore.dart';
import '../validator.dart';
/// [Utf8Codec] which allows malformed strings.
const _utf8AllowMalformed = Utf8Codec(allowMalformed: true);
/// Link to the documentation for the `false_secrets` key in `pubspec.yaml`.
const _falseSecretsDocumentationLink = 'https://dart.dev/go/false-secrets';
/// A validator that validates attempts to find secrets that are about to be
/// accidentally leaked.
@sealed
class LeakDetectionValidator extends Validator {
LeakDetectionValidator(Entrypoint entrypoint) : super(entrypoint);
@override
Future<void> validate() async {
// Load `false_secrets` from `pubspec.yaml`.
final falseSecrets = Ignore(
entrypoint.root.pubspec.falseSecrets,
ignoreCase: Platform.isWindows || Platform.isMacOS,
);
final pool = Pool(20); // don't read more than 20 files concurrently!
final leaks = await Future.wait(entrypoint.root.listFiles().map((f) async {
final relPath = entrypoint.root.relative(f);
// Skip files matching patterns in `false_secrets`
final nixPath = p.posix.joinAll(p.split(relPath));
if (falseSecrets.ignores(nixPath)) {
return <LeakMatch>[];
}
String text;
try {
// On Windows, we can't open some files without normalizing them
final file = File(p.normalize(p.absolute(f)));
text = await pool.withResource(
() async => await file.readAsString(encoding: _utf8AllowMalformed),
);
} on IOException {
// Pass, ignore files we can't read, let something else error later!
return <LeakMatch>[];
}
return leakPatterns
.map((p) => p.findPossibleLeaks(relPath, text))
.expand((i) => i);
})).then((lists) => lists.expand((i) => i).toList());
// Convert detected leaks to errors, if we have more than 3 then we return
// the first 2 leaks, followed by a general summary of leaks.
//
// This way we never return more than 3 errors, and we always show a 2-3
// samples leaks that very concretely demonstrates the strings we're
// worried about leaking.
if (leaks.length > 3) {
errors.addAll(leaks.take(2).map((leak) => leak.toString()));
final files = leaks
.map((leak) => leak.span.sourceUrl!.toFilePath(windows: false))
.toSet()
.toList(growable: false)
..sort();
final s = files.length > 1 ? 's' : '';
errors.add([
'${leaks.length} potential leaks detected in ${files.length} file$s:',
...files.take(10).map((f) => '- /$f'),
if (files.length > 10) '...',
'',
'Add git-ignore style patterns to `false_secrets` in `pubspec.yaml`',
'to ignore this. See $_falseSecretsDocumentationLink'
].join('\n'));
} else if (leaks.isNotEmpty) {
// If we have 3 leaks we return all leaks, but only include the message
// about how ignore them in the last warning.
final lastLeak = leaks.removeLast();
errors.addAll([
...leaks.take(2).map((leak) => leak.toString()),
[
lastLeak.toString(),
'Add a git-ignore style pattern to `false_secrets` in `pubspec.yaml`',
'to ignore this. See $_falseSecretsDocumentationLink',
].join('\n'),
]);
}
}
}
/// Instance of a match against a [LeakPattern].
@sealed
class LeakMatch {
final LeakPattern pattern;
final SourceSpan span;
LeakMatch(this.pattern, this.span);
@override
String toString() =>
span.message('Potential leak of ${pattern.kind} detected.');
}
/// Definition of a pattern for detecting accidentally leaked secrets.
@visibleForTesting
@sealed
class LeakPattern {
/// Human readable name for the kind of secret this pattern matches.
final String kind;
/// Pattern that matches a secret of given [kind].
final RegExp _pattern;
/// List of allow-listed patterns that are always known to be false-positives.
///
/// Examples include dummy values commonly used for in documentation for
/// illustrative purposes.
final List<Pattern> _allowed;
/// Entropy threshold for matched groups in [_pattern].
///
/// This is a map from _group identifier_ to entropy threshold. This is
/// inspired by [1] where researches ignore detected secrets that have entropy
/// less than 3 standard deviations from the mean of secrets of this [kind].
///
/// To compute the mean entropy of a specific [kind] of secret 10 instances
/// of the secret is generated (and immediately revoked).
///
/// [1]: https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf
final Map<int, double> _entropyThresholds;
/// Test vectors that will have a match in [findPossibleLeaks].
@visibleForTesting
final List<String> testsWithLeaks;
/// Test vectors that will not have a match in [findPossibleLeaks].
@visibleForTesting
final List<String> testsWithNoLeaks;
LeakPattern._({
required this.kind,
required String pattern,
Iterable<Pattern> allowed = const <Pattern>[],
Map<int, double> entropyThresholds = const <int, double>{},
Iterable<String> testsWithLeaks = const <String>[],
Iterable<String> testsWithNoLeaks = const <String>[],
}) : _pattern = RegExp(pattern),
_allowed = List.unmodifiable(allowed),
_entropyThresholds = Map.unmodifiable(entropyThresholds),
testsWithLeaks = List.unmodifiable(testsWithLeaks),
testsWithNoLeaks = List.unmodifiable(testsWithNoLeaks);
/// Find possible leaks using this [LeakPattern].
///
/// A possible [LeakMatch] is found when:
/// * [_pattern] is matched,
/// * no pattern in [_allowed] is matched,
/// * Captured group have a entropy higher than [_entropyThresholds] requires
/// for the given _group identifier_, and,
Iterable<LeakMatch> findPossibleLeaks(String file, String content) sync* {
final source = SourceFile.fromString(content, url: file);
for (final m in _pattern.allMatches(content)) {
if (_allowed.any((s) => m.group(0)!.contains(s))) {
continue;
}
if (_entropyThresholds.entries
.any((entry) => _entropy(m.group(entry.key)!) < entry.value)) {
continue;
}
yield LeakMatch(
this,
source.span(m.start, m.start + m.group(0)!.length),
);
}
}
}
/// Compute Shannon entropy [1] of [s].
///
/// [1]: https://en.wikipedia.org/w/index.php?title=Entropy_(information_theory)&oldid=1033726547
double _entropy(String s) {
final length = s.length.toDouble();
final frequencies = <int, int>{};
for (final rune in s.runes) {
frequencies[rune] = (frequencies[rune] ?? 0) + 1;
}
var sum = 0.0;
for (final frequency in frequencies.values) {
sum -= frequency / length * (log(frequency / length) / log(2));
}
return sum;
}
/// Common patterns for detecting accidentally leaked secrets.
///
/// These patterns are adopted from [1] and [2] with lots of tweaks around:
/// * Boundary detection,
/// * Common allow-listed patterns,
/// * Special file patterns to ignore,
/// * entropy threshold for matched groups,
/// * Examples for correctness testing.
///
/// [1]: https://www.ndss-symposium.org/wp-content/uploads/2019/02/ndss2019_04B-3_Meli_paper.pdf
/// [2]: https://github.com/awslabs/git-secrets
@visibleForTesting
final leakPatterns = List<LeakPattern>.unmodifiable([
LeakPattern._(
kind: 'AWS Access Key',
// Unique identifiers are documented here:
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-unique-ids
//
// Maximum length of a access key is specified as 128 here:
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_AccessKey.html#API_AccessKey_Contents
pattern:
r'[^A-Z0-9]((?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{12,128})[^A-Z0-9]',
allowed: [
// Commonly used in AWS documentation and code samples as an example key.
'AKIAIOSFODNN7EXAMPLE',
// Test credentials for play.min.io, see:
// https://docs.min.io/docs/how-to-use-paperclip-with-minio-server.html
'Q3AM3UQ867SPQQA43P2F',
],
entropyThresholds: {
// Add entropy requirement for the first group
//
// Entropy from the 10 example keys below:
// Mean: 3.756
// Std.dev.: 0.145
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 3.322
1: 3.32,
},
// Added a requirement that start/end is different from [^A-Z0-9]
// This minimizes false positives in large base64 blobs
testsWithLeaks: [
// Generated with AWS Console and immediately deactivated and deleted!
'final accessKey = "AKIAVBOGPFGGW6HQOSMY";',
'final accessKey = "AKIAVBOGPFGG77LJO6ZC";',
'final accessKey = "AKIAVBOGPFGG3Y4MQ6LI";',
'final accessKey = "AKIAVBOGPFGG3FLAFH4W";',
'final accessKey = "AKIAVBOGPFGGQDCE4MVN";',
'final accessKey = "AKIAVBOGPFGGVHHAE7EL";',
'final accessKey = "AKIAVBOGPFGG23S677TL";',
'final accessKey = "AKIAVBOGPFGG2GISUKVC";',
'final accessKey = "AKIAVBOGPFGGQCCVD5NH";',
'final accessKey = "AKIAVBOGPFGG6R2WWNYY";',
],
),
LeakPattern._(
kind: 'AWS Secret Key',
pattern:
r'''(?:"|')?(?:AWS|aws|Aws)?_?(?:SECRET|secret|Secret)?_?(?:ACCESS|access|Access)?_?(?:KEY|key|Key)(?:"|')?\s*(?::|=>|=)\s*(?:"|')?([A-Za-z0-9/\+=]{40})(?:"|')''',
allowed: [
// Commonly used in AWS documentation and code samples as an example key
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
// Test credentials for play.min.io, see:
// https://docs.min.io/docs/how-to-use-paperclip-with-minio-server.html
'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
// If the 40 characters contains are just hexadecimal, there isn't enough
// entropy for it to be a key, and we must have matched something else.
RegExp(
r'''(:|=>|=)\s*("|')?[A-Fa-f0-9][A-Fa-f0-9x][A-Fa-f0-9]{38}("|')?''',
),
],
entropyThresholds: {
// Add entropy requirement for the first group
//
// Entropy from the 10 example keys below:
// Mean: 4.759
// Std.dev.: 0.072
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 4.54
1: 4.54,
},
testsWithLeaks: [
// Generated with AWS Console and immediately deactivated and deleted!
'final secretKey = "zuIzgn8PknWrldyGk5N9GcdQaOWGh57VC54qo7Sy";',
'final secretKey = "xsg5EujrI960RFuOR6Y0IROqtK47zlSwAgzFWMbS";',
'final secretKey = "e5lBcRfsby+Du1B/QTbwZ4aLdmsSVytsGvMZC1R3";',
'final secretKey = "UKUx4bN0ZiGlnM/bTtq3lpXTlawxgSX+Ya3KpD0E";',
'final secretKey = "FvcpSaMTo04BiNEeT20cbvkYtnmE0qYrzhKPcLL3";',
'final secretKey = "W8Peo59t66CM8vws1z9HvobIrIFjP47GAM85yBeS";',
'final secretKey = "epC4pMsSFDtl9zFB70UBtI4mknTG2zKGA5pVxgYp";',
'final secretKey = "fVtx9YuRYtrVIVRUnhi6lzMjKlUa4txw0YvYJ18W";',
'final secretKey = "D/GZyi2nQ+dUoJUflYTHI8d+giIMEY9isjsDPE2D";',
'final secretKey = "RcyVZxn9WKV/QaAJdO+s77IQyMaFKJM1CYQkXQ9u";',
],
),
LeakPattern._(
kind: 'Google API Key',
pattern: r'''[^0-9A-Za-z\-_](AIza[0-9A-Za-z\-_]{35})[^0-9A-Za-z\-_]''',
// Added a requirement that start/end is differnet from [^0-9A-Za-z\-_]
// This minimize false positives in large base64 blobs.
entropyThresholds: {
// Add entropy requirement for the first group
//
// Entropy from the 10 example keys below:
// Mean: 4.702
// Std.dev.: 0.150
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 4.25
1: 4.25,
},
testsWithLeaks: [
// Generated with GCP Console and immediately deleted!
'final apiKey = "AIzaSyDG0yD6347wy0i1U4ThqQoEZ0y37ZvFKPM";',
'final apiKey = "AIzaSyCBSJpVO1A2yHOKP627dSmarIrdgvBygjw";',
'final apiKey = "AIzaSyCB1pW0i5c5Wr42jykePxjrYOXwM4V4Kxk";',
'final apiKey = "AIzaSyBg0xThpU0mAbbVgzm-BZ_4r3ByKwq8HQU";',
'final apiKey = "AIzaSyDWpBgA7US5vfQnooBk1WsKa9U0ogKzuaI";',
'final apiKey = "AIzaSyD95YyR7Xv1F7hdp503G6Tr2vi3CGDC27U";',
'final apiKey = "AIzaSyCIKRF0KxSDxMkTAM7npQKQcASzRMItakw";',
'final apiKey = "AIzaSyAH6KPIIZ5eXLrOX3l90su4YwYpaQ8X7cs";',
'final apiKey = "AIzaSyCS78MPRLsd-Qkhc-t31OiaglmwstaU-nI";',
'final apiKey = "AIzaSyAazCCPl4tWkSuDt9XBWRTpHxroViYhSxg";',
],
testsWithNoLeaks: [
// Insufficient entropy
'final apiKey = "AIzaXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";',
],
),
LeakPattern._(
kind: 'Google OAuth ID',
pattern: r'[0-9]+-([0-9A-Za-z_]{32})\.apps\.googleusercontent\.com',
entropyThresholds: {
// Add entropy requirement for the first group, only the random part of
// the string!
// We ignore the project number and the '.apps.googleusercontent.com'
// suffix for the entropy computation.
//
// Entropy from the 10 example keys below:
// Mean: 4.107
// Std.dev.: 0.186
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 3.54
1: 3.54,
},
testsWithLeaks: [
'final id = "204799038523-t6juuc8cvsvn7bdq0chhihkejuru0bkj.apps.googleusercontent.com";',
'final id = "204799038523-lskk842vjvcn1lqjela2ased27sh4s5m.apps.googleusercontent.com";',
'final id = "204799038523-3cer31sm4566gjeccnms5qo6snn4gn82.apps.googleusercontent.com";',
'final id = "204799038523-ojt14ml172su917bdd2er0t433ke2hh0.apps.googleusercontent.com";',
'final id = "204799038523-5hofn8sghib442c9a6clfe4rag0g3123.apps.googleusercontent.com";',
'final id = "204799038523-gbgohctu6a9hcbmhthkmcahnofocb7ue.apps.googleusercontent.com";',
'final id = "204799038523-gbnpjdjvgeijcpft6ak3rcd5ligqbveh.apps.googleusercontent.com";',
'final id = "204799038523-1toc9p0g13rsj02u695u1hqu7pcq9art.apps.googleusercontent.com";',
'final id = "204799038523-brnom9obdhic04q4e7pvcgdopg6lb1ah.apps.googleusercontent.com";',
'final id = "204799038523-4bjsvv3bmqklm92mlaps2nbi9cjbi94i.apps.googleusercontent.com";',
'''
// Not enough entropy:
final id = "191919191919-onesonesonesonesonesonesonesones.apps.googleusercontent.com";
// This will count as being leaked
final superSecret = '204799038523-t6juuc8cvsvn7bdq0chhihkejuru0bkj.apps.googleusercontent.com';
'''
],
testsWithNoLeaks: [
// Not enough entropy:
'final id = "191919191919-onesonesonesonesonesonesonesones.apps.googleusercontent.com";',
],
),
LeakPattern._(
kind: 'Google OAuth Refresh Token',
pattern: r'[^0-9A-Za-z_\-/+&](1//?[0-9A-Za-z_-]{28,255})[^0-9A-Za-z_\-/+=]',
// We don't know length of the format, probably there isn't a formal one.
// But shorter than 28 or longer than 255 seems unlikely. Maybe it could be
// longer in the future, but restricting it to 255 reduces risk of matching
// a large base64 blob.
//
// Added a requirement that start is differnet from [^0-9A-Za-z_-/+&]
// and end is differnet from [^0-9A-Za-z_-/+=].
// This minimize false positives in large base64 blobs
entropyThresholds: {
// Add entropy requirement for the first group.
//
// Entropy from the 10 example keys below:
// Mean: 5.499
// Std.dev.: 0.088
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 5.23
// As std.dev. of 0.06 feels a bit low and we're unlikely to find false
// positives with a threshold lower than 5.5, we'll just use 5
1: 5,
},
testsWithLeaks: [
// Created with [OAuth Playground][1] and revoked with [MyAccount][2].
//
// [1]: https://developers.google.com/oauthplayground/
// [2]: https://myaccount.google.com/permissions
'final refreshToken = "1//042ys8uoFwZrkCgYIARAAGAQSNwF-L9IrXmFYE-sfKefSpoCnyqEcsHX97Y90KY-p8TPYPPnY2IPgRXdy0QeVw7URuF5u9oUeIF0";',
'final refreshToken = "1//04FB0GjFdOAxACgYIARAAGAQSNwF-L9Ir7WcX-BM2PSxVZegTE1ZCzA9nd4dE9o6bPrmqPJsKgRCjuez1LuR5cvTTsLqfHxBgnpk";',
'final refreshToken = "1//04feFjBQvPS3HCgYIARAAGAQSNwF-L9IrIZC-acykL2UV_jEBwgP-6FSZjw91szK8XrejWFhfaP2j5MTi4osihlwI2lkWl6Q8pcc";',
'final refreshToken = "1//04uP_vMZZBdKRCgYIARAAGAQSNwF-L9IreezpHKQJHni026lWYQuNR7yLRTVKE9qBAV9u9msrEXe1Q3rfgoqoPZJje6lZDMH-o9U";',
'final refreshToken = "1//04bNFb5JQTtMHCgYIARAAGAQSNwF-L9IrSZHCDb94QYeOn1fZZobMVb5pNYhqJ1IpVA406nJziljDXRP6YZ84JxxT1ACeX5Ednt4";',
'final refreshToken = "1//04tpLkWJyOUNXCgYIARAAGAQSNwF-L9IrdN_J4xhzUIwFy4W7cHNl8qGTeEjV6_7rRC564Jm9Vgf_vB-k-fYRaNY3uF7cw5LLmvo";',
'final refreshToken = "1//0427tFuPZKd6PCgYIARAAGAQSNwF-L9IrcFbzaHINAVa0GftO8q5-BYsijx-jKpw5MhSu7Kg1hVNR9k61vprm0m5fbYisYF5LdzI";',
'final refreshToken = "1//04j6Awy3hlyQMCgYIARAAGAQSNwF-L9Ir_XPyo9RSakFqTp_mtEqs8CdjzZwcRWE41CaRqIxn7YMyQqXwZLYMbWq766pEX68Q1kI";',
'final refreshToken = "1//04l0n920gild0CgYIARAAGAQSNwF-L9Ir7kdqjs95T0J-yU9PUg30EIBTlvvzPVR8DHTHxK3I_lqgOG-_ma2pM0Q5-gMcgNMQujQ";',
'final refreshToken = "1//04Mzc8Fsyx4PgCgYIARAAGAQSNwF-L9Ir28S7ZydKT1GnUcju5WgBsb6qFCaZQHgtusdTnPgHGlny5vhq1O0M0K1OtDFK-sFKP_k";',
],
),
LeakPattern._(
kind: 'Google OAuth Access Token',
pattern: r'[^0-9A-Za-z._-]ya29\.([0-9A-Za-z_-]{30,255})[^0-9A-Za-z._-]',
// Added minimum size of 30 to minimize false positives
entropyThresholds: {
// Add entropy requirement for the first group, only the random part of
// the string!
//
// Entropy from the 10 example keys below:
// Mean: 5.691
// Std.dev.: 0.060
//
// Assuming a normal distribution we get 99.7% within 3 std.dev. from mean
// so using this as a lower bound seems reasonable:
// Mean - 3 * std.dev. = 5.5
// As std.dev. of 0.06 feels a bit low and we're unlikely to find false
// positives with a threshold lower than 5.5, we'll just use 5
1: 5,
},
testsWithLeaks: [
'final accessToken = "ya29.a0AfH6SMDItdQQ9J7j3FVgJubZUgztl0FThTEkBs4pA4-9tFREyf2cfcL-_YE5Urg1O0NWwQKie4Ce42n9dOKlxohWgcAl8cg9DTxRx-TFZN-S1VYPLVtQLGYyNTfGp054Ad3ej73-FIHz3RZY43lcKSorbZEY4BI";',
'final accessToken = "ya29.a0AfH6SMAPytspCjQX5SEB87E3-jmwTVoNtXsNT7nPyakT26g6zwKaJ5vxxiZj7OB9Z1IYSoi_09WUHKV_xhxnds2p597tlzZ13qXUm8Sdhgo7n7lyoXQlF34_PT9Y5ttGtsZUWKjflYXOQduN-1kJ1iGixDZdsMk";',
'final accessToken = "ya29.a0AfH6SMCBliVvyA43bb4nYZuk050qrAXztBZ0bNQseXAkz0U1s4M7YjZjHShpGPNQUXfHsd1BCs2v5-dEDiZpQB3_fYKrLQpCeduv5Xm-CyBKc1gEzz0beoJs3i9zBjGVdaAJ7a9kikbaZ-J0Yz50S2dSEqlKsz8";',
'final accessToken = "ya29.a0AfH6SMBJBEUIPqM5zTk9qIr7giESOiXRfqx_xteG8BB4FaBlknw4nE_YqGJef9ZW-J_5LBY-AmTAs2t-x8yCSPQcEVRS2pKL0NQtmh-HoQtNEY11afZl43HC5v3u2S--QVBuUVCqj6EHC1g0JGcPNi4IT2f5Sr0";',
'final accessToken = "ya29.a0AfH6SMD6df9ZUsFdb-mkNU0ua_WHbln6cYWpLJHiH1hLJ-XcM8bI-AMjWGu5ZZ6N05BOBzAKFCBHptDjZhRGP1qpAu5UX2MHN_Zgt4hPqcndcUjSMewtXEckynNsq2wCzl7tSo_QnYAyof2TlHbEDF_pFnOAfu0";',
'final accessToken = "ya29.a0AfH6SMCxD4A3TDkDl2ge_X58b2i8a_y4--_rFfmMw69w-K-8hv7gowN6_shU5-GPyGKkPTdyTbTuvKfPH_zdlKp4_SRasNRJ7HoBEB2H3yhsiFZ8v0gDOSH9GbNREjNuOScwVwwKqhCTFqNHEmULrSJHWm4K8Cs";',
'final accessToken = "ya29.a0AfH6SMDEZPbs0TzUWKZXKrL94Y0LHt0OgZn5efSx6I0Z-P9LQelXVFngaMwR9IFeoeCKRduRLGJJSLAThIE5PmBSyw4o75pZzKF_l4KSVIQCPOCZJtWQWM2eOskN6tEst9DMyIT94g8Rl-WMW9U3IZxmcPsglCc";',
'final accessToken = "ya29.a0AfH6SMDB-jjdR-Q48jiUCuUur2NhVFusLqv-l0JBYELjqmJpsmkZy8kscIOrWq21Z-qzcOvPwSeSShCypxsiK2MHRYrF74JK6eCKJjqPayVP5fVBaOdOQxmzi6jCA0aNF9sZjKL9dsCemPnwhuZUs5AeuWD40Vw";',
'final accessToken = "ya29.a0AfH6SMDrJu5ATPtdPfhPfT2kWECrdVYRJbyQKbrY24T8ONN6AcikbacLzm7DIHh9BU-2EiNUu9B4M1o4ITng-hptqsFugMgJwanHDdC3B-NvqfYgT3x_5eo37reGyT2ZpinBcPFukezRX6kLomdsoyopXru054M";',
'final accessToken = "ya29.a0AfH6SMDzpLKMe0726VNxiT7RYf21w7_Rdl1HjYvth-1Ief20N8nEmSzqQ9RAepiQXgn15-MrkPh0VVRypGu9Yxc3ty9N88ADmOaV9xJO4LkTWENyEW4zF_KMtwgxt0-Cb1DtQ84fEtfRdMp3OZJI3kjZQjwXrbc";',
],
),
LeakPattern._(
kind: 'Private Key',
pattern: _pemKeyFormat('PRIVATE KEY'),
testsWithLeaks: [
// Normal text file
'''
-----BEGIN PRIVATE KEY-----
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
-----END PRIVATE KEY-----
''',
// Text file without normal line breaks at the end
'''-----BEGIN PRIVATE KEY-----
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
-----END PRIVATE KEY-----''',
// Normal encoding when embedding in source as multiline string
'''
final privateKey = \'\'\'
-----BEGIN PRIVATE KEY-----
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgVcB/UNPxalR9zDYAjQIf
jojUDiQuGnSJrFEEzZPT/92hRANCAASc7UJtgnF/abqWM60T3XNJEzBv5ez9TdwK
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
-----END PRIVATE KEY-----
\'\'\';
''',
// Allows some arbitrary whitespace
// LAX mode from: https://tools.ietf.org/html/rfc7468
'''
-----BEGIN PRIVATE KEY-----
M IGE
AgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawI
BAQQgVcB/UNPxalR9zDYAjQIf
jojUDiQuGnSJrFEEzZPT/92hRANCAASc7UJtgnF/abqWM60T3XNJEzBv5ez9T
dwK
H0M6xp
M2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9LkJ
-----END PRIVATE KEY-----
''',
// Allows 1 padding character
'''
-----BEGIN PRIVATE KEY-----
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9Lk=
-----END PRIVATE KEY-----
''',
// Allows 2 padding character
'''
-----BEGIN PRIVATE KEY-----
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9L==
-----END PRIVATE KEY-----
''',
// Encoding in exported service-account credentials
r'''
{
"type": "service_account",
"project_id": "api-project-999797222222",
"private_key_id": "1f6070c2f200bcdfdcc03be6555d1fefa0715c5a",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCq60Njgtb74U/7\nkin5YKmY4hVDCb+nskL22TfckskfgFJz6n4dgwkNX2una8LOFimsNqajxHUFiAOy\n8uCBVhLDkMp35lZyprir1wnOwSU/pN3sJh2c22RfS+Q1nAc9/LRyaNxdIxAxaChq\nr48JpfGpACV3siXtp8m2aJd+cyqXUBz3sGMyA43KgPkdeGfL9Rk05Bc457PtH7CX\nGHArJJ2cpDhNRdJ1QBZL6Tb5ZmbQCM0lhpCe5XToVCZMSI5hnce++0vImZxwLhQW\noFyLYceggONmX0ZmNu97gEo9T6koxDXDACVRk4dnTMyktIWiOtTJxF0moALHvNfZ\nCPIlMu/rAgMBAAECggEABxzh9w2GM4E9j4iWT+x34lw2P6NI4zUF7bk9xo4ilI8F\n32Zjd92beNT/E+k7eCbFS9y9CT+vEbCGsYxt+glCSqUx59TMGtquq2gNiRnR764a\nwNhtObk073T8ZZwSqRUP0AW7y0ce8LoW7ymoguGcXEjHRmNBiicGEUiVAAwm5O5n\nugwTwjr5njBw1vZ8kGrHR58o78Fpiqo30unweUL4qjrRYTXsDSOq29JOXy5a8bVP\ntMZM3Y1g5RDNdJaCJO580qpIIcB+1MqGE008+aHTHK/Jx2MjlRh4zF5jTnNk8jcE\nxAX0OM7d9tSsud6/SSgFSSH0JMwFkNSLbyL7GWxU8QKBgQDoSCFi8VvASfOnHfQ9\ntspERdyaUf3meDYrVtG1Zhmuc44lf7gYaWnGvdWpu4KMQQWDC9uL/zlRqLzRp0db\nalNvyuO9Z9GDki28uCR289q6DHeRaGHVKYkx5cfbaOJhTPEiWMoq4vtHsk4jo5eI\n47gxa6BoQO7r/qhGvtMnxu0ehQKBgQC8XxqQhlv5cld4edqQb3bT9R7WzQlYLmlB\nb+tM5eKESt0KWjpxu8QJ5JKgUCgc4jcoXQ/sy8QP/upiF9Xz4QQ37mtFaSup/HlY\nYv3HiT9lUy9yvALLo0yBvtjQvuW7+X0xCnbIzMuNNHcpTCESR44cWERgZUT8TFrw\nv7+PwkK3rwKBgQC8DXmKIyFHAhgS2jtco1oKAA1jmrHWHsisObO6CpkMFV2lmksu\n6FjMn/AVZEuCxTlzKOxr4QtEwzlq+uTYa7J1NWs/coe632PL/8D11OLl8SX0QO/D\npcb+8KrnRXjRkXs/dWbnZbBOEVsVm2IZX1NGH35UKQ3FXfxami9VasWaCQKBgC7U\nogEONi10vMRJ3wmLfIpDZVBXlxwiJa7MCT6L5F2pUvyw49jEqn8fIUjTxLUxlC7n\nu/7NxceIQ3LxpBJGfcr97hNKiz1udCiCK7+Aoo8pOCGZFkTUK0ASV0rGOs0ZwIMB\nq/hN2ckYIwvUTmCCA5WOaCli49ypiu5RbWlrDTUnAoGBAKi2ci9kXlMVTLpbadGE\nzBMl1uB+3HXhgtXsaofA1JmquGuxlRXrq1O/XXGQYBISTKAf87ULKMnMXnh1klmU\nZ84gQQkywISfmMY6tIqqOlWkXXk7OVDDErHdnBj+3UfMkTEZChbiMkQAkSc+Hwd9\n6xhidr9WqPzl7r3PXBPA2Zdx\n-----END PRIVATE KEY-----\n",
"client_email": "test@api-project-999797222222.iam.gserviceaccount.com",
"client_id": "111119132165292222222",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test%40api-project-999797222222.iam.gserviceaccount.com"
}
''',
],
testsWithNoLeaks: [
// 3 base padding characters is never valid!
'''
-----BEGIN PRIVATE KEY-----
H0M6xpM2q+53wmsN/eYLdgtjgBd3DBmHtPilCkiFICXyaA8z9===
-----END PRIVATE KEY-----
''',
],
),
LeakPattern._(
kind: 'RSA Private Key',
pattern: _pemKeyFormat('RSA PRIVATE KEY'),
),
LeakPattern._(
kind: 'EC Private Key',
pattern: _pemKeyFormat('EC PRIVATE KEY'),
),
LeakPattern._(
kind: 'PGP Private Key',
pattern: [
_pemBegin('PGP PRIVATE KEY BLOCK'),
// Allow "Armor Headers" from:
// https://www.rfc-editor.org/rfc/rfc4880.html#section-6.2
'(?:\\w+: [^\\n]{1,1024}$_pemRequireLineBreak$_pemWSP)*',
_pemBase64Block(),
// Require a line break, and a 24-bit base64 encoded checksum prefixed '='
// https://www.rfc-editor.org/rfc/rfc4880.html#section-6
'$_pemRequireLineBreak$_pemWSP',
'=(?:(?:[a-zA-Z0-9+/]$_pemWSP){4})',
_pemEnd('PGP PRIVATE KEY BLOCK'),
].join(),
testsWithLeaks: [
'''
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: Keybase OpenPGP v1.0.0
Comment: https://keybase.io/crypto
xcASBGCFQ7MTBSuBBAAiAwMEvlAADGgIHDMkO5UqDbFaVfARFUzvMJyo355r8LuE
NWW3XWHb+r39waMhqXmQZxes4YuXT2o/68wTzzus14gOtrLWQrrTZgp99duOfiS7
gv2NX7uF0kx2DG5YlD+VMkDl/gkDCOcy7lmIlJawYDrgCBSEfnrXh0m96xhN9RNZ
u7AkuvD+MnhUkC6r/zlKNBERP2QKYhsxQRQDwlCQL3B3Pj07os7DboMpEMGbAGss
PMtqYDExOwfQ6H6+FqDcc7E0VWNSzSF0ZXN0ICh0ZXN0KSA8ZXhhbXBsZUBleGFt
cGxlLmNvbT7CjwQTEwoAFwUCYIVDswIbLwMLCQcDFQoIAh4BAheAAAoJEP1+dYog
Dm+yhCUBgKeyXrKZvzi5OVrFJC6KSbRQB/YaxxIk9g01GupyLLgnti0oea5eSqGS
8YVxT/A16wF9Gqq31sE2yOVC9xAuyohANfV+bMdhgivG8TIUMIFkQ9EOE1WSBuY0
mzXSrS+4KMbDx6UEYIVDsxMIKoZIzj0DAQcCAwT9dBwQahYj+vxBX3Aha4Ti9vaZ
PFdTIN+OfFniiPCaHqHHdZ/I28kAMMEdDwPexYNiR0Fk6iz/Yx86V8jnQH2X/gkD
CG5njLK6ZHWmYNeqMwjEHN8nA7M2bY8BxbHTeJv49FfSr/Zh5O2vNE1uYg/B3gNJ
yGN4fxLBzKYGCDOkiJEYaWJGOBuPWKGd+quyjIuAzjbCwCcEGBMKAA8FAmCFQ7MF
CQ8JnAACGy4AagkQ/X51iiAOb7JfIAQZEwoABgUCYIVDswAKCRBIawKSRH1lh9RY
AQDmPgwHtLjq4Bezi5ouTDFp77ThbJ9CnCcXwrQd7TtSaQD/SXfPpAe1HToSdJoe
Hcbo1elrxh6Rtc+JWd+/XJ/IpLeCEwGA5a9yqbMegLetpj1F2jwxO8O5raamTR4w
/V0Q+Msb73PuNPUikImFZDv+ChI1+cffAX9Jqd7+Eh7WafuoC475izLBZJbJKTW5
BodQdUXsO5WmcVUOivkylktJFwpft3LZWPPHpQRghUOzEwgqhkjOPQMBBwIDBGzY
1lmmmDIJZZwQjeDqac8JMrX0/6VuUw9NBF5r+k8Vkvvx2iaz79IjvJMCN9u50O2w
4bDlmBvZ55koL4PX49z+CQMIk87UpR3v5ktgkx0t1+QkY7byJ59f5tpDTTgez2fT
LhTbfOdHyD5Al/zjU6p5XNF0If4GsjhfVMxoJUbKkLMtPM5xlLmuqnozvJF4PKhH
kcLAJwQYEwoADwUCYIVDswUJDwmcAAIbLgBqCRD9fnWKIA5vsl8gBBkTCgAGBQJg
hUOzAAoJENYMoCRtY0P6rWMBAJgOpN3f6FwSDop+MRCImahF6le6b6GK/vKkCL3V
pjhmAQDMYovIDX7YH831pdv9ggrthZEBTVD/Rtpw8BdLTAsggecSAX9p5otJE+cg
/fQssS2nWDcpSQ4mqjJu5wQLB8u/EWUzDpMDnd6/b4BiaL/CUf33gGIBgPo2WTAM
RmkFFVpJpULM46oNqjI0Ps58ClfR7PH73mJ5T+6CFUAAIm3zBVDlpLE8pA==
=3iPB
-----END PGP PRIVATE KEY BLOCK-----
''',
],
),
]);
// Allow arbitrary whitespace and escaped line breaks
String _pemWSP = r'(?:\\r|\\n|\\t|\s)*';
// Require \n, \r, \\r, or \\n, backslash escaping is allowed if the key
// is in a JSON string. We just require something to indicate line break.
String _pemRequireLineBreak = r'\s*(?:\\r|\\n|\r|\n)\s*';
String _pemBegin(String label) => [
// Require a boundary
'-----BEGIN $label-----',
// Require \n, \r, \\r, or \\n, backslash escaping is allowed if the key
// is in a JSON string. We just require something to indicate line break.
_pemRequireLineBreak,
// Allow arbitrary whitespace and escaped line breaks
_pemWSP,
].join();
String _pemBase64Block() => [
// Require base64 character in blocks of 4, allow arbirary whitespace
// and escaped line breaks in between.
'(?:(?:[a-zA-Z0-9+/]$_pemWSP){4})+',
// We have 3 options for encoding the ending:
// (A) 1 padding character,
// (B) 2 padding characters,
// (C) No padding characters (neither A or B)
'(?:(',
[
// Option (A): 3 base64 characters followed by one base64 padding
// character, allow arbirary whitespace and escaped line breaks
// in between.
'(?:[a-zA-Z0-9+/]$_pemWSP){3}=$_pemWSP',
// Either (A) or (B):
')|(?:',
// Option (B): 2 base64 characters followed by 2 base64 padding
// character, allow arbirary whitespace and escaped line breaks
// in between.
'(?:[a-zA-Z0-9+/]$_pemWSP){2}(?:=$_pemWSP){2}',
].join(),
// End blocks
'))?',
].join();
String _pemEnd(String label) => [
// Require \n, \r, \\r, or \\n, backslash escaping is allowed if the key
// is in a JSON string. We just require something to indicate line break.
_pemRequireLineBreak,
// Allow arbitrary whitespace and escaped line breaks
_pemWSP,
'-----END $label-----',
].join();
String _pemKeyFormat(String label) => [
_pemBegin(label),
_pemBase64Block(),
_pemEnd(label),
].join();