diff --git a/packages/envied/lib/src/envied_base.dart b/packages/envied/lib/src/envied_base.dart index 7b8aecd..ee8d2a8 100644 --- a/packages/envied/lib/src/envied_base.dart +++ b/packages/envied/lib/src/envied_base.dart @@ -28,7 +28,16 @@ class Envied { /// ``` final String? name; - const Envied({String? path, bool? requireEnvFile, this.name}) + /// Allows all the values to be encrypted using a random + /// generated key that is then XOR'd with the encrypted + /// value when being accessed the first time. + /// Please note that the values can not be offered with + /// the const qualifier, but only with final. + /// **Can be overridden by the per-field obfuscate option!** + final bool obfuscate; + + const Envied( + {String? path, bool? requireEnvFile, this.name, this.obfuscate = false}) : path = path ?? '.env', requireEnvFile = requireEnvFile ?? false; } @@ -38,5 +47,13 @@ class EnviedField { /// The environment variable name specified in the `.env` file to generate for the annotated variable final String? varName; - const EnviedField({this.varName}); + /// Allows this values to be encrypted using a random + /// generated key that is then XOR'd with the encrypted + /// value when being accessed the first time. + /// Please note that the values can not be offered with + /// the const qualifier, but only with final. + /// **Overrides the per-class obfuscate option!** + final bool? obfuscate; + + const EnviedField({this.varName, this.obfuscate}); } diff --git a/packages/envied/test/envy_test.dart b/packages/envied/test/envy_test.dart index 836213e..b009067 100644 --- a/packages/envied/test/envy_test.dart +++ b/packages/envied/test/envy_test.dart @@ -6,6 +6,8 @@ void main() { test('Empty constructor', () { final envied = Envied(); expect(envied.path, '.env'); + expect(envied.requireEnvFile, false); + expect(envied.obfuscate, false); }); test('Specified path', () { @@ -22,17 +24,28 @@ void main() { final envied = Envied(name: 'Foo'); expect(envied.name, 'Foo'); }); + + test('Specified obfuscate', () { + final envied = Envied(obfuscate: true); + expect(envied.obfuscate, true); + }); }); group('EnviedField Test Group', () { test('Empty constructor', () { final enviedField = EnviedField(); expect(enviedField.varName, null); + expect(enviedField.obfuscate, null); }); test('Specified path', () { final enviedField = EnviedField(varName: 'test'); expect(enviedField.varName, 'test'); }); + + test('Specified obfuscate', () { + final enviedField = EnviedField(obfuscate: true); + expect(enviedField.obfuscate, true); + }); }); } diff --git a/packages/envied_generator/lib/src/generate_line_encrypted.dart b/packages/envied_generator/lib/src/generate_line_encrypted.dart new file mode 100644 index 0000000..8d28822 --- /dev/null +++ b/packages/envied_generator/lib/src/generate_line_encrypted.dart @@ -0,0 +1,77 @@ +import 'dart:math'; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:source_gen/source_gen.dart'; + +/// Generate the line to be used in the generated class. +/// If [value] is `null`, it means the variable definition doesn't exist +/// and an [InvalidGenerationSourceError] will be thrown. +/// +/// Since this function also does the type casting, +/// an [InvalidGenerationSourceError] will also be thrown if +/// the type can't be casted, or is not supported. +String generateLineEncrypted(FieldElement field, String? value) { + if (value == null) { + throw InvalidGenerationSourceError( + 'Environment variable not found for field `${field.name}`.', + element: field, + ); + } + + final rand = Random.secure(); + final type = field.type.getDisplayString(withNullability: false); + final name = field.name; + final keyName = '_enviedkey$name'; + + switch (type) { + case "int": + final parsed = int.tryParse(value); + if (parsed == null) { + throw InvalidGenerationSourceError( + 'Type `$type` does not align up to value `$value`.', + element: field, + ); + } else { + final key = rand.nextInt(1 << 32); + final encValue = parsed ^ key; + return 'static final int $keyName = $key;\n' + 'static final int $name = $keyName ^ $encValue;'; + } + case "bool": + final lowercaseValue = value.toLowerCase(); + if (['true', 'false'].contains(lowercaseValue)) { + final parsed = lowercaseValue == 'true'; + final key = rand.nextBool(); + final encValue = parsed ^ key; + return 'static final bool $keyName = $key;\n' + 'static final bool $name = $keyName ^ $encValue;'; + } else { + throw InvalidGenerationSourceError( + 'Type `$type` does not align up to value `$value`.', + element: field, + ); + } + case "String": + case "dynamic": + final parsed = value.codeUnits; + final key = parsed.map((e) => rand.nextInt(1 << 32)).toList( + growable: false, + ); + final encValue = List.generate(parsed.length, (i) => i, growable: false) + .map((i) => parsed[i] ^ key[i]) + .toList(growable: false); + final encName = '_envieddata$name'; + return 'static const List $keyName = [${key.join(", ")}];\n' + 'static const List $encName = [${encValue.join(", ")}];\n' + 'static final ${type == 'dynamic' ? '' : 'String'} $name = String.fromCharCodes(\n' + ' List.generate($encName.length, (i) => i, growable: false)\n' + ' .map((i) => $encName[i] ^ $keyName[i])\n' + ' .toList(growable: false),\n' + ');'; + default: + throw InvalidGenerationSourceError( + 'Obfuscated envied can only handle types such as `int`, `bool` and `String`. Type `$type` is not one of them.', + element: field, + ); + } +} diff --git a/packages/envied_generator/lib/src/generator.dart b/packages/envied_generator/lib/src/generator.dart index 3b8f73a..fed2f59 100644 --- a/packages/envied_generator/lib/src/generator.dart +++ b/packages/envied_generator/lib/src/generator.dart @@ -5,6 +5,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:envied/envied.dart'; import 'package:envied_generator/src/generate_line.dart'; +import 'package:envied_generator/src/generate_line_encrypted.dart'; import 'package:envied_generator/src/load_envs.dart'; import 'package:source_gen/source_gen.dart'; @@ -32,6 +33,7 @@ class EnviedGenerator extends GeneratorForAnnotation { requireEnvFile: annotation.read('requireEnvFile').literalValue as bool? ?? false, name: annotation.read('name').literalValue as String?, + obfuscate: annotation.read('obfuscate').literalValue as bool, ); final envs = await loadEnvs(config.path, (error) { @@ -48,15 +50,21 @@ class EnviedGenerator extends GeneratorForAnnotation { if (enviedFieldChecker.hasAnnotationOf(fieldEl)) { DartObject? dartObject = enviedFieldChecker.firstAnnotationOf(fieldEl); ConstantReader reader = ConstantReader(dartObject); + String varName = reader.read('varName').literalValue as String? ?? fieldEl.name; + String? varValue; if (envs.containsKey(varName)) { varValue = envs[varName]; } else if (Platform.environment.containsKey(varName)) { varValue = Platform.environment[varName]; } - return generateLine( + + final bool obfuscate = + reader.read('obfuscate').literalValue as bool? ?? config.obfuscate; + + return (obfuscate ? generateLineEncrypted : generateLine)( fieldEl, varValue, ); diff --git a/packages/envied_generator/test/src/generator_tests.dart b/packages/envied_generator/test/src/generator_tests.dart index 764f683..8862821 100644 --- a/packages/envied_generator/test/src/generator_tests.dart +++ b/packages/envied_generator/test/src/generator_tests.dart @@ -108,3 +108,33 @@ abstract class Env11 { @EnviedField(varName: 'test_string') static const String? testString = null; } + +@ShouldGenerate('static const List _enviedkeytestString', contains: true) +@ShouldGenerate('static const List _envieddatatestString', contains: true) +@ShouldGenerate(''' + static final String testString = String.fromCharCodes( + List.generate(_envieddatatestString.length, (i) => i, growable: false) + .map((i) => _envieddatatestString[i] ^ _enviedkeytestString[i]) + .toList(growable: false), + ); +''', contains: true) +@Envied(path: 'test/.env.example', obfuscate: true) +abstract class Env12 { + @EnviedField() + static const String? testString = null; +} + +@ShouldGenerate('static const List _enviedkeytestString', contains: true) +@ShouldGenerate('static const List _envieddatatestString', contains: true) +@ShouldGenerate(''' + static final String testString = String.fromCharCodes( + List.generate(_envieddatatestString.length, (i) => i, growable: false) + .map((i) => _envieddatatestString[i] ^ _enviedkeytestString[i]) + .toList(growable: false), + ); +''', contains: true) +@Envied(path: 'test/.env.example', obfuscate: false) +abstract class Env13 { + @EnviedField(obfuscate: true) + static const String? testString = null; +}