Add encryption for Kaigen server auth
This commit is contained in:
@@ -6,30 +6,30 @@ The Kaigen service is used for managing user credits during backtest operations.
|
|||||||
|
|
||||||
### Required Environment Variable
|
### Required Environment Variable
|
||||||
|
|
||||||
- **`KAIGEN_PRIVATE_KEY`**: The private key used for signing API requests to the Kaigen service.
|
- **`KAIGEN_SECRET_KEY`**: The secret key used for AES-256-CBC encryption of Basic Auth tokens sent to the Kaigen service.
|
||||||
|
|
||||||
### Setting the Environment Variable
|
### Setting the Environment Variable
|
||||||
|
|
||||||
#### Development
|
#### Development
|
||||||
```bash
|
```bash
|
||||||
export KAIGEN_PRIVATE_KEY="your-private-key-here"
|
export KAIGEN_SECRET_KEY="your-secret-key-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Production
|
#### Production
|
||||||
Set the environment variable in your deployment configuration:
|
Set the environment variable in your deployment configuration:
|
||||||
```bash
|
```bash
|
||||||
KAIGEN_PRIVATE_KEY=your-private-key-here
|
KAIGEN_SECRET_KEY=your-secret-key-here
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
```bash
|
```bash
|
||||||
docker run -e KAIGEN_PRIVATE_KEY=your-private-key-here your-app
|
docker run -e KAIGEN_SECRET_KEY=your-secret-key-here your-app
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
```yaml
|
```yaml
|
||||||
environment:
|
environment:
|
||||||
- KAIGEN_PRIVATE_KEY=your-private-key-here
|
- KAIGEN_SECRET_KEY=your-secret-key-here
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Structure
|
## Configuration Structure
|
||||||
@@ -42,11 +42,28 @@ The Kaigen service configuration is defined in `appsettings.json`:
|
|||||||
"BaseUrl": "https://api.kaigen.managing.live",
|
"BaseUrl": "https://api.kaigen.managing.live",
|
||||||
"DebitEndpoint": "/api/credits/debit",
|
"DebitEndpoint": "/api/credits/debit",
|
||||||
"RefundEndpoint": "/api/credits/refund",
|
"RefundEndpoint": "/api/credits/refund",
|
||||||
"PrivateKey": "${KAIGEN_PRIVATE_KEY}"
|
"SecretKey": "${KAIGEN_SECRET_KEY}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Authentication Method
|
||||||
|
|
||||||
|
The service now uses **Basic Authentication** with AES-256-GCM encrypted tokens:
|
||||||
|
|
||||||
|
1. **Token Format**: `{walletAddress}-{username}`
|
||||||
|
2. **Encryption**: The token is encrypted using AES-256-GCM with the configured secret key
|
||||||
|
3. **Basic Auth**: The encrypted token is sent in the Authorization header as `Basic {base64EncodedToken}:`
|
||||||
|
|
||||||
|
### Example Token Generation
|
||||||
|
```csharp
|
||||||
|
// For user "john" with wallet "0x123..."
|
||||||
|
var authToken = "0x123...-john";
|
||||||
|
var encryptedToken = CryptoHelpers.EncryptAesGcm(authToken, secretKey);
|
||||||
|
var basicAuth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{encryptedToken}:"));
|
||||||
|
// Result: Authorization: Basic {base64EncodedToken}:
|
||||||
|
```
|
||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
- **PUT** `/api/credits/debit` - Debit credits from user account
|
- **PUT** `/api/credits/debit` - Debit credits from user account
|
||||||
@@ -54,11 +71,21 @@ The Kaigen service configuration is defined in `appsettings.json`:
|
|||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
|
|
||||||
- The private key should never be committed to source control
|
- The secret key should never be committed to source control
|
||||||
- Use environment variables or secure configuration management systems
|
- Use environment variables or secure configuration management systems
|
||||||
- The private key is used for signing API requests to ensure authenticity
|
- The secret key is used for AES-256-GCM encryption of authentication tokens
|
||||||
- Rotate the private key regularly for enhanced security
|
- Rotate the secret key regularly for enhanced security
|
||||||
|
- Each request uses a unique nonce for encryption, ensuring replay attack protection
|
||||||
|
- The GCM mode provides both confidentiality and authenticity
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
If the `KAIGEN_PRIVATE_KEY` environment variable is not set, the application will throw an `InvalidOperationException` with a clear error message during startup.
|
If the `KAIGEN_SECRET_KEY` environment variable is not set, the application will throw an `InvalidOperationException` with a clear error message during startup.
|
||||||
|
|
||||||
|
## Migration from Private Key Authentication
|
||||||
|
|
||||||
|
If migrating from the previous private key signature method:
|
||||||
|
|
||||||
|
1. Replace `KAIGEN_PRIVATE_KEY` with `KAIGEN_SECRET_KEY` in your environment variables
|
||||||
|
2. Update any configuration files to use the new `SecretKey` property instead of `PrivateKey`
|
||||||
|
3. The Kaigen server must be updated to handle Basic Auth with AES-256-GCM decryption
|
||||||
@@ -34,6 +34,7 @@ namespace Managing.Application.Tests
|
|||||||
var discordService = new Mock<IMessengerService>().Object;
|
var discordService = new Mock<IMessengerService>().Object;
|
||||||
var scenarioService = new Mock<IScenarioService>().Object;
|
var scenarioService = new Mock<IScenarioService>().Object;
|
||||||
var messengerService = new Mock<IMessengerService>().Object;
|
var messengerService = new Mock<IMessengerService>().Object;
|
||||||
|
var kaigenService = new Mock<IKaigenService>().Object;
|
||||||
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
|
var tradingBotLogger = TradingBaseTests.CreateTradingBotLogger();
|
||||||
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
|
var backtestLogger = TradingBaseTests.CreateBacktesterLogger();
|
||||||
var botService = new Mock<IBotService>().Object;
|
var botService = new Mock<IBotService>().Object;
|
||||||
@@ -45,7 +46,7 @@ namespace Managing.Application.Tests
|
|||||||
_tradingService.Object,
|
_tradingService.Object,
|
||||||
botService);
|
botService);
|
||||||
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
|
_backtester = new Backtester(_exchangeService, _botFactory, backtestRepository, backtestLogger,
|
||||||
scenarioService, _accountService.Object, messengerService);
|
scenarioService, _accountService.Object, messengerService, kaigenService);
|
||||||
_elapsedTimes = new List<double>();
|
_elapsedTimes = new List<double>();
|
||||||
|
|
||||||
// Initialize cross-platform file paths
|
// Initialize cross-platform file paths
|
||||||
|
|||||||
39
src/Managing.Application.Tests/CryptoHelpersTests.cs
Normal file
39
src/Managing.Application.Tests/CryptoHelpersTests.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using Managing.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Managing.Application.Tests;
|
||||||
|
|
||||||
|
public class CryptoHelpersTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EncryptAesGcm_And_DecryptAesGcm_RoundTrip_Works()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var secretKey = "supersecretkey1234567890123456"; // 32 bytes
|
||||||
|
var token = "0x1234567890abcdef-johndoe";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var encrypted = CryptoHelpers.EncryptAesGcm(token, secretKey);
|
||||||
|
var decrypted = CryptoHelpers.DecryptAesGcm(encrypted, secretKey);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(encrypted);
|
||||||
|
Assert.NotEqual(token, encrypted); // Should be encrypted
|
||||||
|
Assert.Equal(token, decrypted); // Should decrypt to original
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecryptAesGcm_WithWrongKey_Throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var secretKey = "supersecretkey1234567890123456";
|
||||||
|
var wrongKey = "wrongsecretkey6543210987654321";
|
||||||
|
var token = "0xabcdef-jane";
|
||||||
|
var encrypted = CryptoHelpers.EncryptAesGcm(token, secretKey);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<CryptographicException>(() =>
|
||||||
|
CryptoHelpers.DecryptAesGcm(encrypted, wrongKey));
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/Managing.Core/CryptoHelpers.cs
Normal file
147
src/Managing.Core/CryptoHelpers.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Managing.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides AES-256-CBC encryption utilities with HMAC-SHA256 authentication for secure token generation.
|
||||||
|
/// </summary>
|
||||||
|
public static class CryptoHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Encrypts a string using AES-256-CBC encryption with HMAC-SHA256 authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plaintext">The text to encrypt</param>
|
||||||
|
/// <param name="secretKey">The secret key for encryption (should be 32 bytes for AES-256)</param>
|
||||||
|
/// <returns>Base64 encoded encrypted data with IV and HMAC</returns>
|
||||||
|
public static string EncryptAesCbc(string plaintext, string secretKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(plaintext))
|
||||||
|
throw new ArgumentException("Plaintext cannot be null or empty", nameof(plaintext));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
throw new ArgumentException("Secret key cannot be null or empty", nameof(secretKey));
|
||||||
|
|
||||||
|
// Convert secret key to bytes (ensure it's 32 bytes for AES-256)
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
if (keyBytes.Length != 32)
|
||||||
|
{
|
||||||
|
// If key is not 32 bytes, hash it to get 32 bytes
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
keyBytes = sha256.ComputeHash(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert plaintext to bytes
|
||||||
|
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||||
|
|
||||||
|
// Generate a random IV (16 bytes for AES)
|
||||||
|
var iv = new byte[16];
|
||||||
|
using (var rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
rng.GetBytes(iv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt using AES-CBC
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = keyBytes;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var ciphertext = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
|
||||||
|
|
||||||
|
// Create HMAC for authentication
|
||||||
|
using var hmac = new HMACSHA256(keyBytes);
|
||||||
|
var hmacBytes = hmac.ComputeHash(ciphertext);
|
||||||
|
|
||||||
|
// Combine IV + encrypted data + HMAC
|
||||||
|
var result = new byte[iv.Length + ciphertext.Length + hmacBytes.Length];
|
||||||
|
Buffer.BlockCopy(iv, 0, result, 0, iv.Length);
|
||||||
|
Buffer.BlockCopy(ciphertext, 0, result, iv.Length, ciphertext.Length);
|
||||||
|
Buffer.BlockCopy(hmacBytes, 0, result, iv.Length + ciphertext.Length, hmacBytes.Length);
|
||||||
|
|
||||||
|
return Convert.ToBase64String(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrypts a string using AES-256-CBC decryption with HMAC-SHA256 authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedData">Base64 encoded encrypted data with IV and HMAC</param>
|
||||||
|
/// <param name="secretKey">The secret key for decryption (should be 32 bytes for AES-256)</param>
|
||||||
|
/// <returns>The decrypted plaintext</returns>
|
||||||
|
public static string DecryptAesCbc(string encryptedData, string secretKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(encryptedData))
|
||||||
|
throw new ArgumentException("Encrypted data cannot be null or empty", nameof(encryptedData));
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(secretKey))
|
||||||
|
throw new ArgumentException("Secret key cannot be null or empty", nameof(secretKey));
|
||||||
|
|
||||||
|
// Convert secret key to bytes (ensure it's 32 bytes for AES-256)
|
||||||
|
var keyBytes = Encoding.UTF8.GetBytes(secretKey);
|
||||||
|
if (keyBytes.Length != 32)
|
||||||
|
{
|
||||||
|
// If key is not 32 bytes, hash it to get 32 bytes
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
keyBytes = sha256.ComputeHash(keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the base64 data
|
||||||
|
var encryptedBytes = Convert.FromBase64String(encryptedData);
|
||||||
|
|
||||||
|
// Extract IV (first 16 bytes), HMAC (last 32 bytes), and ciphertext (middle)
|
||||||
|
if (encryptedBytes.Length < 48) // 16 (IV) + 32 (HMAC) = minimum 48 bytes
|
||||||
|
throw new ArgumentException("Encrypted data is too short", nameof(encryptedData));
|
||||||
|
|
||||||
|
var iv = new byte[16];
|
||||||
|
var hmacBytes = new byte[32];
|
||||||
|
var ciphertext = new byte[encryptedBytes.Length - 48];
|
||||||
|
|
||||||
|
Buffer.BlockCopy(encryptedBytes, 0, iv, 0, 16);
|
||||||
|
Buffer.BlockCopy(encryptedBytes, encryptedBytes.Length - 32, hmacBytes, 0, 32);
|
||||||
|
Buffer.BlockCopy(encryptedBytes, 16, ciphertext, 0, ciphertext.Length);
|
||||||
|
|
||||||
|
// Verify HMAC for authentication
|
||||||
|
using var hmac = new HMACSHA256(keyBytes);
|
||||||
|
var computedHmac = hmac.ComputeHash(ciphertext);
|
||||||
|
|
||||||
|
if (!computedHmac.SequenceEqual(hmacBytes))
|
||||||
|
{
|
||||||
|
throw new CryptographicException("HMAC verification failed - data may have been tampered with");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt using AES-CBC
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = keyBytes;
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.PKCS7;
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
var decryptedBytes = decryptor.TransformFinalBlock(ciphertext, 0, ciphertext.Length);
|
||||||
|
return Encoding.UTF8.GetString(decryptedBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encrypts a string using AES-256-CBC encryption (alias for EncryptAesCbc for backward compatibility).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="plaintext">The text to encrypt</param>
|
||||||
|
/// <param name="secretKey">The secret key for encryption</param>
|
||||||
|
/// <returns>Base64 encoded encrypted data</returns>
|
||||||
|
public static string EncryptAesGcm(string plaintext, string secretKey)
|
||||||
|
{
|
||||||
|
return EncryptAesCbc(plaintext, secretKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrypts a string using AES-256-CBC decryption (alias for DecryptAesCbc for backward compatibility).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="encryptedData">Base64 encoded encrypted data</param>
|
||||||
|
/// <param name="secretKey">The secret key for decryption</param>
|
||||||
|
/// <returns>The decrypted plaintext</returns>
|
||||||
|
public static string DecryptAesGcm(string encryptedData, string secretKey)
|
||||||
|
{
|
||||||
|
return DecryptAesCbc(encryptedData, secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<ProjectReference Include="..\Managing.ABI.GmxV2\Managing.ABI.GmxV2.csproj"/>
|
<ProjectReference Include="..\Managing.ABI.GmxV2\Managing.ABI.GmxV2.csproj"/>
|
||||||
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
|
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
|
||||||
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
|
<ProjectReference Include="..\Managing.Application\Managing.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\Managing.Core\Managing.Core.csproj"/>
|
||||||
<ProjectReference Include="..\Managing.Tools.ABI\Managing.Tools.ABI.csproj"/>
|
<ProjectReference Include="..\Managing.Tools.ABI\Managing.Tools.ABI.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Core;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Nethereum.Signer;
|
|
||||||
|
|
||||||
namespace Managing.Infrastructure.Evm.Services;
|
namespace Managing.Infrastructure.Evm.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration settings for Kaigen credit management service.
|
/// Configuration settings for Kaigen credit management service.
|
||||||
/// The PrivateKey should be set via the KAIGEN_PRIVATE_KEY environment variable for security.
|
/// The SecretKey should be set via the KAIGEN_SECRET_KEY environment variable for security.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class KaigenSettings
|
public class KaigenSettings
|
||||||
{
|
{
|
||||||
public string BaseUrl { get; set; } = "https://api.kaigen.managing.live";
|
public string BaseUrl { get; set; } = "https://api.kaigen.managing.live";
|
||||||
public string DebitEndpoint { get; set; } = "/api/credits/debit";
|
public string DebitEndpoint { get; set; } = "/api/credits/debit";
|
||||||
public string RefundEndpoint { get; set; } = "/api/credits/refund";
|
public string RefundEndpoint { get; set; } = "/api/credits/refund";
|
||||||
public string PrivateKey { get; set; } = string.Empty;
|
public string SecretKey { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class KaigenService : IKaigenService
|
public class KaigenService : IKaigenService
|
||||||
@@ -45,22 +47,22 @@ public class KaigenService : IKaigenService
|
|||||||
if (!_creditsEnabled)
|
if (!_creditsEnabled)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Kaigen credits are disabled via KAIGEN_CREDITS_ENABLED environment variable");
|
_logger.LogInformation("Kaigen credits are disabled via KAIGEN_CREDITS_ENABLED environment variable");
|
||||||
return; // Skip private key validation if credits are disabled
|
return; // Skip secret key validation if credits are disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always read from environment variable for security
|
// Always read from environment variable for security
|
||||||
var envPrivateKey = Environment.GetEnvironmentVariable("KAIGEN_PRIVATE_KEY");
|
var envSecretKey = Environment.GetEnvironmentVariable("KAIGEN_SECRET_KEY");
|
||||||
if (!string.IsNullOrEmpty(envPrivateKey))
|
if (!string.IsNullOrEmpty(envSecretKey))
|
||||||
{
|
{
|
||||||
_settings.PrivateKey = envPrivateKey;
|
_settings.SecretKey = envSecretKey;
|
||||||
_logger.LogInformation("Using KAIGEN_PRIVATE_KEY from environment variable");
|
_logger.LogInformation("Using KAIGEN_SECRET_KEY from environment variable");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required settings only if credits are enabled
|
// Validate required settings only if credits are enabled
|
||||||
if (string.IsNullOrEmpty(_settings.PrivateKey))
|
if (string.IsNullOrEmpty(_settings.SecretKey))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Kaigen PrivateKey is not configured. Please set the KAIGEN_PRIVATE_KEY environment variable.");
|
"Kaigen SecretKey is not configured. Please set the KAIGEN_SECRET_KEY environment variable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_settings.BaseUrl))
|
if (string.IsNullOrEmpty(_settings.BaseUrl))
|
||||||
@@ -86,29 +88,22 @@ public class KaigenService : IKaigenService
|
|||||||
var walletAddress = GetUserWalletAddress(user);
|
var walletAddress = GetUserWalletAddress(user);
|
||||||
var requestId = Guid.NewGuid().ToString();
|
var requestId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
// Create the message to sign (concatenate the values)
|
|
||||||
var message = $"{requestId}{walletAddress}{debitAmount}";
|
|
||||||
|
|
||||||
// Sign the message
|
|
||||||
var signature = SignMessage(message, _settings.PrivateKey);
|
|
||||||
|
|
||||||
// Create the request payload
|
// Create the request payload
|
||||||
var requestPayload = new
|
var requestPayload = new
|
||||||
{
|
{
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
walletAddress = walletAddress,
|
walletAddress = walletAddress,
|
||||||
debitAmount = debitAmount,
|
debitAmount = debitAmount
|
||||||
signature = signature
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Debiting {Amount} credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}",
|
"Debiting {Amount} credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}",
|
||||||
debitAmount, user.Name, walletAddress, requestId);
|
debitAmount, user.Name, walletAddress, requestId);
|
||||||
|
|
||||||
var response = await _httpClient.PutAsJsonAsync(
|
var response = await SendAuthenticatedRequestAsync(
|
||||||
$"{_settings.BaseUrl}{_settings.DebitEndpoint}",
|
$"{_settings.BaseUrl}{_settings.DebitEndpoint}",
|
||||||
requestPayload,
|
requestPayload,
|
||||||
_jsonOptions);
|
user);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -146,28 +141,21 @@ public class KaigenService : IKaigenService
|
|||||||
{
|
{
|
||||||
var walletAddress = GetUserWalletAddress(user);
|
var walletAddress = GetUserWalletAddress(user);
|
||||||
|
|
||||||
// Create the message to sign (concatenate the values)
|
|
||||||
var message = $"{requestId}{walletAddress}";
|
|
||||||
|
|
||||||
// Sign the message
|
|
||||||
var signature = SignMessage(message, _settings.PrivateKey);
|
|
||||||
|
|
||||||
// Create the request payload
|
// Create the request payload
|
||||||
var requestPayload = new
|
var requestPayload = new
|
||||||
{
|
{
|
||||||
requestId = requestId,
|
requestId = requestId,
|
||||||
walletAddress = walletAddress,
|
walletAddress = walletAddress
|
||||||
signature = signature
|
|
||||||
};
|
};
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Refunding credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}",
|
"Refunding credits for user {UserName} (wallet: {WalletAddress}) with request ID {RequestId}",
|
||||||
user.Name, walletAddress, requestId);
|
user.Name, walletAddress, requestId);
|
||||||
|
|
||||||
var response = await _httpClient.PutAsJsonAsync(
|
var response = await SendAuthenticatedRequestAsync(
|
||||||
$"{_settings.BaseUrl}{_settings.RefundEndpoint}",
|
$"{_settings.BaseUrl}{_settings.RefundEndpoint}",
|
||||||
requestPayload,
|
requestPayload,
|
||||||
_jsonOptions);
|
user);
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -188,6 +176,28 @@ public class KaigenService : IKaigenService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> SendAuthenticatedRequestAsync(string url, object payload, User user)
|
||||||
|
{
|
||||||
|
// Create the auth token: "walletaddress-username"
|
||||||
|
var authToken = $"{GetUserWalletAddress(user)}-{user.Name}";
|
||||||
|
|
||||||
|
// Encrypt the auth token using AES-256-GCM
|
||||||
|
var encryptedToken = CryptoHelpers.EncryptAesGcm(authToken, _settings.SecretKey);
|
||||||
|
|
||||||
|
// Create Basic Auth header with the encrypted token
|
||||||
|
var basicAuthString = $"{encryptedToken}:";
|
||||||
|
var base64Auth = Convert.ToBase64String(Encoding.ASCII.GetBytes(basicAuthString));
|
||||||
|
|
||||||
|
// Create a new request with the auth header
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Put, url)
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(payload, options: _jsonOptions)
|
||||||
|
};
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64Auth);
|
||||||
|
|
||||||
|
return await _httpClient.SendAsync(request);
|
||||||
|
}
|
||||||
|
|
||||||
private string GetUserWalletAddress(User user)
|
private string GetUserWalletAddress(User user)
|
||||||
{
|
{
|
||||||
if (user?.Accounts == null || !user.Accounts.Any())
|
if (user?.Accounts == null || !user.Accounts.Any())
|
||||||
@@ -206,13 +216,6 @@ public class KaigenService : IKaigenService
|
|||||||
return walletAddress;
|
return walletAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SignMessage(string message, string privateKey)
|
|
||||||
{
|
|
||||||
var signer = new EthereumMessageSigner();
|
|
||||||
var signature = signer.EncodeUTF8AndSign(message, new EthECKey(privateKey));
|
|
||||||
return signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class KaigenResponse
|
private class KaigenResponse
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user