Skip to content
Open
Show file tree
Hide file tree
Changes from 64 commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
7448db9
Update requirements.txt
ikostan Apr 28, 2026
8fed052
Update README.md
ikostan Apr 28, 2026
4bb7121
Implement Encryption Key Management for Config Files #529
ikostan Apr 28, 2026
9f94f42
[FEATURE] Encrypt Game Settings Save/Load logic (globals.gd) #530
ikostan Apr 28, 2026
f9ad750
Update audio_manager.gd
ikostan Apr 28, 2026
019b4ad
[FEATURE] Encrypt Input Mappings Save/Load logic (settings.gd)
ikostan Apr 28, 2026
f33d2b1
Update globals.gd
ikostan Apr 28, 2026
52311cf
issue (bug_risk): Loading input mappings only via encrypted config ri…
ikostan Apr 28, 2026
31057fb
issue: Audio volume configs are now encrypted-only, which may orphan …
ikostan Apr 28, 2026
a07480c
Write the salt under the [game] section, not [application].
ikostan Apr 28, 2026
02b0f91
428-430: Avoid silent weak-key fallback in non-debug builds
ikostan Apr 28, 2026
176c97f
Update globals.gd
ikostan Apr 28, 2026
bba890d
Update globals.gd
ikostan Apr 28, 2026
b4839df
update the manual setup in test_settings.gd to save the mock files us…
ikostan Apr 28, 2026
9fac881
update the two failing test methods in test_audio_manager.gd
ikostan Apr 28, 2026
7340c2c
need to update the mock file creation and verification steps
ikostan Apr 28, 2026
9503b91
Update test_settings_persistence.gd
ikostan Apr 28, 2026
8940baf
Update test_sfx_weapon_volume_control.gd
ikostan Apr 28, 2026
3c4b502
Update test_settings.gd
ikostan Apr 29, 2026
89c78d4
Update test_settings.gd
ikostan Apr 29, 2026
3944a4a
Add a guard to prevent overwriting existing files when both loads fail.
ikostan Apr 29, 2026
101c108
make the system completely bulletproof against initialization race co…
ikostan Apr 29, 2026
99e6134
Update test_audio_reset_button.gd
ikostan Apr 30, 2026
0102407
Update test_basic_save_load_without_other_settings.gd
ikostan Apr 30, 2026
6521af5
Update test_blank_key_labels_on_missing_config.gd
ikostan Apr 30, 2026
82fbded
Update test_audio_sync_decoupling.gd
ikostan Apr 30, 2026
69b19f6
Update test_deduplication_on_load.gd
ikostan Apr 30, 2026
4c9b18d
Update test_combined_multi_manager_loads.gd
ikostan Apr 30, 2026
8ddbc8c
Update test_deduplication_on_migration.gd
ikostan Apr 30, 2026
857c788
Update test_deduplication_on_reset.gd
ikostan Apr 30, 2026
197aa46
Update test_deduplication_on_save_load_cycle.gd
ikostan Apr 30, 2026
837e735
Update test_error_edge_cases.gd
ikostan Apr 30, 2026
d346639
Update test_fuel_integration.gd
ikostan Apr 30, 2026
50a526e
Update test_fuel_persistence_integration.gd
ikostan Apr 30, 2026
fe8d865
Update test_globals_resource.gd
ikostan Apr 30, 2026
9828191
fixing helper method
ikostan Apr 30, 2026
6795717
Update globals.gd
ikostan Apr 30, 2026
199a217
Update gut_test_helper.gd
ikostan Apr 30, 2026
06f8dc3
Update test_integration_key_mapping.gd
ikostan Apr 30, 2026
ec4db4f
Update test_manual_duplicate_load.gd
ikostan Apr 30, 2026
b7ffb32
Update test_master_volume_control_and_music.gd
ikostan Apr 30, 2026
80d9303
Update test_preserve_other_sections.gd
ikostan Apr 30, 2026
4317f75
Update test_master_volume_control_and_music.gd
ikostan Apr 30, 2026
af57dea
Update test_reset_scenarios.gd
ikostan Apr 30, 2026
1773e39
Update test_settings_ec.gd
ikostan Apr 30, 2026
7a99fe6
Update test_settings_observer.gd
ikostan Apr 30, 2026
db4994a
Update test_settings_unbound_scenarios.gd
ikostan Apr 30, 2026
ef5dc4c
Update test_audio_web_bridge.gd
ikostan Apr 30, 2026
d51076e
Update test/gut/test_deduplication_on_migration.gd
ikostan Apr 30, 2026
50d069b
Merge branch 'implement-encryption-key-management-for-config-files' o…
ikostan Apr 30, 2026
c3858f9
Update test/gut/test_manual_duplicate_load.gd
ikostan Apr 30, 2026
27e775a
Merge branch 'implement-encryption-key-management-for-config-files' o…
ikostan Apr 30, 2026
4200666
Update test_settings_unbound_scenarios.gd
ikostan Apr 30, 2026
9b7d09b
Update test_settings_unbound_scenarios.gd
ikostan Apr 30, 2026
6336864
Update test_master_volume_control_and_music.gd
ikostan Apr 30, 2026
2d287bb
Update test_sfx_volume_control.gd
ikostan Apr 30, 2026
a823170
Update test_sfx_rotor_volume_control.gd
ikostan Apr 30, 2026
5435640
Update test_sfx_weapon_volume_control.gd
ikostan Apr 30, 2026
9cbb838
Update settings.gd
ikostan Apr 30, 2026
1e87e57
Update audio_manager.gd
ikostan Apr 30, 2026
5e94cd9
Add GUT unit tests that specifically cover migration from plaintext t…
ikostan Apr 30, 2026
7c056f6
Update test_settings_migration.gd
ikostan Apr 30, 2026
3d5af68
Update settings.gd
ikostan Apr 30, 2026
e94b916
Update audio_manager.gd
ikostan Apr 30, 2026
980610f
Update test_sfx_rotor_volume_control.gd
ikostan Apr 30, 2026
0be7c9b
Update test_sfx_volume_control.gd
ikostan Apr 30, 2026
5893906
Update difficulty_flow_test.py
ikostan Apr 30, 2026
877f071
style: format code with Black and isort
deepsource-autofix[bot] Apr 30, 2026
e309246
🚨 issue (security): Returning an empty string as a "hard fail" key ma…
ikostan Apr 30, 2026
f4e9bab
suggestion (bug_risk): The salt injection step is tightly coupled to …
ikostan Apr 30, 2026
e2eec15
Classic "DRY" (Don't Repeat Yourself) refactoring opportunity.
ikostan Apr 30, 2026
2d03d58
Update globals.gd
ikostan Apr 30, 2026
c7b76a2
Update audio_manager.gd
ikostan Apr 30, 2026
d4f427b
Refactoring
ikostan Apr 30, 2026
0789bd5
Update settings.gd
ikostan Apr 30, 2026
e31d692
Update test_error_edge_cases.gd
ikostan Apr 30, 2026
721e7d5
Update test_settings_ec.gd
ikostan Apr 30, 2026
59f891b
Update test_settings.gd
ikostan Apr 30, 2026
3a6074d
Update globals.gd
ikostan Apr 30, 2026
fd0eeb2
Fixing Playwright tests
ikostan May 1, 2026
e5c2b7f
Update globals.gd
ikostan May 1, 2026
0ea599b
Still working on playwright tsts
ikostan May 1, 2026
bf0b27e
ditch the feature flags entirely and make your game self-aware
ikostan May 1, 2026
a988c35
style: format code with Black and isort
deepsource-autofix[bot] May 1, 2026
b7ab971
Update globals.gd
ikostan May 1, 2026
b8464dc
Update settings.gd
ikostan May 1, 2026
a8f939b
issue (bug_risk): Guard against failed config loads before overwritin…
ikostan May 1, 2026
509fb31
Update settings.gd
ikostan May 1, 2026
87cff8b
Update test_settings_migration.gd
ikostan May 1, 2026
8687b8f
suggestion: Consider centralizing these hard-coded timeouts so browse…
ikostan May 1, 2026
ca7c5b4
style: format code with Black and isort
deepsource-autofix[bot] May 1, 2026
ce74c3e
Update audio_flow_test.py
ikostan May 1, 2026
d6bb969
suggestion: Consider centralizing these hard-coded timeouts so browse…
ikostan May 1, 2026
2ba79fd
Merge branch 'implement-encryption-key-management-for-config-files' o…
ikostan May 1, 2026
8f43171
style: format code with Black and isort
deepsource-autofix[bot] May 1, 2026
ab3f338
suggestion (testing): This assertion is tightly coupled to a specific…
ikostan May 2, 2026
8cffe3b
suggestion (bug_risk): Escape or constrain the salt value to avoid br…
ikostan May 2, 2026
49170dc
Update deploy_to_itch.yml
ikostan May 2, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/deploy_to_itch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ on: # yamllint disable-line rule:truthy
ITCHIO_API_KEY:
description: "Itch.io API key"
required: true
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
PRODUCTION_SALT:
description: "production salt secret key"
required: true

jobs:
export-and-deploy:
Expand All @@ -38,6 +41,19 @@ jobs:
else
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
printf '\n[application]\nconfig/version="%s"\n' "$ESCAPED_VERSION" >> project.godot
fi
- name: "Inject Production Salt into project.godot"
run: |
SALT="${{ secrets.PRODUCTION_SALT }}"
# Check if the specific key already exists to update it
if grep -q '^security/save_salt=' project.godot; then
sed -i "s|^security/save_salt=.*$|security/save_salt=\"$SALT\"|g" project.godot
# If the key doesn't exist but the [game] section does, append it there
elif grep -q '^\[game\]' project.godot; then
sed -i "/^\[game\]/a security\/save_salt=\"$SALT\"" project.godot
# Fallback: create the [game] section and the key if both are missing
else
printf '\n[game]\nsecurity/save_salt="%s"\n' "$SALT" >> project.godot
fi
- name: "Create Export Directories"
run: |
mkdir -p export/web
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/lint_test_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ jobs:
version: ${{ needs.release_drafter.outputs.release_tag }}
secrets:
ITCHIO_API_KEY: "${{ secrets.ITCHIO_API_KEY }}"
PRODUCTION_SALT: "${{ secrets.PRODUCTION_SALT }}"
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
![Repo Size](https://img.shields.io/github/repo-size/ikostan/SkyLockAssault?style=flat-square)
![Closed Issues](https://img.shields.io/github/issues-closed/ikostan/SkyLockAssault?style=flat-square&label=Issues&color=green)
![Open Issues](https://img.shields.io/github/issues/ikostan/SkyLockAssault?style=flat-square&label=Issues&color=red)
[![Known Vulnerabilities](https://snyk.io/test/github/ikostan/SkyLockAssault/badge.svg)](https://snyk.io/test/github/ikostan/SkyLockAssault)
[![All Contributors](https://img.shields.io/github/all-contributors/ikostan/SkyLockAssault?color=ee8449&style=flat-square)](#contributors)

## A top-down online web browser game built with Godot 4.5
Expand Down
Binary file modified requirements.txt
Binary file not shown.
160 changes: 112 additions & 48 deletions scripts/core/globals.gd
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ enum LogLevel { DEBUG, INFO, WARNING, ERROR, NONE = 4 }
## Path to the navigation sound file
const UI_NAV_SOUND_PATH: String = "res://files/sounds/sfx/ui_navigation.wav"

# --- TASK #529: Encryption Key Management ---
## Centralized key for securing local configuration files.
## This ensures consistent encryption/decryption across different game systems. [cite: 3]
## Define the variable by pulling from ProjectSettings.
## If the setting doesn't exist, it falls back to a non-secure string.
var save_encryption_pass: String = _get_encryption_key()

# Add the resource reference here
var settings: GameSettingsResource
# In globals.gd (add after @export vars)
Expand Down Expand Up @@ -158,100 +165,121 @@ func load_key_mapping(menu_to_hide: Node) -> void:
get_tree().root.add_child(km_instance)


## Loads persisted settings from config if valid types;
## skips invalid/missing to keep current.
## Loads persisted settings with backward compatibility for plaintext files.
## :param path: Config file path (default: Settings.CONFIG_PATH).
## skips invalid/missing to keep current.
## :type path: String
## :rtype: void
func _load_settings(path: String = Settings.CONFIG_PATH) -> void:
var config: ConfigFile = ConfigFile.new()
var err: int = config.load(path)

# Ensure the key is ready before we even try
if save_encryption_pass.is_empty():
save_encryption_pass = _get_encryption_key()

# Step 1: Attempt to load with encryption
var err: int = config.load_encrypted_pass(path, save_encryption_pass)
var needs_migration: bool = false

# Step 2: Migration Check
# We ONLY fallback if the error is 15 (Invalid Data) or 43 (Corrupt)
# because that indicates it might be plaintext.
if err == ERR_INVALID_DATA or err == ERR_FILE_CORRUPT:
log_message(
"Encrypted load failed (Code %d). Checking if file is legacy plaintext..." % err,
LogLevel.DEBUG
)

# Reset config object before trying a different load method
config = ConfigFile.new()
err = config.load(path)

if err == OK:
log_message("Legacy plaintext settings found. Migration required.", LogLevel.INFO)
needs_migration = true
else:
log_message("File is not valid plaintext either. Abandoning load.", LogLevel.ERROR)

if err == OK:
# Enable the guard before starting bulk updates
_is_loading_settings = true

# Load Log Level
if config.has_section_key("Settings", "log_level"):
var loaded_log_level: Variant = config.get_value("Settings", "log_level")
if (
loaded_log_level is int
and loaded_log_level >= LogLevel.DEBUG
and loaded_log_level <= LogLevel.NONE
):
if loaded_log_level is int and loaded_log_level >= 0 and loaded_log_level <= 4:
settings.current_log_level = loaded_log_level
log_message(
"Loaded saved log level: " + LogLevel.keys()[settings.current_log_level],
LogLevel.DEBUG
)
else:
log_message(
"Invalid type or value for log_level: " + str(typeof(loaded_log_level)),
LogLevel.WARNING
)

# Load Difficulty
if config.has_section_key("Settings", "difficulty"):
var loaded_difficulty: Variant = config.get_value("Settings", "difficulty")
if (loaded_difficulty is float) or (loaded_difficulty is int):
# Validate and clamp difficulty to slider range (0.5-2.0)
settings.difficulty = loaded_difficulty
log_message("Loaded saved difficulty: " + str(settings.difficulty), LogLevel.DEBUG)
else:
log_message(
"Invalid type for difficulty: " + str(typeof(loaded_difficulty)),
LogLevel.WARNING
)

# NEW: Load the debug logging flag

# Load Debug Logging Flag
if config.has_section_key("Settings", "enable_debug_logging"):
var loaded_debug: Variant = config.get_value("Settings", "enable_debug_logging")
if loaded_debug is bool:
settings.enable_debug_logging = loaded_debug
log_message(
"Loaded saved debug logging: " + str(settings.enable_debug_logging),
LogLevel.DEBUG
)

# NEW: Load the fuel related settings
# Load Fuel Settings
if config.has_section_key("Settings", "max_fuel"):
var loaded_max: Variant = config.get_value("Settings", "max_fuel")
if loaded_max is float or loaded_max is int:
settings.max_fuel = float(loaded_max)
else:
log_message(
"Invalid type for max_fuel: " + str(typeof(loaded_max)), LogLevel.WARNING
)

# Disable the guard and log a single summary instead
_is_loading_settings = false
log_message("All settings loaded and synchronized.", LogLevel.DEBUG)
log_message("Settings synchronization complete.", LogLevel.DEBUG)

# Step 3: Immediate Upgrade
if needs_migration:
log_message("Upgrading settings file to encrypted format...", LogLevel.INFO)
_save_settings(path)

elif err == ERR_FILE_NOT_FOUND:
log_message("No settings config found, using defaults.", LogLevel.DEBUG)
log_message("No configuration file found; using defaults.", LogLevel.DEBUG)
else:
log_message("Failed to load settings config: " + str(err), LogLevel.ERROR)
log_message("Failed to load settings (Error %d)." % err, LogLevel.ERROR)


# New: Add _save_settings to globals.gd (move from options_menu.gd if needed)
## New: Add _save_settings to globals.gd (move from options_menu.gd if needed)
## Persists current settings to an encrypted config file.
## :param path: Config file path (default: Settings.CONFIG_PATH).
func _save_settings(path: String = Settings.CONFIG_PATH) -> void:
var config: ConfigFile = ConfigFile.new()
var err: int = config.load(path) # Load existing to preserve other sections

# Use the same dual-load logic to ensure we preserve all sections
var err: int = config.load_encrypted_pass(path, save_encryption_pass)
if err != OK and err != ERR_FILE_NOT_FOUND:
# Fallback to plaintext load to ensure we don't overwrite blindly
err = config.load(path)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

# SECURITY GUARD: Prevent overwriting existing files when both loads fail.
# If the file exists but we can't read it (corrupted, locked, etc.),
# aborting prevents us from wiping out the audio and input sections.
if err != OK and err != ERR_FILE_NOT_FOUND:
log_message(
"Failed to load settings from " + path + " for save: " + str(err), LogLevel.ERROR
(
"CRITICAL: Could not load settings from "
+ path
+ ", aborting save to prevent data loss."
),
LogLevel.ERROR
)
return

# Update values in the ConfigFile object
config.set_value("Settings", "log_level", settings.current_log_level)
config.set_value("Settings", "difficulty", settings.difficulty)
# NEW: Persist the debug logging flag
config.set_value("Settings", "enable_debug_logging", settings.enable_debug_logging)
# NEW: Persist the fuel settings
config.set_value("Settings", "max_fuel", settings.max_fuel)

err = config.save(path)
# Always save using encryption from this point forward
err = config.save_encrypted_pass(path, save_encryption_pass)

if err != OK:
log_message("Failed to save settings: " + str(err), LogLevel.ERROR)
log_message("CRITICAL: Failed to save encrypted settings: " + str(err), LogLevel.ERROR)
else:
log_message("Settings saved.", LogLevel.DEBUG)
log_message("Encrypted settings persisted successfully.", LogLevel.DEBUG)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


func _on_options_exited_unexpectedly() -> void:
Expand Down Expand Up @@ -410,3 +438,39 @@ func _play_ui_navigation_sfx() -> void:
# If the sound is already playing (e.g., from rapid button presses),
# restart it from the beginning to feel responsive.
_nav_sfx_player.play()


## Generates a unique, deterministic encryption key for local save files.
##
## This function combines the device's hardware ID (`OS.get_unique_id()`) with a
## project-specific salt retrieved from `ProjectSettings`, returning a SHA-256 hash.
##
## Security Guard:
## In production builds (when neither 'editor' nor 'debug' features are present),
## this function strictly validates that a secure salt was successfully injected
## during the CI/CD deployment. If the salt is missing or matches the weak development
## fallback, it logs a critical error and returns an empty string. This intentionally
## breaks downstream `load_encrypted_pass` and `save_encrypted_pass` calls to prevent
## the game from persisting weakly-encrypted user data.
##
## :rtype: String (The SHA-256 hashed key, or an empty string if production validation fails)
func _get_encryption_key() -> String:
# Fetches the salt injected by GitHub Actions or uses the dev fallback
var salt: String = ProjectSettings.get_setting("game/security/save_salt", "dev_fallback_salt")

# SECURITY GUARD: Prevent silent weak-key fallback in production
if not OS.has_feature("editor") and not OS.has_feature("debug"):
if salt == "dev_fallback_salt" or salt.is_empty():
# Log the critical failure
log_message(
(
"CRITICAL SECURITY ERROR: Production build missing injected salt. "
+ "Refusing to generate weak key."
),
LogLevel.ERROR
)
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated
# Returning an empty string ensures load_encrypted_pass and
# save_encrypted_pass immediately fail, refusing persistence.
return ""

return (OS.get_unique_id() + salt).sha256_text()
Loading
Loading