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:
parent
de22e5695f
commit
ff5d31bf05
.run
.workdir
Agent/Phantom.Agent
Server/Phantom.Server
Utils/Phantom.Utils.Cryptography
@ -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" />
|
||||
|
@ -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" />
|
||||
|
@ -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
3
.workdir/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/Agent*/data
|
||||
/Agent*/temp
|
||||
/Server/keys
|
2
.workdir/Agent1/.gitignore
vendored
Normal file
2
.workdir/Agent1/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
Binary file not shown.
2
.workdir/Agent2/.gitignore
vendored
Normal file
2
.workdir/Agent2/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
Binary file not shown.
2
.workdir/Agent3/.gitignore
vendored
Normal file
2
.workdir/Agent3/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
Binary file not shown.
1
.workdir/Server/.gitignore
vendored
Normal file
1
.workdir/Server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/keys
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
102
Utils/Phantom.Utils.Cryptography/Base24.cs
Normal file
102
Utils/Phantom.Utils.Cryptography/Base24.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user