using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using IdesLspPoc.Credentials;
using IdesLspPoc.Output;
using Microsoft.VisualStudio.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using StreamJsonRpc;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace IdesLspPoc.LspClient.S3
{
///
/// This class knows how to push credentials over to the language server (via SendIamCredentialsAsync)
/// PROOF OF CONCEPT - this class would be used in a product like the AWS Toolkit
/// to push credentials to the server whenever the credentials state changes (eg: user selects another profile, or
/// credentials expire). For this concept, it is resolving and pushing credentials every 10 seconds as a
/// simulation of the Toolkit sending credentials to the server.
/// This class would also know how to clear credentials from the language server.
///
internal class S3CredentialsUpdater
{
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy(),
}
};
private Timer _resendTimer;
private OutputWindow _outputWindow;
private JsonRpc _rpc;
private byte[] _aesKey;
public S3CredentialsUpdater(JsonRpc rpc, byte[] aesKey, OutputWindow outputWindow)
{
this._rpc = rpc;
_aesKey = aesKey;
_outputWindow = outputWindow;
}
///
/// We start sending the lsp server credentials every 10 seconds as a simulation of the credentials state changing
///
public void StartCredentialsRefreshSimulation()
{
_resendTimer?.Stop();
_resendTimer = new Timer()
{
AutoReset = true,
Interval = 10_000,
};
_resendTimer.Elapsed += OnRefreshCredentials;
_resendTimer.Start();
}
private void OnRefreshCredentials(object sender, ElapsedEventArgs e)
{
// PROOF OF CONCEPT
// We will resolve the default profile from the local system.
// In a product, the host extension would know which profile it is configured to provide to the language server.
var creds = new SharedCredentialsFile();
if (!creds.TryGetProfile("default", out var profile))
{
_outputWindow.WriteLine("Client: Unable to get default profile");
return;
}
Task.Run(async () =>
{
AWSCredentials awsCredentials = profile.GetAWSCredentials(creds);
var request = CreateUpdateCredentialsRequest(await awsCredentials.GetCredentialsAsync(), _aesKey);
await SendIamCredentialsAsync(request);
}).Forget();
}
public async Task SendIamCredentialsAsync(UpdateCredentialsRequest request)
{
_outputWindow.WriteLine("Client: Sending (simulated) refreshed Credentials to the server");
await this._rpc.NotifyAsync("$/aws/credentials/iam/update", request);
}
private static UpdateCredentialsRequest CreateUpdateCredentialsRequest(ImmutableCredentials credentials, byte[] aesKey)
{
var requestData = new UpdateIamCredentialsRequestData
{
AccessKeyId = credentials.AccessKey,
SecretAccessKey = credentials.SecretKey,
SessionToken = credentials.Token,
};
return CreateUpdateCredentialsRequest(requestData, aesKey);
}
///
/// Creates an "update credentials" request that contains encrypted data
///
private static UpdateCredentialsRequest CreateUpdateCredentialsRequest(object data, byte[] aesKey)
{
byte[] iv = CreateInitializationVector();
var aesEngine = new AesEngine();
int macSize = 8 * aesEngine.GetBlockSize();
GcmBlockCipher cipher = new GcmBlockCipher(aesEngine);
AeadParameters parameters = new AeadParameters(new KeyParameter(aesKey), macSize, iv);
cipher.Init(true, parameters);
var json = JsonConvert.SerializeObject(data, _serializerSettings);
// Encrypt json
byte[] cipherText = new byte[cipher.GetOutputSize(json.Length)];
int len = cipher.ProcessBytes(Encoding.UTF8.GetBytes(json), 0, json.Length, cipherText, 0);
cipher.DoFinal(cipherText, len);
// Get the authTag
byte[] mac = cipher.GetMac();
string authtag = Convert.ToBase64String(mac);
var dataLength = cipherText.Length - mac.Length;
return new UpdateCredentialsRequest
{
Iv = Convert.ToBase64String(iv),
// Remove Mac from end of cipherText
Data = Convert.ToBase64String(cipherText, 0, dataLength),
AuthTag = authtag,
};
}
private static byte[] CreateInitializationVector()
{
var iv = new byte[16];
SecureRandom random = new SecureRandom();
random.NextBytes(iv);
return iv;
}
}
}