zcbot/windows-node/Zcbot.WindowsNode/NodeConfigStore.cs

133 lines
5.4 KiB
C#

using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Text.Json;
namespace Zcbot.WindowsNode;
internal sealed class NodeConfigStore(NodePaths paths)
{
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("zcbot.windows-node.v1");
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
internal bool Exists => File.Exists(paths.ConfigPath);
internal string ConfigPath => paths.ConfigPath;
internal void Save(NodeConfig config)
{
Directory.CreateDirectory(paths.RootDirectory);
RestrictDirectory(paths.RootDirectory);
var protectedToken = ProtectedData.Protect(
Encoding.UTF8.GetBytes(config.NodeToken), Entropy, DataProtectionScope.LocalMachine);
var stored = new StoredNodeConfig(
config.ServerUrl.AbsoluteUri,
config.NodeId,
config.InstallId,
config.NodeName,
Convert.ToBase64String(protectedToken),
config.HeartbeatSeconds,
config.Capabilities);
var temporaryPath = paths.ConfigPath + ".tmp";
File.WriteAllText(temporaryPath, JsonSerializer.Serialize(stored, JsonOptions), Encoding.UTF8);
RestrictFile(temporaryPath);
File.Move(temporaryPath, paths.ConfigPath, overwrite: false);
RestrictFile(paths.ConfigPath);
}
internal NodeConfig Load()
{
if (!Exists)
{
throw new NodeConfigurationException(
"Node is not registered. Run the enroll command first.");
}
try
{
var stored = JsonSerializer.Deserialize<StoredNodeConfig>(
File.ReadAllText(paths.ConfigPath, Encoding.UTF8))
?? throw new NodeConfigurationException("Node configuration is empty.");
var token = Encoding.UTF8.GetString(ProtectedData.Unprotect(
Convert.FromBase64String(stored.ProtectedNodeToken),
Entropy,
DataProtectionScope.LocalMachine));
if (stored.NodeId == Guid.Empty
|| stored.InstallId == Guid.Empty
|| string.IsNullOrWhiteSpace(stored.NodeName)
|| string.IsNullOrWhiteSpace(token)
|| stored.Capabilities.Count == 0
|| stored.Capabilities.Any(item => item != "origin.plot@v1"))
{
throw new NodeConfigurationException("Node configuration contains an invalid identity or capability.");
}
return new NodeConfig(
NodeUri.NormalizeServerUrl(stored.ServerUrl),
stored.NodeId,
stored.InstallId,
stored.NodeName,
token,
Math.Clamp(stored.HeartbeatSeconds, 5, 300),
stored.Capabilities);
}
catch (NodeConfigurationException)
{
throw;
}
catch (Exception exception) when (
exception is CryptographicException
or FormatException
or IOException
or JsonException
or UnauthorizedAccessException)
{
throw new NodeConfigurationException(
$"Node configuration cannot be loaded: {exception.Message}");
}
}
internal void DeleteLocalIdentity()
{
if (File.Exists(paths.ConfigPath))
{
File.Delete(paths.ConfigPath);
}
var temporaryPath = paths.ConfigPath + ".tmp";
if (File.Exists(temporaryPath))
{
File.Delete(temporaryPath);
}
}
private static void RestrictDirectory(string path)
{
var identity = WindowsIdentity.GetCurrent();
var user = identity.User
?? throw new NodeConfigurationException("Current Windows account has no security identifier.");
var security = new DirectorySecurity();
security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
security.AddAccessRule(new FileSystemAccessRule(
user, FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
PropagationFlags.None, AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
PropagationFlags.None, AccessControlType.Allow));
new DirectoryInfo(path).SetAccessControl(security);
}
private static void RestrictFile(string path)
{
var identity = WindowsIdentity.GetCurrent();
var user = identity.User
?? throw new NodeConfigurationException("Current Windows account has no security identifier.");
var security = new FileSecurity();
security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
security.AddAccessRule(new FileSystemAccessRule(user, FileSystemRights.FullControl, AccessControlType.Allow));
security.AddAccessRule(new FileSystemAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
FileSystemRights.FullControl, AccessControlType.Allow));
new FileInfo(path).SetAccessControl(security);
}
}