1
0
mirror of https://github.com/chylex/Minecraft-Phantom-Panel.git synced 2025-05-10 17:34:13 +02:00

Make it possible to use condensed agent key via environment variables

This commit is contained in:
chylex 2022-10-18 02:41:19 +02:00
parent de22e5695f
commit ff5d31bf05
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
17 changed files with 188 additions and 24 deletions

View File

@ -5,6 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent1" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 1" />
<env name="ALLOWED_RCON_PORTS" value="25575,27000,27001" />
<env name="ALLOWED_SERVER_PORTS" value="25565,26000,26001" />

View File

@ -5,6 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent2" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 2" />
<env name="ALLOWED_RCON_PORTS" value="27002-27006" />
<env name="ALLOWED_SERVER_PORTS" value="26002-26006" />

View File

@ -5,6 +5,7 @@
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/.workdir/Agent3" />
<option name="PASS_PARENT_ENVS" value="1" />
<envs>
<env name="AGENT_KEY" value="JXBQQYG5T267RQS75MXWBTCJZY5CKTCCGQY22MCZPHSQQSJYCHH2NG2TCNXQY6TBSXM9NQDRS2CMX" />
<env name="AGENT_NAME" value="Agent 3" />
<env name="ALLOWED_RCON_PORTS" value="27007" />
<env name="ALLOWED_SERVER_PORTS" value="26007" />

3
.workdir/.gitignore vendored
View File

@ -1,3 +0,0 @@
/Agent*/data
/Agent*/temp
/Server/keys

2
.workdir/Agent1/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

2
.workdir/Agent2/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

2
.workdir/Agent3/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

Binary file not shown.

1
.workdir/Server/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/keys

View File

@ -1,6 +1,7 @@
using NetMQ;
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO;
using Serilog;
@ -9,21 +10,51 @@ namespace Phantom.Agent;
static class AgentKey {
private static ILogger Logger { get; } = PhantomLogger.Create(typeof(AgentKey));
public static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) {
public static Task<(NetMQCertificate, AgentAuthToken)?> Load(string? agentKeyToken, string? agentKeyFilePath) {
if (agentKeyFilePath != null) {
return LoadFromFile(agentKeyFilePath);
}
else if (agentKeyToken != null) {
return Task.FromResult(LoadFromToken(agentKeyToken));
}
else {
throw new InvalidOperationException();
}
}
private static async Task<(NetMQCertificate, AgentAuthToken)?> LoadFromFile(string agentKeyFilePath) {
if (!File.Exists(agentKeyFilePath)) {
Logger.Fatal("Cannot load agent key, missing key file: {AgentKeyFilePath}", agentKeyFilePath);
Logger.Fatal("Missing agent key file: {AgentKeyFilePath}", agentKeyFilePath);
return null;
}
try {
Files.RequireMaximumFileSize(agentKeyFilePath, 64);
var (publicKey, agentToken) = AgentKeyData.FromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
var serverCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key.");
return (serverCertificate, agentToken);
} catch (Exception e) {
Logger.Fatal(e, "Error loading agent key from key file: {AgentKeyFilePath}", agentKeyFilePath);
return LoadFromBytes(await File.ReadAllBytesAsync(agentKeyFilePath));
} catch (IOException e) {
Logger.Fatal("Error loading agent key from file: {AgentKeyFilePath}", agentKeyFilePath);
Logger.Fatal(e.Message);
return null;
} catch (Exception) {
Logger.Fatal("File does not contain a valid agent key: {AgentKeyFilePath}", agentKeyFilePath);
return null;
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromToken(string agentKey) {
try {
return LoadFromBytes(TokenGenerator.DecodeBytes(agentKey));
} catch (Exception) {
Logger.Fatal("Invalid agent key: {AgentKey}", agentKey);
return null;
}
}
private static (NetMQCertificate, AgentAuthToken)? LoadFromBytes(byte[] agentKey) {
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
var serverCertificate = NetMQCertificate.FromPublicKey(publicKey);
Logger.Information("Loaded agent key.");
return (serverCertificate, agentToken);
}
}

View File

@ -21,10 +21,9 @@ try {
PhantomLogger.Root.InformationHeading("Initializing Phantom Panel agent...");
PhantomLogger.Root.Information("Agent version: {Version}", AssemblyAttributes.GetFullVersion(Assembly.GetExecutingAssembly()));
var (serverHost, serverPort, javaSearchPath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts) = Variables.LoadOrExit();
string agentKeyPath = Path.GetFullPath("./secrets/agent.key");
var agentKey = await AgentKey.LoadFromFile(agentKeyPath);
var (serverHost, serverPort, javaSearchPath, agentKeyToken, agentKeyFilePath, agentName, maxInstances, maxMemory, allowedServerPorts, allowedRconPorts) = Variables.LoadOrExit();
var agentKey = await AgentKey.Load(agentKeyToken, agentKeyFilePath);
if (agentKey == null) {
Environment.Exit(1);
}

View File

@ -9,6 +9,8 @@ sealed record Variables(
string ServerHost,
ushort ServerPort,
string JavaSearchPath,
string? AgentKeyToken,
string? AgentKeyFilePath,
string AgentName,
ushort MaxInstances,
RamAllocationUnits MaxMemory,
@ -16,12 +18,15 @@ sealed record Variables(
AllowedPorts AllowedRconPorts
) {
private static Variables LoadOrThrow() {
var (agentKeyToken, agentKeyFilePath) = EnvironmentVariables.GetEitherString("AGENT_KEY", "AGENT_KEY_FILE").Require;
var javaSearchPath = EnvironmentVariables.GetString("JAVA_SEARCH_PATH").WithDefaultGetter(GetDefaultJavaSearchPath);
return new Variables(
EnvironmentVariables.GetString("SERVER_HOST").Require,
EnvironmentVariables.GetPortNumber("SERVER_PORT").WithDefault(9401),
javaSearchPath,
agentKeyToken,
agentKeyFilePath,
EnvironmentVariables.GetString("AGENT_NAME").Require,
(ushort) EnvironmentVariables.GetInteger("MAX_INSTANCES", min: 1, max: 10000).Require,
EnvironmentVariables.GetString("MAX_MEMORY").MapParse(RamAllocationUnits.FromString).Require,

View File

@ -1,6 +1,7 @@
using NetMQ;
using Phantom.Common.Data.Agent;
using Phantom.Common.Logging;
using Phantom.Utils.Cryptography;
using Phantom.Utils.IO;
using Serilog;
@ -22,10 +23,13 @@ static class CertificateFiles {
if (secretKeyFileExists && agentKeyFileExists) {
try {
return await LoadCertificatesFromFiles(secretKeyFilePath, agentKeyFilePath);
} catch (Exception e) {
} catch (IOException e) {
Logger.Fatal("Error reading certificate files.");
Logger.Fatal(e.Message);
return null;
} catch (Exception) {
Logger.Fatal("Certificate files contain invalid data.");
return null;
}
}
@ -54,7 +58,7 @@ static class CertificateFiles {
var (publicKey, agentToken) = AgentKeyData.FromBytes(agentKey);
var certificate = new NetMQCertificate(secretKey, publicKey);
Logger.Information("Loaded existing certificate files. Agents will need {AgentKeyFilePath} to connect.", agentKeyFilePath);
LogAgentConnectionInfo("Loaded existing certificate files.", agentKeyFilePath, agentKey);
return (certificate, agentToken);
}
@ -66,11 +70,18 @@ static class CertificateFiles {
private static async Task<(NetMQCertificate, AgentAuthToken)> GenerateCertificateFiles(string secretKeyFilePath, string agentKeyFilePath) {
var certificate = new NetMQCertificate();
var agentToken = AgentAuthToken.Generate();
var agentKey = AgentKeyData.ToBytes(certificate.PublicKey, agentToken);
await Files.WriteBytesAsync(secretKeyFilePath, certificate.SecretKey, FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(agentKeyFilePath, AgentKeyData.ToBytes(certificate.PublicKey, agentToken), FileMode.Create, Chmod.URW_GR);
await Files.WriteBytesAsync(agentKeyFilePath, agentKey, FileMode.Create, Chmod.URW_GR);
Logger.Information("Certificates created. Agents will need {AgentKeyFilePath} to connect.", agentKeyFilePath);
LogAgentConnectionInfo("Created new certificate files.", agentKeyFilePath, agentKey);
return (certificate, agentToken);
}
private static void LogAgentConnectionInfo(string message, string agentKeyFilePath, byte[] agentKey) {
Logger.Information(message + " Agents will need the agent key to connect.");
Logger.Information("Agent key file: {AgentKeyFilePath}", agentKeyFilePath);
Logger.Information("Agent key: {AgentKey}", TokenGenerator.EncodeBytes(agentKey));
}
}

View File

@ -0,0 +1,102 @@
using System.Buffers.Binary;
using System.Text;
namespace Phantom.Utils.Cryptography;
// MIT License
//
// Copyright (c) 2020 Niklas Mollenhauer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// ---------------------------------------------------------------------
// This is a modified version of https://github.com/nikeee/dotnet-base24
// ---------------------------------------------------------------------
sealed class Base24 {
private readonly string alphabet;
private readonly uint alphabetLength;
private readonly Dictionary<char, uint> decodeMap;
public Base24(string alphabet) {
this.alphabet = alphabet;
this.alphabetLength = (uint) alphabet.Length;
this.decodeMap = new Dictionary<char, uint>(alphabet.Length);
for (int i = 0; i < alphabet.Length; ++i) {
this.decodeMap[alphabet[i]] = (uint) i;
}
}
public string Encode(ReadOnlySpan<byte> data) {
if (data.Length == 0) {
return string.Empty;
}
if (data.Length % 4 != 0) {
throw new ArgumentException("The data length must be multiple of 4 bytes (32 bits).");
}
var encodedDataLength = (data.Length / 4) * 7;
var result = new StringBuilder(encodedDataLength);
Span<char> subResult = stackalloc char[7];
for (int i = 0; i < data.Length; i += 4) {
uint value = BinaryPrimitives.ReadUInt32LittleEndian(data[i..]);
for (int k = 6; k >= 0; --k) {
uint idx = value % alphabetLength;
value /= alphabetLength;
subResult[k] = alphabet[(int) idx];
}
result.Append(subResult);
}
return result.ToString();
}
public byte[] Decode(ReadOnlySpan<char> data) {
if (data == null) {
throw new ArgumentNullException(nameof(data));
}
if (data.Length % 7 != 0) {
throw new ArgumentException("The data length must be multiple of 7 chars.");
}
var decodedDataLength = (data.Length / 7) * 4;
var result = new byte[decodedDataLength];
for (int i = 0; i < data.Length / 7; ++i) {
var subData = data.Slice(i * 7, 7);
uint value = 0;
foreach (char c in subData) {
value = (alphabetLength * value) + decodeMap[c];
}
var resultIndex = i * 4;
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(resultIndex), value);
}
return result;
}
}

View File

@ -1,12 +1,13 @@
using System.Security.Cryptography;
using System.Text;
namespace Phantom.Utils.Cryptography;
namespace Phantom.Utils.Cryptography;
public static class TokenGenerator {
private const string AllowedCharacters = "25679BCDFGHJKMNPQRSTWXYZ";
private static readonly HashSet<char> AllowedCharacterSet = new (AllowedCharacters);
private static readonly Base24 Base24 = new (AllowedCharacters);
public static string Create(int length) {
char[] result = new char[length];
@ -14,7 +15,7 @@ public static class TokenGenerator {
for (int i = 0; i < length; i++) {
result[i] = AllowedCharacters[RandomNumberGenerator.GetInt32(AllowedCharacters.Length)];
}
return new string(result);
}
@ -22,13 +23,21 @@ public static class TokenGenerator {
if (token.Length == 0) {
throw new ArgumentOutOfRangeException(nameof(token), "Invalid token (empty).");
}
foreach (char c in token) {
if (!AllowedCharacterSet.Contains(c)) {
throw new ArgumentOutOfRangeException(nameof(token), "Invalid token: " + token);
}
}
return Encoding.ASCII.GetBytes(token);
}
public static string EncodeBytes(byte[] bytes) {
return Base24.Encode(bytes);
}
public static byte[] DecodeBytes(string token) {
return Base24.Decode(token);
}
}