Skip to content

Commit c84b137

Browse files
encrypt lcid (#7)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7ada2e6 commit c84b137

File tree

7 files changed

+756
-14
lines changed

7 files changed

+756
-14
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Threading.Tasks;
2+
using Unity.Services.CloudCode.Apis;
3+
using Unity.Services.CloudCode.Core;
4+
5+
namespace Unity.WalmartAuthRelay.Interfaces;
6+
7+
public interface IEncryptionService
8+
{
9+
Task<string> EncryptAsync(IExecutionContext ctx, IGameApiClient client, string plainText);
10+
Task<string> DecryptAsync(IExecutionContext ctx, IGameApiClient client, string encryptedText);
11+
Task<bool> IsEncryptedAsync(string value);
12+
}

Project/ModuleConfig.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public void Setup(ICloudCodeConfig config)
1515
config.Dependencies.AddSingleton<IMapperService, MapperService>();
1616
config.Dependencies.AddSingleton<IConfigService, RemoteConfigService>();
1717
config.Dependencies.AddSingleton<ISecretService, SecretManagerService>();
18+
config.Dependencies.AddSingleton<IEncryptionService, AesEncryptionService>();
1819
config.Dependencies.AddSingleton<IPlayerDataService, PlayerDataService>();
1920
config.Dependencies.AddSingleton<IHttpClientFactory, HttpClientFactory>();
2021
config.Dependencies.AddSingleton<IWalmartAuthService, WalmartAuthService>();
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using System;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
using System.Threading.Tasks;
5+
using Microsoft.Extensions.Logging;
6+
using Unity.WalmartAuthRelay.Interfaces;
7+
using Unity.Services.CloudCode.Apis;
8+
using Unity.Services.CloudCode.Core;
9+
10+
namespace Unity.WalmartAuthRelay.Services;
11+
12+
public class AesEncryptionService : IEncryptionService
13+
{
14+
private readonly ILogger<AesEncryptionService> _logger;
15+
private readonly ISecretService _secretService;
16+
17+
private const string ENCRYPTION_PREFIX = "ENC:";
18+
private const string ENCRYPTION_KEY_NAME = "LCID_ENCRYPTION_KEY";
19+
private const int IV_SIZE = 12; // 96-bit IV for GCM
20+
private const int TAG_SIZE = 16; // 128-bit tag for GCM
21+
22+
public AesEncryptionService(ILogger<AesEncryptionService> logger, ISecretService secretService)
23+
{
24+
_logger = logger;
25+
_secretService = secretService;
26+
}
27+
28+
public async Task<string> EncryptAsync(IExecutionContext ctx, IGameApiClient client, string plainText)
29+
{
30+
if (string.IsNullOrEmpty(plainText))
31+
{
32+
throw new ArgumentException("Plain text cannot be null or empty", nameof(plainText));
33+
}
34+
35+
try
36+
{
37+
var key = await GetEncryptionKeyAsync(ctx, client);
38+
var plainBytes = Encoding.UTF8.GetBytes(plainText);
39+
40+
using var aes = new AesGcm(key, TAG_SIZE);
41+
var iv = new byte[IV_SIZE];
42+
var ciphertext = new byte[plainBytes.Length];
43+
var tag = new byte[TAG_SIZE];
44+
45+
RandomNumberGenerator.Fill(iv);
46+
aes.Encrypt(iv, plainBytes, ciphertext, tag);
47+
48+
// Format: IV + Tag + Ciphertext
49+
var encryptedData = new byte[IV_SIZE + TAG_SIZE + ciphertext.Length];
50+
Buffer.BlockCopy(iv, 0, encryptedData, 0, IV_SIZE);
51+
Buffer.BlockCopy(tag, 0, encryptedData, IV_SIZE, TAG_SIZE);
52+
Buffer.BlockCopy(ciphertext, 0, encryptedData, IV_SIZE + TAG_SIZE, ciphertext.Length);
53+
54+
return ENCRYPTION_PREFIX + Convert.ToBase64String(encryptedData);
55+
}
56+
catch (Exception ex)
57+
{
58+
_logger.LogError(ex, "Failed to encrypt data");
59+
throw new InvalidOperationException("Encryption failed", ex);
60+
}
61+
}
62+
63+
public async Task<string> DecryptAsync(IExecutionContext ctx, IGameApiClient client, string encryptedText)
64+
{
65+
if (string.IsNullOrEmpty(encryptedText))
66+
{
67+
throw new ArgumentException("Encrypted text cannot be null or empty", nameof(encryptedText));
68+
}
69+
70+
if (!encryptedText.StartsWith(ENCRYPTION_PREFIX))
71+
{
72+
throw new ArgumentException("Invalid encrypted data format", nameof(encryptedText));
73+
}
74+
75+
try
76+
{
77+
var key = await GetEncryptionKeyAsync(ctx, client);
78+
var base64Data = encryptedText.Substring(ENCRYPTION_PREFIX.Length);
79+
var encryptedData = Convert.FromBase64String(base64Data);
80+
81+
if (encryptedData.Length < IV_SIZE + TAG_SIZE)
82+
{
83+
throw new ArgumentException("Invalid encrypted data length", nameof(encryptedText));
84+
}
85+
86+
var iv = new byte[IV_SIZE];
87+
var tag = new byte[TAG_SIZE];
88+
var ciphertext = new byte[encryptedData.Length - IV_SIZE - TAG_SIZE];
89+
90+
Buffer.BlockCopy(encryptedData, 0, iv, 0, IV_SIZE);
91+
Buffer.BlockCopy(encryptedData, IV_SIZE, tag, 0, TAG_SIZE);
92+
Buffer.BlockCopy(encryptedData, IV_SIZE + TAG_SIZE, ciphertext, 0, ciphertext.Length);
93+
94+
using var aes = new AesGcm(key, TAG_SIZE);
95+
var plainBytes = new byte[ciphertext.Length];
96+
aes.Decrypt(iv, ciphertext, tag, plainBytes);
97+
98+
return Encoding.UTF8.GetString(plainBytes);
99+
}
100+
catch (Exception ex)
101+
{
102+
_logger.LogError(ex, "Failed to decrypt data");
103+
throw new InvalidOperationException("Decryption failed", ex);
104+
}
105+
}
106+
107+
public Task<bool> IsEncryptedAsync(string value)
108+
{
109+
return Task.FromResult(!string.IsNullOrEmpty(value) && value.StartsWith(ENCRYPTION_PREFIX));
110+
}
111+
112+
private async Task<byte[]> GetEncryptionKeyAsync(IExecutionContext ctx, IGameApiClient client)
113+
{
114+
try
115+
{
116+
var keyBase64 = await _secretService.GetValueWithRetryAsync(ctx, client, ENCRYPTION_KEY_NAME);
117+
if (string.IsNullOrEmpty(keyBase64))
118+
{
119+
throw new InvalidOperationException($"Encryption key '{ENCRYPTION_KEY_NAME}' not found in Secret Manager");
120+
}
121+
122+
var key = Convert.FromBase64String(keyBase64);
123+
if (key.Length != 32) // 256 bits = 32 bytes
124+
{
125+
throw new InvalidOperationException($"Encryption key must be 256 bits (32 bytes), but was {key.Length} bytes");
126+
}
127+
128+
return key;
129+
}
130+
catch (Exception ex)
131+
{
132+
_logger.LogError(ex, "Failed to retrieve encryption key from Secret Manager");
133+
throw;
134+
}
135+
}
136+
}

Project/Services/PlayerDataService.cs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net;
45
using System.Threading.Tasks;
@@ -14,61 +15,118 @@ namespace Unity.WalmartAuthRelay.Services;
1415
public class PlayerDataService : IPlayerDataService
1516
{
1617
private readonly ILogger<IPlayerDataService> _logger;
18+
private readonly IEncryptionService _encryptionService;
1719

1820
// Name of key within Player Data that stores the user's LCID.
1921
private const string LCID_PLAYER_DATA_KEY = "LCID";
2022

21-
public PlayerDataService(ILogger<IPlayerDataService> logger)
23+
public PlayerDataService(ILogger<IPlayerDataService> logger, IEncryptionService encryptionService)
2224
{
23-
_logger = logger;
25+
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
26+
_encryptionService = encryptionService ?? throw new ArgumentNullException(nameof(encryptionService));
2427
}
2528

2629
public async Task<string> GetPlayerLcidAsync(IExecutionContext ctx, IGameApiClient client)
2730
{
28-
string playerLcid;
29-
3031
if (ctx.PlayerId == null)
3132
{
32-
var message = "Received null PlayerId from context in StorePlayerLcidAsync()";
33+
var message = "Received null PlayerId from context in GetPlayerLcidAsync()";
3334
_logger.LogError(message);
3435
throw new ApiException(ApiExceptionType.InvalidParameters, message);
3536
}
3637

38+
string storedValue;
3739
try
3840
{
3941
var result = await client.CloudSaveData.GetItemsAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.PlayerId,
4042
new List<string>{ LCID_PLAYER_DATA_KEY });
41-
playerLcid = (string)result.Data.Results.First().Value;
43+
storedValue = (string)result.Data.Results.First().Value;
4244
}
4345
catch (ApiException e)
4446
{
4547
_logger.LogError($"Failed to retrieve player's LCID in Player Data: {e.Message}");
4648
throw;
4749
}
4850

49-
return playerLcid;
51+
// Check if the value is already encrypted
52+
if (await _encryptionService.IsEncryptedAsync(storedValue))
53+
{
54+
// Decrypt and return
55+
try
56+
{
57+
var decryptedValue = await _encryptionService.DecryptAsync(ctx, client, storedValue);
58+
_logger.LogDebug("Successfully decrypted LCID for player {PlayerId}", ctx.PlayerId);
59+
return decryptedValue;
60+
}
61+
catch (Exception ex)
62+
{
63+
_logger.LogError(ex, "Failed to decrypt LCID for player {PlayerId}", ctx.PlayerId);
64+
throw;
65+
}
66+
}
67+
68+
// Value is plain text - perform lazy migration
69+
_logger.LogInformation("Performing lazy migration: encrypting plain text LCID for player {PlayerId}", ctx.PlayerId);
70+
71+
try
72+
{
73+
// Store the encrypted version (lazy migration)
74+
await StorePlayerLcidAsync(ctx, client, storedValue);
75+
return storedValue;
76+
}
77+
catch (Exception ex)
78+
{
79+
_logger.LogWarning(ex, "Failed to migrate LCID to encrypted format for player {PlayerId}, returning plain text", ctx.PlayerId);
80+
// Return plain text even if migration fails to maintain functionality
81+
return storedValue;
82+
}
5083
}
5184

5285
public async Task<bool> StorePlayerLcidAsync(IExecutionContext ctx, IGameApiClient client, string lcid)
5386
{
54-
bool stored;
55-
5687
if (ctx.PlayerId == null)
5788
{
5889
var message = "Received null PlayerId from context in StorePlayerLcidAsync()";
5990
_logger.LogError(message);
6091
throw new ApiException(ApiExceptionType.InvalidParameters, message);
6192
}
6293

94+
if (string.IsNullOrEmpty(lcid))
95+
{
96+
var message = "LCID cannot be null or empty";
97+
_logger.LogError(message);
98+
throw new ApiException(ApiExceptionType.InvalidParameters, message);
99+
}
100+
101+
string encryptedLcid;
102+
try
103+
{
104+
// Always encrypt the LCID before storing
105+
encryptedLcid = await _encryptionService.EncryptAsync(ctx, client, lcid);
106+
_logger.LogDebug("Successfully encrypted LCID for player {PlayerId}", ctx.PlayerId);
107+
}
108+
catch (Exception ex)
109+
{
110+
var message = $"Failed to encrypt LCID for player {ctx.PlayerId}: {ex.Message}";
111+
_logger.LogError(ex, message);
112+
throw new InvalidOperationException(message, ex);
113+
}
114+
115+
bool stored;
63116
try
64117
{
65118
var result = await client.CloudSaveData.SetItemAsync(ctx, ctx.AccessToken, ctx.ProjectId, ctx.PlayerId,
66-
new SetItemBody(LCID_PLAYER_DATA_KEY, lcid));
119+
new SetItemBody(LCID_PLAYER_DATA_KEY, encryptedLcid));
67120
stored = result.StatusCode == HttpStatusCode.OK;
121+
122+
if (stored)
123+
{
124+
_logger.LogDebug("Successfully stored encrypted LCID for player {PlayerId}", ctx.PlayerId);
125+
}
68126
}
69127
catch (ApiException e)
70128
{
71-
var message = $"Failed to store player's LCID in Player Data: {e.Message}";
129+
var message = $"Failed to store encrypted LCID in Player Data: {e.Message}";
72130
_logger.LogError(message);
73131
throw;
74132
}

0 commit comments

Comments
 (0)