Skip to content
Draft
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
72 changes: 70 additions & 2 deletions lib/providers/js_runtime_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,36 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
late final JavascriptRuntime _runtime;
String? _currentRequestId;

// Security: Maximum script length to prevent DoS attacks
static const int _maxScriptLength = 50000; // 50KB

// Security: Dangerous JavaScript patterns that could lead to code injection
static const List<String> _dangerousPatterns = [
r'eval\s*\(',
r'Function\s*\(',
r'constructor\s*\[',
r'__proto__',
];

/// Validates user script for basic security checks
/// Returns null if valid, error message if invalid
String? _validateScript(String script) {
// Check script length to prevent DoS
if (script.length > _maxScriptLength) {
return 'Script exceeds maximum length of $_maxScriptLength characters';
}

// Check for dangerous patterns
for (final pattern in _dangerousPatterns) {
final regex = RegExp(pattern, caseSensitive: false);
if (regex.hasMatch(script)) {
return 'Script contains potentially dangerous pattern: ${pattern.replaceAll(r'\s*\(', '(').replaceAll(r'\s*\[', '[')}';
}
}

return null; // Script is valid
}

void _initialize() {
if (state.initialized) return;
_runtime = getJavascriptRuntime();
Expand Down Expand Up @@ -100,7 +130,26 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
}

final httpRequest = currentRequestModel.httpRequestModel;
final userScript = currentRequestModel.preRequestScript;
final userScript = currentRequestModel.preRequestScript!;

// Security: Validate user script before execution
final validationError = _validateScript(userScript);
if (validationError != null) {
final term = ref.read(terminalStateProvider.notifier);
term.logJs(
level: 'error',
args: ['Script validation failed', validationError],
context: 'preRequest',
contextRequestId: requestId,
);
state = state.copyWith(lastError: validationError);
// Return original request without executing the script
return (
updatedRequest: httpRequest!,
updatedEnvironment: activeEnvironment,
);
}

final requestJson = jsonEncode(httpRequest?.toJson());
final environmentJson = jsonEncode(activeEnvironment);
final dataInjection = '''
Expand Down Expand Up @@ -190,7 +239,26 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {

final httpRequest = currentRequestModel.httpRequestModel; // for future use
final httpResponse = currentRequestModel.httpResponseModel;
final userScript = currentRequestModel.postRequestScript;
final userScript = currentRequestModel.postRequestScript!;

// Security: Validate user script before execution
final validationError = _validateScript(userScript);
if (validationError != null) {
final term = ref.read(terminalStateProvider.notifier);
term.logJs(
level: 'error',
args: ['Script validation failed', validationError],
context: 'postResponse',
contextRequestId: requestId,
);
state = state.copyWith(lastError: validationError);
// Return original response without executing the script
return (
updatedResponse: httpResponse!,
updatedEnvironment: activeEnvironment,
);
}

final requestJson = jsonEncode(httpRequest?.toJson());
final responseJson = jsonEncode(httpResponse?.toJson());
final environmentJson = jsonEncode(activeEnvironment);
Expand Down
106 changes: 103 additions & 3 deletions lib/services/hive_services.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'secure_credential_storage.dart';

enum HiveBoxType { normal, lazy }

Expand Down Expand Up @@ -127,11 +128,110 @@ class HiveHandler {
environmentBox.put(kKeyEnvironmentBoxIds, ids);

dynamic getEnvironment(String id) => environmentBox.get(id);

/// Sets environment with automatic encryption of secrets
Future<void> setEnvironment(
String id, Map<String, dynamic>? environmentJson) =>
environmentBox.put(id, environmentJson);
String id, Map<String, dynamic>? environmentJson) async {
if (environmentJson == null) {
return environmentBox.put(id, null);
}

// Create a copy to avoid modifying the original
final secureEnvData = Map<String, dynamic>.from(environmentJson);

// Check if values array exists and process secrets
if (secureEnvData['values'] is List) {
final values = secureEnvData['values'] as List;

for (var i = 0; i < values.length; i++) {
final variable = values[i];

if (variable is Map &&
variable['type'] == 'secret' &&
variable['value'] != null &&
variable['value'].toString().isNotEmpty) {

// Store secret in secure storage
try {
await SecureCredentialStorage.storeEnvironmentSecret(
environmentId: id,
variableKey: variable['key'] ?? 'unknown_$i',
value: variable['value'].toString(),
);

// Replace value with placeholder in Hive
secureEnvData['values'][i] = {
...variable,
'value': '***SECURE***',
'isEncrypted': true,
};
} catch (e) {
// If secure storage fails, keep original value but log
// In production, consider proper error handling
}
}
}
}

return environmentBox.put(id, secureEnvData);
}

/// Gets environment with automatic decryption of secrets
Future<Map<String, dynamic>?> getEnvironmentSecure(String id) async {
final data = environmentBox.get(id);
if (data == null) return null;

// Create a copy to modify
final envData = Map<String, dynamic>.from(data);

Future<void> deleteEnvironment(String id) => environmentBox.delete(id);
// Process encrypted values
if (envData['values'] is List) {
final values = List.from(envData['values']);

for (var i = 0; i < values.length; i++) {
final variable = values[i];

if (variable is Map &&
variable['isEncrypted'] == true &&
variable['type'] == 'secret') {

// Retrieve secret from secure storage
try {
final decryptedValue = await SecureCredentialStorage.retrieveEnvironmentSecret(
environmentId: id,
variableKey: variable['key'] ?? 'unknown_$i',
);

if (decryptedValue != null) {
values[i] = {
...variable,
'value': decryptedValue,
'isEncrypted': false,
};
}
} catch (e) {
// If decryption fails, keep placeholder
}
}
}

envData['values'] = values;
}

return envData;
}

Future<void> deleteEnvironment(String id) async {
// Clean up secure storage for this environment
try {
await SecureCredentialStorage.clearEnvironmentSecrets(
environmentId: id,
);
} catch (e) {
// Log error but continue with deletion
}
return environmentBox.delete(id);
}

dynamic getHistoryIds() => historyMetaBox.get(kHistoryBoxIds);
Future<void> setHistoryIds(List<String>? ids) =>
Expand Down
114 changes: 114 additions & 0 deletions lib/services/secure_credential_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:crypto/crypto.dart';

/// Service for securely storing and retrieving OAuth2 credentials
/// Uses flutter_secure_storage for encryption keys and encrypted values
class SecureCredentialStorage {
static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(
encryptedSharedPreferences: true,
),
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);

/// Generates a storage key from client credentials for OAuth2
static String _generateStorageKey(String clientId, String tokenUrl) {
final combined = '$clientId:$tokenUrl';
final bytes = utf8.encode(combined);
final hash = sha256.convert(bytes);
return 'oauth2_${hash.toString().substring(0, 16)}';
}

/// Store OAuth2 credentials securely
static Future<void> storeOAuth2Credentials({
required String clientId,
required String tokenUrl,
required String credentialsJson,
}) async {
final key = _generateStorageKey(clientId, tokenUrl);
await _secureStorage.write(key: key, value: credentialsJson);
}

/// Retrieve OAuth2 credentials securely
static Future<String?> retrieveOAuth2Credentials({
required String clientId,
required String tokenUrl,
}) async {
final key = _generateStorageKey(clientId, tokenUrl);
return await _secureStorage.read(key: key);
}

/// Delete OAuth2 credentials
static Future<void> deleteOAuth2Credentials({
required String clientId,
required String tokenUrl,
}) async {
final key = _generateStorageKey(clientId, tokenUrl);
await _secureStorage.delete(key: key);
}

/// Clear all OAuth2 credentials
static Future<void> clearAllOAuth2Credentials() async {
final allKeys = await _secureStorage.readAll();
for (final key in allKeys.keys) {
if (key.startsWith('oauth2_')) {
await _secureStorage.delete(key: key);
}
}
}

/// Store environment variable securely (for secrets)
static Future<void> storeEnvironmentSecret({
required String environmentId,
required String variableKey,
required String value,
}) async {
final key = 'env_${environmentId}_$variableKey';
await _secureStorage.write(key: key, value: value);
}

/// Retrieve environment variable secret
static Future<String?> retrieveEnvironmentSecret({
required String environmentId,
required String variableKey,
}) async {
final key = 'env_${environmentId}_$variableKey';
return await _secureStorage.read(key: key);
}

/// Delete environment variable secret
static Future<void> deleteEnvironmentSecret({
required String environmentId,
required String variableKey,
}) async {
final key = 'env_${environmentId}_$variableKey';
await _secureStorage.delete(key: key);
}

/// Clear all environment secrets for a specific environment
static Future<void> clearEnvironmentSecrets({
required String environmentId,
}) async {
final allKeys = await _secureStorage.readAll();
final prefix = 'env_${environmentId}_';
for (final key in allKeys.keys) {
if (key.startsWith(prefix)) {
await _secureStorage.delete(key: key);
}
}
}

/// Check if secure storage is available
static Future<bool> isSecureStorageAvailable() async {
try {
await _secureStorage.read(key: '__test__');
return true;
} catch (e) {
return false;
}
}
}
Loading