Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add obfuscation option #4

Merged
merged 3 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions packages/envied/lib/src/envied_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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});
}
13 changes: 13 additions & 0 deletions packages/envied/test/envy_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand All @@ -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);
});
});
}
77 changes: 77 additions & 0 deletions packages/envied_generator/lib/src/generate_line_encrypted.dart
Original file line number Diff line number Diff line change
@@ -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<int> $keyName = [${key.join(", ")}];\n'
'static const List<int> $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,
);
}
}
10 changes: 9 additions & 1 deletion packages/envied_generator/lib/src/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -32,6 +33,7 @@ class EnviedGenerator extends GeneratorForAnnotation<Envied> {
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) {
Expand All @@ -48,15 +50,21 @@ class EnviedGenerator extends GeneratorForAnnotation<Envied> {
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,
);
Expand Down
30 changes: 30 additions & 0 deletions packages/envied_generator/test/src/generator_tests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,33 @@ abstract class Env11 {
@EnviedField(varName: 'test_string')
static const String? testString = null;
}

@ShouldGenerate('static const List<int> _enviedkeytestString', contains: true)
@ShouldGenerate('static const List<int> _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<int> _enviedkeytestString', contains: true)
@ShouldGenerate('static const List<int> _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;
}