Skip to content

Encrypt Discord token at rest in settings file (machine-bound)#1491

Merged
Tyrrrz merged 18 commits intomasterfrom
copilot/add-token-encryption-settings
Feb 27, 2026
Merged

Encrypt Discord token at rest in settings file (machine-bound)#1491
Tyrrrz merged 18 commits intomasterfrom
copilot/add-token-encryption-settings

Conversation

Copy link
Contributor

Copilot AI commented Feb 27, 2026

  • In Read(): cipher naming (encrypted data being read)
  • In Write(): cipherSource variable for plaintext span before in-place encryption; layout comment with size annotation
  • Fix CSharpier formatting in 5 localization files (PersistTokenTooltip = """ inline)
  • Fix CSharpier formatting in TokenEncryptionConverter.cs (exception filter style + trailing newline)
  • Build: 0 warnings, 0 errors, format check passing

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copilot AI changed the title [WIP] Add token encryption for saving and loading settings Encrypt Discord token at rest in settings file Feb 27, 2026
@Tyrrrz Tyrrrz marked this pull request as ready for review February 27, 2026 08:11
Copilot AI review requested due to automatic review settings February 27, 2026 08:11
@Tyrrrz
Copy link
Owner

Tyrrrz commented Feb 27, 2026

@copilot additional requirement: the token should be decryptable only on the machine it was encrypted for. If Settings.dat is stolen, the attacker shouldn't be able to decrypt it on a different machine.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to prevent Discord tokens from being stored in plain text inside Settings.dat by introducing transparent encryption at the JSON serialization layer and updating UI tooltips to clarify the security properties.

Changes:

  • Added TokenEncryptionConverter (JsonConverter<string?>) that encrypts/decrypts LastToken values using AES-CBC with a randomized IV and randomized plaintext prefix, storing values as enc:<base64(...)>.
  • Annotated SettingsService.LastToken with the new converter for backward-compatible read and automatic re-save in encrypted form.
  • Updated localization strings for PersistTokenTooltip across all supported languages to mention encrypted-at-rest storage.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

File Description
DiscordChatExporter.Gui/Services/TokenEncryptionConverter.cs Introduces the token encryption/decryption converter and on-disk format (enc: + Base64 blob).
DiscordChatExporter.Gui/Services/SettingsService.cs Applies the converter to LastToken so persisted settings no longer store the token in plaintext.
DiscordChatExporter.Gui/Localization/LocalizationManager.{English,French,German,Spanish,Ukrainian}.cs Updates tooltip text to reflect encrypted-at-rest storage and its limitations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +36
var decrypted = aes.DecryptCbc(data[17..], data[..16]);
return Encoding.UTF8.GetString(decrypted[data[16]..]);
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Read() allocates multiple temporary arrays/strings via range slicing (data[17..], data[..16], and decrypted[...]). Since DecryptCbc and Encoding.UTF8.GetString accept spans, consider using data.AsSpan(...) and decrypted.AsSpan(padLen) (and cache padLen) to avoid these allocations.

Suggested change
var decrypted = aes.DecryptCbc(data[17..], data[..16]);
return Encoding.UTF8.GetString(decrypted[data[16]..]);
var ivSpan = data.AsSpan(0, 16);
var padLen = data[16];
var cipherSpan = data.AsSpan(17);
var decrypted = aes.DecryptCbc(cipherSpan, ivSpan);
return Encoding.UTF8.GetString(decrypted.AsSpan(padLen));

Copilot uses AI. Check for mistakes.
Copy link
Owner

@Tyrrrz Tyrrrz left a comment

Choose a reason for hiding this comment

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


try
{
var data = Convert.FromBase64String(value[Prefix.Length..]);
Copy link
Owner

Choose a reason for hiding this comment

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

We use a hex string for encoding bytes in other places, can we do that here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to Convert.FromHexString / Convert.ToHexStringLower in 1f791ee, consistent with the pattern in ExportAssetDownloader.cs.

aes.Key = Key;

// Layout: IV (16 bytes) | padLen (1 byte) | ciphertext
var decrypted = aes.DecryptCbc(data[17..], data[..16]);
Copy link
Owner

Choose a reason for hiding this comment

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

Why did you choose CBC? Can we use GCM? Or is it for a specific reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to AES-GCM in 1f791ee. CBC was the initial choice for simplicity, but GCM is strictly better here — it gives authenticated encryption (integrity via the 16-byte tag) so a tampered or wrong-key ciphertext is detected cleanly. The new layout is Nonce[12] | padLen[1] | Tag[16] | Ciphertext.

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot additional requirement: the token should be decryptable only on the machine it was encrypted for. If Settings.dat is stolen, the attacker shouldn't be able to decrypt it o...

Done in 1721ac4. The encryption key is now derived from a machine-specific identifier via PBKDF2-SHA256 (10,000 iterations):

  • Windows: HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (stable GUID written at OS install)
  • Linux: /etc/machine-id or /var/lib/dbus/machine-id (set by systemd at first boot)
  • macOS / fallback: Environment.MachineName

If Settings.dat is stolen and moved to a different machine, the derived key will differ so decryption will fail and return null — the user simply re-enters their token.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll unset --global tch-f9790762941c1c04/dbs/csharp/working/.dotnet/dotnet pull.rebase (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title Encrypt Discord token at rest in settings file Encrypt Discord token at rest in settings file (machine-bound) Feb 27, 2026
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Owner

@Tyrrrz Tyrrrz left a comment

Choose a reason for hiding this comment

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

… annotation

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Tyrrrz
Copy link
Owner

Tyrrrz commented Feb 27, 2026

@copilot Note the build errors

@Tyrrrz
Copy link
Owner

Tyrrrz commented Feb 27, 2026

@copilot can you fix the build errors please?

…ormat exception filter

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot Note the build errors

Fixed in 93d4a63 — the CSharpier format check was failing because multiline string assignments in 5 localization files used =\n """ instead of = """, and TokenEncryptionConverter.cs had a trailing blank line and different exception-filter indentation. Running dotnet build -t:CSharpierFormat auto-corrected all 6 files; those fixes are now committed.

@copilot can you fix the build errors please?

Done in 93d4a63 — the format job failure is fixed. All 6 affected files were reformatted by CSharpier and committed.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@Tyrrrz
Copy link
Owner

Tyrrrz commented Feb 27, 2026

@copilot currently getting this:

DiscordChatExporter.Cli/bin/publish/ --configuration Release --runtime osx-x64 --self-contained
  dotnet publish DiscordChatExporter.Cli -p:Version=999.9.9-ci-9149cd4cdf7214115d3ccd3f0ae2a816bf6ef5e4 -p:CSharpier_Bypass=true -p:EncryptionSalt=*** -p:PublishMacOSBundle=true --output DiscordChatExporter.Cli/bin/publish/ --configuration Release --runtime osx-x64 --self-contained
  shell: /bin/bash -e {0}
  env:
    DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
    DOTNET_NOLOGO: true
    DOTNET_CLI_TELEMETRY_OPTOUT: true
    DOTNET_ROOT: /Users/runner/.dotnet
/Users/runner/work/_temp/6a02142b-9f43-4f93-9a4a-9af7c337bd2c.sh: line 1: unexpected EOF while looking for matching `''
Error: Process completed with exit code 2.

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot currently getting this:

...

Fixed in cf8fdea — the ENCRYPTION_SALT secret value contains a ', which broke bash when passed unquoted. Changed to "-p:EncryptionSalt=${{ secrets.ENCRYPTION_SALT }}" so the value is inside double quotes; single quotes have no special meaning there.

@Tyrrrz
Copy link
Owner

Tyrrrz commented Feb 27, 2026

@copilot new error popped up so I changed the secret to not contain '. Please revert your last fix.

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot new error popped up so I changed the secret to not contain '. Please revert your last fix.

Reverted in 18e3ec1 — the argument is back to unquoted -p:EncryptionSalt=${{ secrets.ENCRYPTION_SALT }}.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Tyrrrz and others added 2 commits February 27, 2026 13:56
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@Tyrrrz Tyrrrz merged commit eef0fc7 into master Feb 27, 2026
25 checks passed
@Tyrrrz Tyrrrz deleted the copilot/add-token-encryption-settings branch February 27, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants