Skip to content

Commit 1820d90

Browse files
committed
encrypt passwords stored in config files
1 parent d12edde commit 1820d90

File tree

5 files changed

+241
-3
lines changed

5 files changed

+241
-3
lines changed

SourceServerManager/Models/ServerConfig.cs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.ComponentModel;
22
using System.Runtime.CompilerServices;
33
using System.Text.Json.Serialization;
4+
using SourceServerManager.Services;
45

56
namespace SourceServerManager.Models;
67

@@ -63,10 +64,60 @@ public int RconPort
6364
set => SetField(ref _rconPort, value);
6465
}
6566

67+
[JsonIgnore]
6668
public string RconPassword
69+
{
70+
get
71+
{
72+
// If password is encrypted, decrypt it for UI usage
73+
if (!string.IsNullOrEmpty(_rconPassword) && EncryptionService.IsEncrypted(_rconPassword))
74+
{
75+
try
76+
{
77+
return EncryptionService.DecryptString(_rconPassword);
78+
}
79+
catch
80+
{
81+
// If decryption fails, return empty string
82+
return string.Empty;
83+
}
84+
}
85+
// Return as-is (either plain text for migration, or empty)
86+
return _rconPassword;
87+
}
88+
set
89+
{
90+
var currentDecrypted = RconPassword;
91+
if (!string.Equals(currentDecrypted, value))
92+
{
93+
// Encrypt and store the password
94+
if (!string.IsNullOrEmpty(value))
95+
{
96+
try
97+
{
98+
_rconPassword = EncryptionService.EncryptString(value);
99+
}
100+
catch
101+
{
102+
// If encryption fails, store as plain text (fallback)
103+
_rconPassword = value;
104+
}
105+
}
106+
else
107+
{
108+
_rconPassword = string.Empty;
109+
}
110+
OnPropertyChanged();
111+
}
112+
}
113+
}
114+
115+
// JSON property - stores the encrypted password directly
116+
[JsonPropertyName("rconPassword")]
117+
public string RconPasswordStorage
67118
{
68119
get => _rconPassword;
69-
set => SetField(ref _rconPassword, value);
120+
set => _rconPassword = value ?? string.Empty;
70121
}
71122

72123
// FTP settings
@@ -88,10 +139,60 @@ public string FtpUsername
88139
set => SetField(ref _ftpUsername, value);
89140
}
90141

142+
[JsonIgnore]
91143
public string FtpPassword
144+
{
145+
get
146+
{
147+
// If password is encrypted, decrypt it for UI usage
148+
if (!string.IsNullOrEmpty(_ftpPassword) && EncryptionService.IsEncrypted(_ftpPassword))
149+
{
150+
try
151+
{
152+
return EncryptionService.DecryptString(_ftpPassword);
153+
}
154+
catch
155+
{
156+
// If decryption fails, return empty string
157+
return string.Empty;
158+
}
159+
}
160+
// Return as-is (either plain text for migration, or empty)
161+
return _ftpPassword;
162+
}
163+
set
164+
{
165+
var currentDecrypted = FtpPassword;
166+
if (!string.Equals(currentDecrypted, value))
167+
{
168+
// Encrypt and store the password
169+
if (!string.IsNullOrEmpty(value))
170+
{
171+
try
172+
{
173+
_ftpPassword = EncryptionService.EncryptString(value);
174+
}
175+
catch
176+
{
177+
// If encryption fails, store as plain text (fallback)
178+
_ftpPassword = value;
179+
}
180+
}
181+
else
182+
{
183+
_ftpPassword = string.Empty;
184+
}
185+
OnPropertyChanged();
186+
}
187+
}
188+
}
189+
190+
// JSON property - stores the encrypted password directly
191+
[JsonPropertyName("ftpPassword")]
192+
public string FtpPasswordStorage
92193
{
93194
get => _ftpPassword;
94-
set => SetField(ref _ftpPassword, value);
195+
set => _ftpPassword = value ?? string.Empty;
95196
}
96197

97198
public string FtpRootDirectory
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
5+
namespace SourceServerManager.Services;
6+
7+
public class EncryptionService
8+
{
9+
public static string EncryptString(string plainText)
10+
{
11+
if (string.IsNullOrEmpty(plainText))
12+
return plainText;
13+
14+
try
15+
{
16+
byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);
17+
byte[] encryptedBytes = ProtectedData.Protect(
18+
plainTextBytes,
19+
null, // No additional entropy
20+
DataProtectionScope.CurrentUser // Encrypt for current user only
21+
);
22+
return Convert.ToBase64String(encryptedBytes);
23+
}
24+
catch (Exception ex)
25+
{
26+
throw new Exception($"Failed to encrypt string: {ex.Message}", ex);
27+
}
28+
}
29+
30+
public static string DecryptString(string encryptedText)
31+
{
32+
if (string.IsNullOrEmpty(encryptedText))
33+
return encryptedText;
34+
35+
try
36+
{
37+
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
38+
byte[] decryptedBytes = ProtectedData.Unprotect(
39+
encryptedBytes,
40+
null, // No additional entropy
41+
DataProtectionScope.CurrentUser // Decrypt for current user only
42+
);
43+
return Encoding.UTF8.GetString(decryptedBytes);
44+
}
45+
catch (Exception ex)
46+
{
47+
throw new Exception($"Failed to decrypt string: {ex.Message}", ex);
48+
}
49+
}
50+
51+
public static bool IsEncrypted(string value)
52+
{
53+
if (string.IsNullOrEmpty(value))
54+
return false;
55+
56+
try
57+
{
58+
// Try to decode as Base64 - if it fails, it's likely plain text
59+
byte[] bytes = Convert.FromBase64String(value);
60+
61+
// If it's valid Base64, try to decrypt it
62+
// If decryption succeeds, it's encrypted
63+
ProtectedData.Unprotect(bytes, null, DataProtectionScope.CurrentUser);
64+
return true;
65+
}
66+
catch
67+
{
68+
// If Base64 decode or decryption fails, it's plain text
69+
return false;
70+
}
71+
}
72+
}

SourceServerManager/Services/ServerConfigurationService.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,71 @@ public async Task<List<ServerConfig>> LoadServersAsync()
6565

6666
// Deserialize the server configurations
6767
var servers = JsonSerializer.Deserialize<List<ServerConfig>>(json, _jsonOptions);
68+
var serverList = servers ?? [];
6869

69-
return servers ?? [];
70+
// Check if migration from plain text passwords is needed
71+
bool needsMigration = MigratePlainTextPasswords(serverList);
72+
73+
// If migration occurred, save the updated configurations
74+
if (needsMigration)
75+
{
76+
await SaveServersAsync(serverList);
77+
}
78+
79+
return serverList;
7080
}
7181
catch (Exception ex)
7282
{
7383
throw new Exception($"Failed to load server configurations: {ex.Message}", ex);
7484
}
7585
}
86+
87+
private bool MigratePlainTextPasswords(List<ServerConfig> servers)
88+
{
89+
bool migrationOccurred = false;
90+
91+
try
92+
{
93+
foreach (var server in servers)
94+
{
95+
bool serverMigrated = false;
96+
97+
// Check if RCON password needs migration (plain text to encrypted)
98+
var currentRconPassword = GetStoredPassword(server, "_rconPassword");
99+
if (!string.IsNullOrEmpty(currentRconPassword) && !EncryptionService.IsEncrypted(currentRconPassword))
100+
{
101+
server.RconPassword = currentRconPassword; // This will encrypt it automatically
102+
serverMigrated = true;
103+
}
104+
105+
// Check if FTP password needs migration (plain text to encrypted)
106+
var currentFtpPassword = GetStoredPassword(server, "_ftpPassword");
107+
if (!string.IsNullOrEmpty(currentFtpPassword) && !EncryptionService.IsEncrypted(currentFtpPassword))
108+
{
109+
server.FtpPassword = currentFtpPassword; // This will encrypt it automatically
110+
serverMigrated = true;
111+
}
112+
113+
if (serverMigrated)
114+
{
115+
migrationOccurred = true;
116+
}
117+
}
118+
}
119+
catch (Exception ex)
120+
{
121+
// Log the error but don't fail the entire load process
122+
Console.WriteLine($"Warning: Password migration failed: {ex.Message}");
123+
}
124+
125+
return migrationOccurred;
126+
}
127+
128+
private string GetStoredPassword(ServerConfig server, string fieldName)
129+
{
130+
// Access the private field using reflection to get stored password
131+
var field = typeof(ServerConfig).GetField(fieldName,
132+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
133+
return field?.GetValue(server) as string ?? string.Empty;
134+
}
76135
}

SourceServerManager/SourceServerManager.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<PackageReference Include="FluentFTP" Version="52.0.0" />
2929
<PackageReference Include="Okolni.Source.Query" Version="2.1.0" />
3030
<PackageReference Include="SSH.NET" Version="2024.2.0" />
31+
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
3132
</ItemGroup>
3233

3334
<ItemGroup>

global.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"sdk": {
3+
"version": "9.0.304"
4+
}
5+
}

0 commit comments

Comments
 (0)