Skip to content

Implement encryption key management for config files#588

Open
ikostan wants to merge 98 commits intomainfrom
implement-encryption-key-management-for-config-files
Open

Implement encryption key management for config files#588
ikostan wants to merge 98 commits intomainfrom
implement-encryption-key-management-for-config-files

Conversation

@ikostan
Copy link
Copy Markdown
Owner

@ikostan ikostan commented Apr 28, 2026


name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''

Description

What does this PR do? (e.g., "Fixes player jump physics in level 2" or "Adds
new enemy AI script")

Related Issue

Closes #ISSUE_NUMBER (if applicable)

Changes

  • List key changes here (e.g., "Updated Jump.gd to use Godot 4.4's new Tween
    system")
  • Any breaking changes? (e.g., "Deprecated old signal; migrate to new one")

Testing

  • Ran the game in Godot v4.5 editor—describe what you tested (e.g., "Jump
    works on Win10 with 60 FPS")
  • Any new unit tests added? (Link to test scene if yes)
  • Screenshots/GIFs if UI-related: (Attach below)

Checklist

  • Code follows Godot style guide (e.g., snake_case for variables)
  • No console errors in editor/output
  • Ready for review!

Additional Notes

Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")

Summary by Sourcery

Introduce centralized encryption key management for game configuration and migrate all config persistence to use encrypted files while preserving backward compatibility with legacy plaintext configs.

New Features:

  • Generate a deterministic per-device encryption key from project salt and hardware ID for securing local configuration files.
  • Provide centralized helpers for safely loading either encrypted or legacy plaintext ConfigFiles and for managing the active encryption key.
  • Support deterministic test encryption keys and web automation detection to keep browser-based tests stable.

Enhancements:

  • Encrypt all settings, input mappings, audio volumes, and last input device preferences when saving to disk, with automatic migration from legacy plaintext files.
  • Preserve unrelated config sections during encrypted saves to keep multi-writer configuration workflows lossless.
  • Expose a JavaScript-ready engine initialization flag on web builds and relax Playwright timeouts for more reliable browser-based flows.
  • Harden input mapping loading against malformed data and ensure legacy migration flags and defaults behave correctly after encryption changes.

CI:

  • Inject a production-only security salt into project.godot during Itch.io deploys and thread it through the shared encryption key helper.
  • Propagate the new PRODUCTION_SALT secret into the main lint/test/deploy workflow for release builds.

Documentation:

  • Remove the external vulnerability scanning badge from the README to simplify project badges.

Tests:

  • Update all Godot GUT and GdUnit tests plus Python Playwright flows to work with encrypted config files instead of plaintext, including new coverage for migration and error handling paths.
  • Add dedicated tests for plaintext-to-encrypted migration, multi-writer preservation, and logically corrupt-but-decryptable config inputs.
  • Stabilize UI/audio tests by awaiting frame processing, fixing mock resources, quieting log noise, and adjusting expectations for new encrypted save log messages.

Summary by CodeRabbit

  • New Features

    • Settings, input mappings, and audio preferences now persist encrypted by default and will be auto-migrated from legacy plaintext files.
  • Chores

    • Deployment workflows now require a production salt secret and forward it to deployment; missing or non-production salts disable encrypted persistence in production-like builds.
  • Tests

    • Test suite updated to read/write configs using the new encrypted persistence approach.
  • Documentation

    • Removed the known-vulnerabilities badge from the README.

ikostan added 6 commits April 27, 2026 17:20
Implement Task #529: Encryption Key ManagementThis commit introduces a centralized security layer for local configuration files.Changes:
* globals.gd: Added save_encryption_pass which fetches a salt from ProjectSettings and combines it with the hardware ID for a unique, deterministic key.
* Persistence: Refactored _load_settings and _save_settings to utilize load_encrypted_pass and save_encrypted_pass respectively.  CI/CD: Split the version injection into two steps in deploy_to_itch.yml and added a step to inject PRODUCTION_SALT from GitHub Secrets.
* Style: Fixed naming conventions to satisfy gdlint class-variable-name requirements.
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 28, 2026

Reviewer's Guide

Introduce centralized encryption key management and migrate all config persistence (settings, input mappings, audio) to encrypted files with backward-compatible loading, CI-injected production salt, and extensive test updates for encrypted I/O and migration flows.

Sequence diagram for loading and migrating settings from plaintext to encrypted

sequenceDiagram
    actor Player
    participant Game as GameStartup
    participant Globals as Globals
    participant Config as ConfigFile
    participant FS as FileAccess

    Player->>Game: Start game
    Game->>Globals: _load_settings(path)
    Globals->>Globals: safe_load_config(path)
    alt file_missing
        FS->>Globals: file_exists(path) = false
        Globals-->>Globals: return {config, ERR_FILE_NOT_FOUND, is_legacy false}
        Globals-->>Game: err = ERR_FILE_NOT_FOUND
        Game-->>Player: Use default settings
    else file_exists
        FS->>Globals: file_exists(path) = true
        Globals->>Globals: is_file_encrypted(path)
        alt encrypted_file
            Globals->>Config: load_encrypted_pass(path, ensure_encryption_key())
            Config-->>Globals: err = OK, is_legacy = false
        else legacy_plaintext
            Globals->>Config: load(path)
            Config-->>Globals: err = OK, is_legacy = true
        end
        Globals-->>Game: {config, err, is_legacy}
        alt err == OK
            Game->>Game: apply settings from config
            alt is_legacy == true
                Game->>Globals: log_message(Migration required)
                Game->>Globals: _save_settings(path)
                Globals->>Globals: safe_load_config(path)
                Globals->>Config: set_value(Settings, ...)
                Globals->>Config: save_encrypted_pass(path, ensure_encryption_key())
                Config-->>Globals: err = OK
            else already_encrypted
                Game->>Globals: log_message(Settings synchronized)
            end
        else err != OK and err != ERR_FILE_NOT_FOUND
            Game->>Globals: log_message(load error)
            Game-->>Player: Continue with in-memory defaults
        end
    end
Loading

Sequence diagram for saving encrypted input mappings and audio settings

sequenceDiagram
    actor Player
    participant Settings as SettingsScript
    participant Audio as AudioManager
    participant Globals as Globals
    participant Config as ConfigFile

    Player->>Settings: Change key bindings
    Settings->>Settings: InputMap updated
    Player->>Audio: Adjust volumes
    Audio->>Audio: Bus states updated

    rect rgb(230,230,255)
        Settings->>Settings: save_input_mappings(path)
        Settings->>Globals: safe_load_config(path)
        Globals-->>Settings: {config, err, is_legacy}
        alt err == OK or ERR_FILE_NOT_FOUND
            Settings->>Config: set_value(input, action, serials)
            Settings->>Globals: ensure_encryption_key()
            Globals-->>Settings: encryption_key
            Settings->>Config: save_encrypted_pass(path, encryption_key)
            Config-->>Settings: err = OK
        else load_error
            Settings->>Globals: log_message(load error)
        end
    end

    rect rgb(230,255,230)
        Audio->>Audio: save_volumes(path)
        Audio->>Globals: safe_load_config(path)
        Globals-->>Audio: {config, err, is_legacy}
        alt err == OK or ERR_FILE_NOT_FOUND
            Audio->>Config: set_value(audio, volume_var, volume)
            Audio->>Config: set_value(audio, muted_var, muted)
            Audio->>Globals: ensure_encryption_key()
            Globals-->>Audio: encryption_key
            Audio->>Config: save_encrypted_pass(path, encryption_key)
            Config-->>Audio: err = OK
        else load_error
            Audio->>Globals: log_message(load error)
        end
    end
Loading

Class diagram for encrypted config management and key handling

classDiagram
    class Globals {
        <<node>>
        +LogLevel enum
        +save_encryption_pass String
        +settings GameSettingsResource
        +_ready() void
        +_load_settings(path String) void
        +_save_settings(path String) void
        +ensure_encryption_key() String
        +_get_encryption_key() String
        +is_file_encrypted(path String) bool
        +safe_load_config(path String) Dictionary
        +set_test_encryption_key(override_key String) void
    }

    class SettingsScript {
        <<script>>
        +CONFIG_PATH String
        +ACTIONS Array~String~
        +LEGACY_MIGRATION_KEY String
        +_needs_save bool
        +load_input_mappings(path String, actions Array~String~) void
        +save_input_mappings(path String, actions Array~String~) void
        +save_last_input_device(device String) void
        +load_last_input_device() void
        +_is_file_encrypted(path String) bool
    }

    class AudioManager {
        <<script>>
        +current_config_path String
        +load_volumes(path String) void
        +save_volumes(path String) void
        +set_bus_state(bus String, volume float, muted bool) void
        +get_bus_state(bus String) Dictionary
    }

    class ConfigFile {
        +load(path String) int
        +save(path String) int
        +load_encrypted_pass(path String, key String) int
        +save_encrypted_pass(path String, key String) int
        +has_section_key(section String, key String) bool
        +get_value(section String, key String, default Variant) Variant
        +set_value(section String, key String, value Variant) void
    }

    class FileAccess {
        +file_exists(path String) bool
        +open(path String, mode int) FileAccess
        +get_length() int
        +get_32() int
        +close() void
    }

    class ProjectSettings {
        +get_setting(key String, default Variant) Variant
    }

    class OS {
        +has_feature(feature String) bool
        +get_name() String
        +get_unique_id() String
        +crash(message String) void
    }

    class JavaScriptBridge {
        +eval(script String) Variant
    }

    %% Relationships
    Globals --> ConfigFile : uses
    Globals --> FileAccess : uses
    Globals --> ProjectSettings : reads_save_salt
    Globals --> OS : checks_environment
    Globals --> JavaScriptBridge : detects_webdriver

    SettingsScript --> Globals : uses_safe_load_config
    SettingsScript --> Globals : uses_ensure_encryption_key
    SettingsScript --> ConfigFile : operates_on_configs

    AudioManager --> Globals : uses_safe_load_config
    AudioManager --> Globals : uses_ensure_encryption_key
    AudioManager --> ConfigFile : operates_on_configs

    SettingsScript --> FileAccess : legacy_is_file_encrypted
Loading

File-Level Changes

Change Details Files
Centralize encryption key management and safe config loading in Globals, including CI-injected production salt and test overrides.
  • Add save_encryption_pass field and ensure_encryption_key/_get_encryption_key helpers that derive a SHA-256 key from device ID and ProjectSettings salt with production guard rails and web/test exceptions.
  • Introduce is_file_encrypted and safe_load_config helpers to transparently handle encrypted vs legacy plaintext ConfigFile loading and surface an is_legacy flag.
  • Add set_test_encryption_key for deterministic keys in tests and a web flag (window.godotInitialized) plus Playwright webdriver detection for test environments.
scripts/core/globals.gd
Migrate input settings to encrypted configs while preserving legacy formats, migration flags, and conflict handling.
  • Refactor load_input_mappings/save_input_mappings to use Globals.safe_load_config, detect legacy plaintext, set _needs_save for migration, and save via save_encrypted_pass with the centralized key.
  • Update load_last_input_device/save_last_input_device to go through safe_load_config and encrypted saves, keeping validation and defaults behavior intact.
  • Add local _is_file_encrypted helper in settings.gd for tests and migration checks, and keep existing deduplication, conflict resolution, and default backfill logic unchanged aside from I/O path.
scripts/core/settings.gd
Convert audio volume persistence to use encrypted configs with legacy detection and migration.
  • Change load_volumes to read via Globals.safe_load_config, detect legacy plaintext, optionally call save_volumes for upgrade, and maintain existing per-bus validation and application logic.
  • Change save_volumes to pre-load via safe_load_config, preserve non-audio sections, and save using save_encrypted_pass with the centralized key.
scripts/managers/audio_manager.gd
Inject production save salt via CI and deployment, wiring Godot ProjectSettings to a secret value.
  • Update deploy_to_itch workflow to require PRODUCTION_SALT and add an awk script to set game/security/save_salt in project.godot within the [game] section or create it if missing.
  • Propagate PRODUCTION_SALT through lint_test_deploy workflow secrets.
  • Adjust browser_test timeout defaults for more robust Playwright runs and keep run_browser_tests.sh export unchanged aside from a comment.
.github/workflows/deploy_to_itch.yml
.github/workflows/lint_test_deploy.yml
tests/no_error_logs_test.py
.github/workflows/browser_test.yml
run_browser_tests.sh
Align all GUT/GdUnit4 and Playwright tests with encrypted config behavior and migration semantics.
  • Replace raw ConfigFile.load/save usage across tests with load_encrypted_pass/save_encrypted_pass plus Globals.ensure_encryption_key or Globals.save_encryption_pass, including corruption, migration, and preservation scenarios.
  • Add a dedicated test_settings_migration.gd suite to cover new-install encryption, legacy plaintext fallback, automatic upgrade to encrypted, and lossless multi-writer migration across sections.
  • Refine various tests for deterministic behavior and stability: reset singleton flags (_needs_save), avoid C++ parse errors by simulating logical corruption instead of malformed files, adjust InputMap setup/teardown, mute logging where needed, await frames for UI focus, and add dummy textures to avoid sprite warnings.
  • Update Playwright flows to use longer timeouts, check for the new encrypted settings log message instead of the old "settings saved" text, and rely on window.godotInitialized set from Globals._ready().
test/gut/test_settings_ec.gd
test/gdunit4/test_settings.gd
test/gut/test_combined_multi_manager_loads.gd
test/gut/test_preserve_other_sections.gd
test/gut/test_reset_scenarios.gd
test/gut/test_settings_unbound_scenarios.gd
test/gut/test_error_edge_cases.gd
test/gut/test_integration_key_mapping.gd
test/gut/test_basic_save_load_without_other_settings.gd
test/gut/test_globals_resource.gd
test/gut/test_sfx_volume_control.gd
test/gut/test_settings_observer.gd
test/gut/test_sfx_rotor_volume_control.gd
test/gdunit4/test_audio_manager.gd
test/gdunit4/test_globals.gd
test/gut/test_master_volume_control_and_music.gd
test/gut/test_sfx_weapon_volume_control.gd
tests/difficulty_flow_test.py
tests/audio_flow_test.py
tests/back_flow_test.py
tests/load_main_menu_test.py
tests/navigation_to_audio_test.py
tests/reset_audio_flow_test.py
tests/volume_sliders_mutes_test.py
test/gut/test_audio_sync_decoupling.gd
test/gut/test_hud.gd
tests/validate_clean_load_test.py
test/gut/test_audio_reset_button.gd
test/gut/test_fuel_persistence_integration.gd
test/gut/test_fuel_integration.gd
test/gut/test_deduplication_on_load.gd
test/gut/test_deduplication_on_reset.gd
test/gdunit4/test_settings_persistence.gd
test/gut/gut_test_helper.gd
test/gut/test_audio_web_bridge.gd
test/gut/test_deduplication_on_migration.gd
test/gut/test_blank_key_labels_on_missing_config.gd
test/gut/test_deduplication_on_save_load_cycle.gd
test/gut/test_manual_duplicate_load.gd
test/gut/test_settings_migration.gd
test/gut/test_settings_migration.gd.uid
Miscellaneous maintenance: docs badge cleanup and minor test harness fixes.
  • Remove obsolete Snyk vulnerabilities badge from README to reflect current tooling.
  • Tighten various GUT setups/teardowns to avoid memory leaks (e.g., autofree prev_menu) and stub misuse, and ensure HUD/player integration tests restore global settings.
README.md
test/gut/test_sfx_volume_control.gd
test/gut/test_sfx_rotor_volume_control.gd
test/gut/test_fuel_persistence_integration.gd
test/gut/test_hud.gd

Assessment against linked issues

Issue Objective Addressed Explanation
#529 Introduce centralized encryption key management in globals.gd that derives Globals.save_encryption_pass from ProjectSettings.game/security/save_salt and OS.get_unique_id(), with deterministic per-device behavior and guard rails for production.
#529 Use the centralized encryption key to encrypt and decrypt configuration files (settings, inputs, audio, etc.), including updating _save_settings() and _load_settings() to use save_encrypted_pass()/load_encrypted_pass() (or equivalent), while keeping salts out of the repository.
#529 Integrate CI/CD so that GitHub Actions injects a production salt (PRODUCTION_SALT) into project.godot as game/security/save_salt during itch.io deployments.
#530 Refactor globals.gd _save_settings() to persist game settings using Godot's encrypted ConfigFile methods instead of plaintext saves.
#530 Refactor globals.gd _load_settings() to first attempt encrypted load with a shared encryption key, then fall back to plaintext ConfigFile.load() for legacy files and, on success, immediately re-save to upgrade them to encrypted format while handling missing/corrupt files gracefully.
#530 Ensure the pre-save loading logic inside _save_settings() uses the same encrypted+fallback mechanism to preserve existing sections, provide backward compatibility for existing plaintext configs, and avoid data loss during migration.
#531 Refactor settings.gd input mapping persistence (save_input_mappings and load_input_mappings) to use encrypted config methods with the same key and settings file as globals.gd, including pre-loading existing data before saving to preserve unrelated sections.
#531 Add backward-compatible fallback logic in settings.gd so input mappings can be loaded from legacy plaintext configs, set _needs_save when plaintext is used, and ensure a subsequent save rewrites the data in encrypted format.
#531 Ensure safe coexistence and multi-writer safety between settings.gd and globals.gd on the shared user://settings.cfg file by centralizing encryption key management and config-loading logic, preventing corruption or loss of unrelated settings sections.
#532 Add GUT unit tests that verify new installs create encrypted save/config files by default and that encryption is explicitly validated, not assumed.
#532 Add GUT unit tests that validate migration from legacy plaintext config files to the new encrypted format, ensuring backward compatibility, successful fallback loading of plaintext, automatic upgrade to encrypted files, and no data loss during migration.
#532 Add GUT unit tests that validate safe coexistence of multiple systems (settings/globals, input mappings, audio manager) writing to the same config file, ensuring unrelated sections are preserved and that tests operate on isolated test files with proper cleanup.

Possibly linked issues

  • #FEATURE Implement Encryption Key Management for Config Files: PR fulfills the issue by adding Globals.save_encryption_pass, _get_encryption_key, encrypted config usage, and CI salt injection.
  • #FEATURE Encrypt Game Settings Save/Load logic (globals.gd): Globals._load_settings/_save_settings now use encrypted passes with legacy plaintext fallback and auto-migration, fulfilling the issue.
  • #FEATURE Encrypt Input Mappings Save/Load logic (settings.gd): PR updates settings.gd input mapping load/save to encrypted, shared-key config with plaintext fallback and migration as requested.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ikostan ikostan linked an issue Apr 28, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds encrypted ConfigFile persistence with derived encryption key tied to ProjectSettings salt and device ID, updates runtime to prefer encrypted load/save with plaintext-fallback migration, injects PRODUCTION_SALT from CI into project.godot, and updates many tests to use encrypted save/load APIs. (29 words)

Changes

Cohort / File(s) Summary
CI workflows
.github/workflows/deploy_to_itch.yml, .github/workflows/lint_test_deploy.yml
Require/forward new PRODUCTION_SALT secret and inject it into project.godot (set game/security/save_salt) during deploy and via reusable workflow job.
Globals (encryption key + settings I/O)
scripts/core/globals.gd
Add save_encryption_pass and _get_encryption_key(); switch _load_settings()/_save_settings() to load_encrypted_pass/save_encrypted_pass with targeted plaintext-fallback and migration behavior; abort on ambiguous corruption to avoid data loss.
Settings & audio managers
scripts/core/settings.gd, scripts/managers/audio_manager.gd
Switch input mappings, last-input-device, and audio volume persistence to encrypted ConfigFile APIs, preserve/handle legacy plaintext files via migration paths.
Tests (wide coverage)
test/gdunit4/..., test/gut/... (many files listed in raw summary)
Update numerous unit and GUT tests to write/read configs with save_encrypted_pass/load_encrypted_pass using Globals.save_encryption_pass; adjust some setups, disable two legacy/error tests, and add small test helpers/awaits.
Docs / README
README.md
Removed Snyk “Known Vulnerabilities” badge.
Test helper asset fix
test/gut/gut_test_helper.gd
Assign a dummy 1×1 RGBA texture to mock player sprite and restore CollisionPolygon2D name to avoid warnings.

Sequence Diagram(s)

sequenceDiagram
  participant CI as GitHub Actions (deploy)
  participant Repo as Repository (project.godot)
  participant Game as Game runtime (Globals)
  participant Device as DeviceID provider
  participant Config as ConfigFile API

  CI->>Repo: inject `PRODUCTION_SALT` into `game/security/save_salt`
  Repo->>Game: runtime starts, reads ProjectSettings (including save_salt)
  Game->>Device: query unique device id
  Game->>Game: _get_encryption_key() := SHA256(salt + device_id)
  Game->>Config: load_encrypted_pass(path, encryption_key)
  alt encrypted load OK
    Config-->>Game: return config
  else ERR_INVALID_DATA / ERR_FILE_CORRUPT
    Game->>Config: attempt legacy plaintext load(path)
    alt legacy load OK
      Config-->>Game: return legacy config
      Game->>Config: save_encrypted_pass(path, encryption_key)  -- migrate to encrypted
    else
      Config-->>Game: return defaults / error (abort save to avoid overwriting)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested labels

github actions, documentation

Poem

🐰
I hopped through salts and secrets bright,
I stitched device IDs into night,
Plaintext tucked where it used to roam,
Encrypted settings find a home,
Hop — deploy secure — the rabbit's code takes flight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete; it contains only the template structure with checked boxes but no actual content filled in for the key sections. Fill in the Description section with what the PR does, provide the actual Issue number in Related Issue, describe the actual key changes and testing performed, and add relevant notes about implementation and testing environment.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly and concisely describes the main objective of the changeset: implementing encryption key management for config files, which is the core feature being added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch implement-encryption-key-management-for-config-files

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ikostan ikostan linked an issue Apr 28, 2026 that may be closed by this pull request
@deepsource-io
Copy link
Copy Markdown

deepsource-io Bot commented Apr 28, 2026

DeepSource Code Review

We reviewed changes in 685ce08...49170dc on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python May 2, 2026 4:23a.m. Review ↗
JavaScript May 2, 2026 4:23a.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The migration logic that falls back to plaintext and re-saves encrypted is implemented only for _load_settings/_save_settings; consider applying the same dual-load approach to input mappings and audio configs so existing plaintext config files are seamlessly migrated instead of just failing encrypted loads.
  • In places that rely on Globals.save_encryption_pass (e.g., settings.gd and audio_manager.gd), you assume the key is non-empty; for robustness you might mirror the _load_settings safeguard by re-deriving the key if it ends up empty or uninitialized at call time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The migration logic that falls back to plaintext and re-saves encrypted is implemented only for `_load_settings`/`_save_settings`; consider applying the same dual-load approach to input mappings and audio configs so existing plaintext config files are seamlessly migrated instead of just failing encrypted loads.
- In places that rely on `Globals.save_encryption_pass` (e.g., `settings.gd` and `audio_manager.gd`), you assume the key is non-empty; for robustness you might mirror the `_load_settings` safeguard by re-deriving the key if it ends up empty or uninitialized at call time.

## Individual Comments

### Comment 1
<location path="scripts/core/settings.gd" line_range="256-257" />
<code_context>
 	var config: ConfigFile = ConfigFile.new()
-	var err: int = config.load(path)
+
+	# Use encrypted load
+	var err: int = config.load_encrypted_pass(path, Globals.save_encryption_pass)
+
 	if err != OK and err != ERR_FILE_NOT_FOUND:  # Handle errors except missing file
</code_context>
<issue_to_address>
**issue (bug_risk):** Loading input mappings only via encrypted config risks breaking backward compatibility with existing plaintext configs.

In `globals.gd`, `_load_settings` already handles migration by trying encrypted first, then falling back to plaintext on `ERR_INVALID_DATA`/`ERR_FILE_CORRUPT`, and re-saving encrypted.

`load_input_mappings` now only calls `load_encrypted_pass` and treats any non-OK (except `ERR_FILE_NOT_FOUND`) as fatal. Existing plaintext input configs will likely return `ERR_INVALID_DATA`, causing all mappings to fail to load and skipping migration.

To avoid this regression, mirror the `_load_settings` flow here:
- Call `load_encrypted_pass` first.
- On `ERR_INVALID_DATA`/`ERR_FILE_CORRUPT`, reset the `ConfigFile` and call `load(path)`.
- If that succeeds, flag the config for migration and re-save it encrypted in `save_input_mappings`.
</issue_to_address>

### Comment 2
<location path="scripts/managers/audio_manager.gd" line_range="244-247" />
<code_context>
-	var err: int = config.load(path)
+
+	# Use encrypted load
+	var err: int = config.load_encrypted_pass(path, Globals.save_encryption_pass)
+
 	if err != OK and err != ERR_FILE_NOT_FOUND:  # Handle errors except missing file
</code_context>
<issue_to_address>
**issue:** Audio volume configs are now encrypted-only, which may orphan existing plaintext configs.

`load_volumes`/`save_volumes` now assume the file is encrypted. On systems with an existing plaintext audio config, `load_encrypted_pass` will likely return `ERR_INVALID_DATA`/`ERR_FILE_CORRUPT`, so volumes are skipped with no migration and users effectively lose their saved settings.

To prevent that, consider:
- Using the same approach as `settings.gd`: try encrypted first, then fall back to plaintext on specific error codes.
- When plaintext load succeeds, immediately re-save using `save_encrypted_pass` to migrate the file.

This preserves existing volume settings while adopting encryption.
</issue_to_address>

### Comment 3
<location path=".github/workflows/deploy_to_itch.yml" line_range="16-15" />
<code_context>
       ITCHIO_API_KEY:
         description: "Itch.io API key"
         required: true
+      PRODUCTION_SALT:
+        description: "production salt secret key"
+        required: true

 jobs:
</code_context>
<issue_to_address>
**suggestion:** PRODUCTION_SALT is defined as a workflow input but the step reads it from secrets instead.

The `workflow_dispatch` input `PRODUCTION_SALT` is declared but not used; the step "Inject Production Salt into project.godot" references `${{ secrets.PRODUCTION_SALT }}` instead of `${{ inputs.PRODUCTION_SALT }}`. Either remove the unused input if you intend to rely solely on the secret, or update the step to use `${{ inputs.PRODUCTION_SALT }}` and document which source is authoritative.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/core/settings.gd Outdated
Comment thread scripts/managers/audio_manager.gd Outdated
Comment thread .github/workflows/deploy_to_itch.yml
…sks breaking backward compatibility with existing plaintext configs.

issue (bug_risk): Loading input mappings only via encrypted config risks breaking backward compatibility with existing plaintext configs.

In globals.gd, _load_settings already handles migration by trying encrypted first, then falling back to plaintext on ERR_INVALID_DATA/ERR_FILE_CORRUPT, and re-saving encrypted.

load_input_mappings now only calls load_encrypted_pass and treats any non-OK (except ERR_FILE_NOT_FOUND) as fatal. Existing plaintext input configs will likely return ERR_INVALID_DATA, causing all mappings to fail to load and skipping migration.

To avoid this regression, mirror the _load_settings flow here:

Call load_encrypted_pass first.
On ERR_INVALID_DATA/ERR_FILE_CORRUPT, reset the ConfigFile and call load(path).
If that succeeds, flag the config for migration and re-save it encrypted in save_input_mappings.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
scripts/core/globals.gd (1)

428-430: Avoid silent weak-key fallback in non-debug builds

At Line 428, falling back to "dev_fallback_salt" silently weakens the encryption guarantee if secret injection fails. Recommend failing fast (or at least logging ERROR and refusing encrypted persistence) in production builds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/core/globals.gd` around lines 428 - 430, The current salt retrieval
in the function that returns the hashed key uses
ProjectSettings.get_setting("game/security/save_salt", "dev_fallback_salt") and
silently falls back to the weak "dev_fallback_salt"; change this so production
builds do not accept the fallback: detect when the setting is missing or equals
the fallback and in non-debug (release) mode either log an ERROR via
ProjectSettings/OS logger and abort/refuse encrypted persistence or raise/return
a failure instead of using the weak salt; keep the current behavior only in
debug/dev builds (using Engine.is_editor_hint() or OS.has_feature("debug") as
appropriate) and reference the salt variable, ProjectSettings.get_setting call,
and the function that returns (OS.get_unique_id() + salt).sha256_text() to
locate where to implement the check and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/deploy_to_itch.yml:
- Around line 44-56: The workflow injects the salt under [application] but Godot
expects the key under the [game] section
(ProjectSettings.get_setting("game/security/save_salt", ...)); change the script
so it checks/updates the key prefix '^game/security/save_salt=' and, if missing,
appends the line under the '[game]' section (or creates a new '[game]' section
in the fallback) instead of using '[application]'. Ensure the inserted key name
is security/save_salt (i.e. game/security/save_salt) so the runtime lookup in
scripts/core/globals.gd picks up the injected salt.

In `@scripts/core/globals.gd`:
- Around line 248-252: The code currently falls back from
config.load_encrypted_pass to config.load but proceeds to save regardless,
risking overwriting unrelated sections; modify the logic around
config.load_encrypted_pass(...) and config.load(...) (variables err and
save_encryption_pass) so that after the plaintext fallback you check if err ==
OK before proceeding to any save/write operations (skip or abort save if err
remains an error), and add a clear log/error return path when both loads fail to
avoid destructive overwrites.
- Around line 175-199: The file fails the formatter check; run the Godot
formatter and commit the changes: format scripts/core/globals.gd with gdformat
(run gdformat --write ./scripts/core/globals.gd) so the block around
save_encryption_pass, _get_encryption_key, config.load_encrypted_pass,
ConfigFile.new, config.load and log_message is normalized; after formatting
re-run CI and commit the formatted file.

In `@scripts/core/settings.gd`:
- Around line 593-597: The code calls config.load_encrypted_pass(CONFIG_PATH,
Globals.save_encryption_pass) but ignores failures and then immediately calls
config.save_encrypted_pass(...), which can truncate settings; update
save_last_input_device to detect load failure (check the return value or wrap
load_encrypted_pass in a try/if) and only proceed to
config.save_encrypted_pass(CONFIG_PATH, Globals.save_encryption_pass) after a
successful load (or merge with existing config), otherwise avoid saving or
recreate a full config before calling config.save_encrypted_pass; refer to
load_encrypted_pass, save_encrypted_pass,
config.set_value("input","last_input_device", device), CONFIG_PATH and
Globals.save_encryption_pass to locate the change.

---

Nitpick comments:
In `@scripts/core/globals.gd`:
- Around line 428-430: The current salt retrieval in the function that returns
the hashed key uses ProjectSettings.get_setting("game/security/save_salt",
"dev_fallback_salt") and silently falls back to the weak "dev_fallback_salt";
change this so production builds do not accept the fallback: detect when the
setting is missing or equals the fallback and in non-debug (release) mode either
log an ERROR via ProjectSettings/OS logger and abort/refuse encrypted
persistence or raise/return a failure instead of using the weak salt; keep the
current behavior only in debug/dev builds (using Engine.is_editor_hint() or
OS.has_feature("debug") as appropriate) and reference the salt variable,
ProjectSettings.get_setting call, and the function that returns
(OS.get_unique_id() + salt).sha256_text() to locate where to implement the check
and error handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: adbcc4d4-fc1f-4a7a-891f-c614ec13a521

📥 Commits

Reviewing files that changed from the base of the PR and between 685ce08 and 019b4ad.

📒 Files selected for processing (7)
  • .github/workflows/deploy_to_itch.yml
  • .github/workflows/lint_test_deploy.yml
  • README.md
  • requirements.txt
  • scripts/core/globals.gd
  • scripts/core/settings.gd
  • scripts/managers/audio_manager.gd
💤 Files with no reviewable changes (1)
  • README.md
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-04-10T00:07:55.427Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 534
File: scripts/player.gd:146-149
Timestamp: 2026-04-10T00:07:55.427Z
Learning: In `scripts/globals.gd` and `scripts/player.gd` (GDScript, Godot 4), `current_fuel` is intentionally treated as volatile session data and should NOT be saved to or loaded from `settings.cfg`. Only `max_fuel` (tank capacity) is a persistent setting. `current_fuel` is always reset to `max_fuel` unconditionally in `player._ready()`. Persisting `current_fuel` is considered an architectural mistake by the project maintainer (ikostan). Mid-run fuel state persistence is planned via a separate `SaveGameResource` in PR `#535`.

Applied to files:

  • scripts/core/globals.gd
🪛 GitHub Actions: Pull Request Pipeline
scripts/core/globals.gd

[error] 172-172: gdformat --diff --check reported formatting changes would be applied to ./scripts/core/globals.gd (exit code 1). Run 'gdformat --write ./scripts/core/globals.gd' (or 'gdformat --write ./scripts') to fix formatting.

🔇 Additional comments (4)
scripts/managers/audio_manager.gd (1)

244-245: Encrypted volume persistence update looks consistent

The switch to load_encrypted_pass / save_encrypted_pass is applied consistently in both load and save paths, with existing error branching preserved.

Also applies to: 302-303, 311-312

scripts/core/settings.gd (1)

253-258: No action required. The autoload order in project.godot already ensures Globals initializes before Settings (line 27 before line 28), preventing the initialization regression described. Both encrypted load calls at lines 253-258 and 606-609 safely depend on Globals.save_encryption_pass being available.

.github/workflows/deploy_to_itch.yml (1)

16-18: This required secret wiring looks correct.

The reusable workflow now declares PRODUCTION_SALT as required, and this matches the caller change in lint_test_deploy.yml.

.github/workflows/lint_test_deploy.yml (1)

92-92: Forwarding the new secret is correct.

This completes the contract with the reusable deploy workflow.

Comment thread .github/workflows/deploy_to_itch.yml Outdated
Comment thread scripts/core/globals.gd Outdated
Comment thread scripts/core/globals.gd Outdated
Comment thread scripts/core/settings.gd Outdated
ikostan added 2 commits April 27, 2026 19:04
…existing plaintext configs.

issue: Audio volume configs are now encrypted-only, which may orphan existing plaintext configs.

load_volumes/save_volumes now assume the file is encrypted. On systems with an existing plaintext audio config, load_encrypted_pass will likely return ERR_INVALID_DATA/ERR_FILE_CORRUPT, so volumes are skipped with no migration and users effectively lose their saved settings.

To prevent that, consider:

Using the same approach as settings.gd: try encrypted first, then fall back to plaintext on specific error codes.
When plaintext load succeeds, immediately re-save using save_encrypted_pass to migrate the file.
This preserves existing volume settings while adopting encryption.
Write the salt under the [game] section, not [application].

Godot project settings use hierarchical paths stored as INI section/key pairs. The path game/security/save_salt maps to the [game] section with key security/save_salt. Writing it under [application] creates a different (non-existent) setting, so the runtime lookup in scripts/core/globals.gd:431 (ProjectSettings.get_setting("game/security/save_salt", "dev_fallback_salt")) falls back to the dev default instead of using the injected salt.

That is an excellent catch. Because Godot parses the INI file by mapping the section headers to the first part of the setting path, injecting game/security/save_salt under the [application] header created a mismatched property that the engine couldn't resolve at runtime.

To fix this, we need to update the bash script in that step to explicitly target the [game] section and use the correct security/save_salt key.
@ikostan ikostan linked an issue Apr 28, 2026 that may be closed by this pull request
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented Apr 28, 2026

428-430: Avoid silent weak-key fallback in non-debug builds

At Line 428, falling back to "dev_fallback_salt" silently weakens the encryption guarantee if secret injection fails. Recommend failing fast (or at least logging ERROR and refusing encrypted persistence) in production builds.

This is an excellent security catch. The linter/scanner is absolutely right: silently falling back to a hardcoded string in a production environment completely defeats the purpose of encrypting the save files. If the GitHub Actions injection step fails for any reason, the game would ship with a compromised key.

To fix this, we need to update _get_encryption_key() in globals.gd to check Godot's feature tags. We will use OS.has_feature("debug") and OS.has_feature("editor") to allow the fallback during local development, but forcefully reject it in release builds.

ikostan added 3 commits April 27, 2026 19:29
428-430: Avoid silent weak-key fallback in non-debug builds

At Line 428, falling back to "dev_fallback_salt" silently weakens the encryption guarantee if secret injection fails. Recommend failing fast (or at least logging ERROR and refusing encrypted persistence) in production build

Verify each finding against the current code and only fix it if needed.

In `@scripts/core/globals.gd` around lines 428 - 430, The current salt retrieval
in the function that returns the hashed key uses
ProjectSettings.get_setting("game/security/save_salt", "dev_fallback_salt") and
silently falls back to the weak "dev_fallback_salt"; change this so production
builds do not accept the fallback: detect when the setting is missing or equals
the fallback and in non-debug (release) mode either log an ERROR via
ProjectSettings/OS logger and abort/refuse encrypted persistence or raise/return
a failure instead of using the weak salt; keep the current behavior only in
debug/dev builds (using Engine.is_editor_hint() or OS.has_feature("debug") as
appropriate) and reference the salt variable, ProjectSettings.get_setting call,
and the function that returns (OS.get_unique_id() + salt).sha256_text() to
locate where to implement the check and error handling.

This is an excellent security catch. The linter/scanner is absolutely right: silently falling back to a hardcoded string in a production environment completely defeats the purpose of encrypting the save files. If the GitHub Actions injection step fails for any reason, the game would ship with a compromised key.

To fix this, we need to update _get_encryption_key() in globals.gd to check Godot's feature tags. We will use OS.has_feature("debug") and OS.has_feature("editor") to allow the fallback during local development, but forcefully reject it in release builds.
ikostan and others added 6 commits April 30, 2026 16:41
The Fix: Web-Safe Encryption Key
We need to update _get_encryption_key in globals.gd to check if we are running in a browser. If we are, we should use a consistent fallback string since "encryption" on the web is mostly a deterrent against casual users editing .cfg files in their browser storage anyway.
Before the refactor, your game likely didn't have as many complex startup dependencies. With the introduction of the centralized encryption pipeline, several high-overhead events are now happening simultaneously during the _ready() sequence:  Wasm Initialization: The browser is still heavy-lifting the Godot engine compilation.  Encryption Key Generation: The system is now hashing device IDs and salts during that same critical window.  File Migration: On the first load, the game is checking for legacy plaintext files and converting them to encrypted formats.  In a headless CI/CD environment (like GitHub Actions), the CPU is shared and throttled, making these new cryptographic and disk-checking steps take just long enough to push you past the 5-second mark.

To fix this across your local machine and GitHub CI/CD, we need to adjust the timeouts in three specific places to give the engine more room to breathe.
The issue isn't that your game is "too slow" for the 15-second or 30-second window—it's that the signal to Playwright is never being sent because your Globals singleton is crashing during initialization.

The TimeoutError is a secondary symptom; the root cause is the OS.get_unique_id() error we identified in the last step.

Why this fixes your local failures:Singleton Safety: By removing the OS.get_unique_id() call on the web, your Globals singleton can now finish initializing and enter its _ready() state.  The Missing Link: The JavaScriptBridge call provides the exact variable (window.godotInitialized) that your Playwright tests are waiting for[cite: 11].
Playwright (and all modern browser automation tools) strictly follows a W3C standard where it injects a specific flag into the browser environment: navigator.webdriver = true. We can use Godot's JavaScriptBridge to read this flag. If the game detects it is being controlled by a robot, it stands down the security guard automatically!
This commit fixes the style issues introduced in bf0b27e according to the output
from Black and isort.

Details: #588
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 1, 2026

  • The encrypted/legacy config load/save logic (including _is_file_encrypted, dual-path load_encrypted_pass/load, and migration flags) is now duplicated across Settings, Globals and AudioManager; consider extracting a shared helper to reduce drift and keep migration behavior consistent.

Great call. I completely agree that the duplicated load/fallback logic was getting messy. In the latest commit, I've extracted all of this into a centralized Globals.safe_load_config(path) helper. It handles the dual-path loading and encryption header detection under the hood, and returns a structured dictionary (including an is_legacy boolean) so Settings, Globals, and AudioManager can handle migrations cleanly and consistently.

@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 1, 2026

  • When _get_encryption_key() returns an empty string in production, callers currently proceed to load_encrypted_pass/save_encrypted_pass with that value; you may want to bail out early with a single clear log to avoid repeated failures/log spam and make the 'persistence disabled' state more explicit.

Great thought process regarding the empty key state! I actually took a slightly more aggressive approach here.

If you look at _get_encryption_key(), if it detects a missing salt in a production build, it doesn't just return an empty string—it actually calls OS.crash(). The return "" at the end of that block is just unreachable code to satisfy the GDScript compiler's type checker. Because the engine hard-aborts instantly, we completely bypass the risk of downstream log spam or silent 'persistence disabled' states.

@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 1, 2026

  • Globals-wide save_encryption_pass and OS-coupled keys are now used directly in tests; consider providing a way to inject or override a deterministic test key (e.g., via ProjectSettings or a test helper) to decouple tests from hardware IDs and make failures easier to reproduce.

This is the final boss of this PR! And honestly, it is an excellent piece of engineering advice from the reviewer.

When tests rely on OS.get_unique_id(), you create a classic "it works on my machine" scenario. If a test fails and generates a corrupted .cfg artifact, you wouldn't be able to easily inspect or decrypt it on a different computer because your hardware IDs wouldn't match.

Because we already centralized everything into the save_encryption_pass variable, giving tests a way to bypass the hardware ID is incredibly easy.

> * Globals-wide `save_encryption_pass` and OS-coupled keys are now used directly in tests; consider providing a way to inject or override a deterministic test key (e.g., via ProjectSettings or a test helper) to decouple tests from hardware IDs and make failures easier to reproduce.

This is the final boss of this PR! And honestly, it is an excellent piece of engineering advice from the reviewer.

When tests rely on OS.get_unique_id(), you create a classic "it works on my machine" scenario. If a test fails and generates a corrupted .cfg artifact, you wouldn't be able to easily inspect or decrypt it on a different computer because your hardware IDs wouldn't match.

Because we already centralized everything into the save_encryption_pass variable, giving tests a way to bypass the hardware ID is incredibly easy.

I've added a new set_test_encryption_key(override_key) helper directly to the Globals singleton. Test suites can now call this in their setup phases to forcibly overwrite the cached save_encryption_pass with a deterministic string. This completely bypasses the OS.get_unique_id() hardware coupling, ensuring that any encrypted artifacts generated during test failures are predictable and reproducible across different machines and CI environments.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 1, 2026

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 6 issues, and left some high level feedback:

  • There are now two nearly identical helpers to detect encrypted files (Settings._is_file_encrypted and Globals.is_file_encrypted); consider keeping a single shared implementation (e.g., only in globals.gd) to avoid divergence and make future changes easier.
  • Several call sites/tests still reach for Globals.save_encryption_pass directly while others use Globals.ensure_encryption_key(); it would be more robust to standardize on the helper everywhere so the lazy initialization and production guards can't be accidentally bypassed.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are now two nearly identical helpers to detect encrypted files (`Settings._is_file_encrypted` and `Globals.is_file_encrypted`); consider keeping a single shared implementation (e.g., only in `globals.gd`) to avoid divergence and make future changes easier.
- Several call sites/tests still reach for `Globals.save_encryption_pass` directly while others use `Globals.ensure_encryption_key()`; it would be more robust to standardize on the helper everywhere so the lazy initialization and production guards can't be accidentally bypassed.

## Individual Comments

### Comment 1
<location path="scripts/core/settings.gd" line_range="625-634" />
<code_context>
+	return (device_id + salt).sha256_text()
+
+
+## Helper to determine if a config file is encrypted.
+func is_file_encrypted(path: String) -> bool:
+	if not FileAccess.file_exists(path):
+		return false
+	var f: FileAccess = FileAccess.open(path, FileAccess.READ)
+	if not f:
+		return false
+	if f.get_length() < 4:
+		f.close()
+		return false
+	var magic: int = f.get_32()
+	f.close()
+	# Godot Encrypted File Magic Number: 0x43454447 ("GDEC")
+	return magic == 0x43454447
+
</code_context>
<issue_to_address>
**suggestion:** Avoid duplicating the file-encryption helper that already exists in `globals.gd`.

There are now two helpers for detecting encrypted config files: this `is_file_encrypted` and `Globals.is_file_encrypted` in `globals.gd`. This duplication risks the logic diverging (e.g., if magic headers or formats change). Prefer removing this local helper and reusing the centralized one, or expose a single shared helper that all call sites use.

Suggested implementation:

```

```

1. Replace all call sites of `is_file_encrypted(path)` in `scripts/core/settings.gd` with `Globals.is_file_encrypted(path)`.
2. If `Globals` is not yet imported or accessible in this file, ensure it is properly referenced (e.g., via singleton/autoload or `Globals` class reference consistent with the rest of the codebase).
3. If any other files introduced a similar local `is_file_encrypted` helper, apply the same refactor there to ensure all encryption checks go through `Globals.is_file_encrypted`.
</issue_to_address>

### Comment 2
<location path="scripts/core/settings.gd" line_range="620-629" />
<code_context>


-## Saves the last selected input device to config.
-func save_last_input_device(device: String) -> void:
-	if device not in ["keyboard", "gamepad"]:
-		return
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against failed config loads before overwriting when saving the last input device.

In `save_last_input_device`, `Globals.safe_load_config`’s `err` result is ignored. If the load fails for reasons other than `ERR_FILE_NOT_FOUND` (permissions, corruption, etc.), this function will still overwrite the config with a new encrypted file, unlike `_save_settings` and `save_input_mappings`, which bail out on such errors. Consider checking `err` and early-returning (or at least logging and handling explicitly) for non-`OK`/non-`ERR_FILE_NOT_FOUND` cases to avoid potential data loss.
</issue_to_address>

### Comment 3
<location path=".github/workflows/deploy_to_itch.yml" line_range="16-15" />
<code_context>
       ITCHIO_API_KEY:
         description: "Itch.io API key"
         required: true
+      PRODUCTION_SALT:
+        description: "production salt secret key"
+        required: true

 jobs:
</code_context>
<issue_to_address>
**issue:** The new `PRODUCTION_SALT` input is marked required but the job actually reads from `secrets.PRODUCTION_SALT`.

Right now the required `workflow_dispatch.inputs.PRODUCTION_SALT` is never used, since the step reads `SALT="${{ secrets.PRODUCTION_SALT }}"`. This makes manual runs require an input that’s ignored. I’d suggest either removing the input and relying only on the secret, or updating the step to use `github.event.inputs.PRODUCTION_SALT` if you want a user-supplied value at dispatch time.
</issue_to_address>

### Comment 4
<location path=".github/workflows/deploy_to_itch.yml" line_range="46-55" />
<code_context>
+          SALT="${{ secrets.PRODUCTION_SALT }}"
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Escape or constrain the salt value to avoid breaking the `project.godot` syntax.

Because the workflow writes `security/save_salt="${SALT}"` directly into `project.godot`, any `"` or `\` in `PRODUCTION_SALT` would corrupt the config syntax. Since this value is security-sensitive and controlled, either restrict it to a safe encoding (e.g., base64/hex) or escape `"` and `\` before writing to ensure the file remains parseable.

Suggested implementation:

```
      - name: "Inject Production Salt into project.godot"
        run: |
          # Raw production salt (may contain characters that need escaping in project.godot)
          SALT="${{ secrets.PRODUCTION_SALT }}"

          # Escape backslashes and double quotes so the value is safe inside a quoted Godot config string.
          # This ensures `security/save_salt="..."` remains syntactically valid even if the secret contains
          # `\` or `"`. Newlines are not supported and will break the config, so the secret should avoid them.
          ESCAPED_SALT="$(printf '%s' "$SALT" | sed 's/\\/\\\\/g; s/"/\\"/g')"

          # Use a section-aware awk script to update security/save_salt only within the [game] section.
          # Behavior:
          # - If [game] exists and security/save_salt is present within it, replace that line.
          # - If [game] exists but security/save_salt is missing, append it at the end of the [game] section.
          # - If [game] does not exist, append a new [game] section with security/save_salt at the end of the file.
          awk '
            BEGIN {
              salt = ENVIRON["ESCAPED_SALT"]
              in_game = 0

```

If the awk script later references the `SALT` environment variable directly (outside of the shown `BEGIN` block), those references should be updated to use `ESCAPED_SALT` as well to ensure consistent escaping throughout the script.
</issue_to_address>

### Comment 5
<location path="tests/difficulty_flow_test.py" line_range="75" />
<code_context>

         page.goto(
-            "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000
+            "http://localhost:8080/index.html", wait_until="networkidle", timeout=15000
         )
         # 1. Wait for the engine to actually start the splash scene
</code_context>
<issue_to_address>
**suggestion:** Consider centralizing these hard-coded timeouts so browser tests can be tuned consistently via configuration or environment variables.

Other tests (e.g. `no_error_logs_test.py`) already use a `DEFAULT_TIMEOUT` derived from `TEST_TIMEOUT`. Reusing that pattern here (and in other Playwright flows) instead of inlining `15000` in multiple files would keep timeouts consistent and easier to tune for slower CI environments.

Suggested implementation:

```python
        page.goto(
            "http://localhost:8080/index.html",
            wait_until="networkidle",
            timeout=DEFAULT_TIMEOUT,
        )
        # 1. Wait for the engine to actually start the splash scene
        page.wait_for_timeout(5000)
        # Wait for Godot engine init (ensures 'godot' object is defined)
        page.wait_for_function("() => window.godotInitialized", timeout=DEFAULT_TIMEOUT)

        # Verify canvas and title to ensure game is initialized
        canvas = page.locator("canvas")
        page.wait_for_selector("canvas", state="visible", timeout=DEFAULT_TIMEOUT)
        box: Optional[Dict[str, float]] = canvas.bounding_box()

```

To fully implement the suggestion, you should:
1. Ensure `DEFAULT_TIMEOUT` is defined in a shared place (e.g. derived from `TEST_TIMEOUT` as in `no_error_logs_test.py`), for example:
   ```python
   TEST_TIMEOUT = int(os.getenv("TEST_TIMEOUT", "15000"))
   DEFAULT_TIMEOUT = TEST_TIMEOUT
   ```
2. Import or define `DEFAULT_TIMEOUT` in `tests/difficulty_flow_test.py` consistently with how it is done in `no_error_logs_test.py` (e.g. `from .no_error_logs_test import DEFAULT_TIMEOUT` or from a shared test utilities module).
3. Optionally, you can also centralize the `5000` timeout (e.g. as `SHORT_TIMEOUT`) if you want *all* Playwright waits configurable.
</issue_to_address>

### Comment 6
<location path="tests/difficulty_flow_test.py" line_range="148-150" />
<code_context>
         assert any(
             "log level changed to: debug" in log["text"].lower() for log in new_logs
         ), "Failed to set log level to DEBUG"
+        # FIX: Look for the new encrypted save log instead of "settings saved"
         assert any(
-            "settings saved" in log["text"].lower() for log in new_logs
+            "encrypted settings persisted successfully" in log["text"].lower()
+            for log in new_logs
         ), "Failed to save the settings"
</code_context>
<issue_to_address>
**suggestion (testing):** This assertion is tightly coupled to a specific log string; consider making it more robust against log message wording changes.

Because this test is really verifying that settings persistence (via the encrypted path) occurs, asserting on the full log line makes it brittle to harmless wording tweaks. Instead of matching the entire message, consider asserting on a more stable pattern (e.g., checking that a line contains both "encrypted" and "settings") or using a helper that encapsulates the expected pattern. That still confirms the encrypted save path is used without tying the test to the exact log text.

Suggested implementation:

```python
        # Verify that settings were persisted via the encrypted path without relying on exact log wording
        assert any(
            "encrypted" in log["text"].lower() and "settings" in log["text"].lower()
            for log in new_logs
        ), "Failed to save the settings"

```

```python
        # Verify that settings were persisted via the encrypted path without relying on exact log wording
        assert any(
            "encrypted" in log["text"].lower() and "settings" in log["text"].lower()

```

If there are other tests asserting on this same log line, consider updating them similarly to check for a stable pattern (e.g., `"encrypted"` and `"settings"` together) or extracting a small helper like `assert_encrypted_settings_persisted(new_logs)` to keep the expectation consistent across the suite.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/core/settings.gd Outdated
Comment thread scripts/core/settings.gd Outdated
Comment thread .github/workflows/deploy_to_itch.yml
Comment thread .github/workflows/deploy_to_itch.yml Outdated
Comment thread tests/difficulty_flow_test.py Outdated
Comment thread tests/difficulty_flow_test.py Outdated
Great catch! Because I recently centralized the loading logic into Globals.safe_load_config(), this local _is_file_encrypted helper actually became orphaned dead code. I've completely removed it from settings.gd. All encryption detection now routes exclusively through the centralized helper in Globals.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 1, 2026

  • There are now two nearly identical helpers to detect encrypted files (Settings._is_file_encrypted and Globals.is_file_encrypted); consider keeping a single shared implementation (e.g., only in globals.gd) to avoid divergence and make future changes easier.

Great catch! Because I recently centralized the loading logic into Globals.safe_load_config(), this local _is_file_encrypted helper actually became orphaned dead code. I've completely removed it from settings.gd. All encryption detection now routes exclusively through the centralized helper in Globals.

ikostan and others added 12 commits April 30, 2026 19:39
…g when saving the last input device.

issue (bug_risk): Guard against failed config loads before overwriting when saving the last input device.

In save_last_input_device, Globals.safe_load_config’s err result is ignored. If the load fails for reasons other than ERR_FILE_NOT_FOUND (permissions, corruption, etc.), this function will still overwrite the config with a new encrypted file, unlike _save_settings and save_input_mappings, which bail out on such errors. Consider checking err and early-returning (or at least logging and handling explicitly) for non-OK/non-ERR_FILE_NOT_FOUND cases to avoid potential data loss.

This is an excellent catch by the reviewer. Data loss bugs are the absolute worst kind of bugs to slip into production!

Here is exactly what the reviewer noticed: When safe_load_config() runs, it starts with a completely blank in-memory ConfigFile object. If the physical file on the hard drive is corrupted or locked, the load fails, but that in-memory object remains blank. If you then set last_input_device on that blank object and save it to the hard drive, you just successfully deleted all of the player's custom keybinds!
…r tests can be tuned consistently via configuration or environment variables.

suggestion: Consider centralizing these hard-coded timeouts so browser tests can be tuned consistently via configuration or environment variables.

Other tests (e.g. no_error_logs_test.py) already use a DEFAULT_TIMEOUT derived from TEST_TIMEOUT. Reusing that pattern here (and in other Playwright flows) instead of inlining 15000 in multiple files would keep timeouts consistent and easier to tune for slower CI environments.
This commit fixes the style issues introduced in 8687b8f according to the output
from Black and isort.

Details: #588
…r tests can be tuned consistently via configuration or environment variables.

suggestion: Consider centralizing these hard-coded timeouts so browser tests can be tuned consistently via configuration or environment variables.

Other tests (e.g. no_error_logs_test.py) already use a DEFAULT_TIMEOUT derived from TEST_TIMEOUT. Reusing that pattern here (and in other Playwright flows) instead of inlining 15000 in multiple files would keep timeouts consistent and easier to tune for slower CI environments.
This commit fixes the style issues introduced in 2ba79fd according to the output
from Black and isort.

Details: #588
… log string; consider making it more robust against log message wording changes.

suggestion (testing): This assertion is tightly coupled to a specific log string; consider making it more robust against log message wording changes.

Because this test is really verifying that settings persistence (via the encrypted path) occurs, asserting on the full log line makes it brittle to harmless wording tweaks. Instead of matching the entire message, consider asserting on a more stable pattern (e.g., checking that a line contains both "encrypted" and "settings") or using a helper that encapsulates the expected pattern. That still confirms the encrypted save path is used without tying the test to the exact log text.
…eaking the project.godot syntax.

suggestion (bug_risk): Escape or constrain the salt value to avoid breaking the project.godot syntax.

Because the workflow writes security/save_salt="${SALT}" directly into project.godot, any " or \ in PRODUCTION_SALT would corrupt the config syntax. Since this value is security-sensitive and controlled, either restrict it to a safe encoding (e.g., base64/hex) or escape " and \ before writing to ensure the file remains parseable.
@ikostan
Copy link
Copy Markdown
Owner Author

ikostan commented May 2, 2026

@sourcery-ai review

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 2, 2026

Sorry @ikostan, your pull request is larger than the review limit of 150000 diff characters

1 similar comment
@SourceryAI
Copy link
Copy Markdown

Sorry @ikostan, your pull request is larger than the review limit of 150000 diff characters

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants