From d9632d3501cf8a4795d3d3ad3c7e07c55597fd79 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 16 Apr 2025 09:04:28 -0500 Subject: [PATCH] Hello world --- .dockerignore | 11 +++ .gitignore | 3 +- Dockerfile | 23 ++++++ Meshtastic.Mqtt.csproj | 30 +++++++ Meshtastic.Mqtt.sln | 24 ++++++ Program.cs | 178 +++++++++++++++++++++++++++++++++++++++++ README.md | 73 ++++++++++++++++- cert.pem | 31 +++++++ certificate.pfx | Bin 0 -> 4307 bytes key.pem | 54 +++++++++++++ 10 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Meshtastic.Mqtt.csproj create mode 100644 Meshtastic.Mqtt.sln create mode 100644 Program.cs create mode 100644 cert.pem create mode 100644 certificate.pfx create mode 100644 key.pem diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bf79e22 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +bin/ +obj/ +*.user +*.suo +*.log +.vs/ +.vscode/ +.git/ +.gitignore +Dockerfile +.dockerignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 35063fc..caed449 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ CodeCoverage/ # NUnit *.VisualState.xml TestResult.xml -nunit-*.xml \ No newline at end of file +nunit-*.xml +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..101e03d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +# Copy csproj and restore dependencies +COPY Meshtastic.Mqtt.csproj ./ +RUN dotnet restore + +# Copy the rest of the code +COPY . ./ +RUN dotnet publish -c Release -o /app + +# Build runtime image +FROM mcr.microsoft.com/dotnet/runtime:9.0 +WORKDIR /app +COPY --from=build /app ./ + +# Expose ports +EXPOSE 1883 8883 + +# Set environment variable to control SSL mode +# ENV SSL=true # Uncomment to enable SSL by default + +ENTRYPOINT ["dotnet", "Meshtastic.Mqtt.dll"] \ No newline at end of file diff --git a/Meshtastic.Mqtt.csproj b/Meshtastic.Mqtt.csproj new file mode 100644 index 0000000..f46a74b --- /dev/null +++ b/Meshtastic.Mqtt.csproj @@ -0,0 +1,30 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/Meshtastic.Mqtt.sln b/Meshtastic.Mqtt.sln new file mode 100644 index 0000000..ce050c9 --- /dev/null +++ b/Meshtastic.Mqtt.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Meshtastic.Mqtt", "Meshtastic.Mqtt.csproj", "{7467C293-5E7D-1ADF-451D-7E36BDAC9410}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7467C293-5E7D-1ADF-451D-7E36BDAC9410}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {62FE50C6-004E-4C4D-BE74-89E28A6A1064} + EndGlobalSection +EndGlobal diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c6af7ac --- /dev/null +++ b/Program.cs @@ -0,0 +1,178 @@ +using MQTTnet.Server; +using Meshtastic.Protobufs; +using Google.Protobuf; +using Serilog; +using MQTTnet.Protocol; +using System.Runtime.Loader; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog.Formatting.Compact; +using Meshtastic.Crypto; +using Meshtastic; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +using System.Reflection; + +var mqttFactory = new MqttServerFactory(); + +// #if SSL +var currentPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; +#pragma warning disable SYSLIB0057 // Type or member is obsolete +var certificate = new X509Certificate2(Path.Combine(currentPath, "certificate.pfx"), "large4cats", X509KeyStorageFlags.Exportable); +#pragma warning restore SYSLIB0057 // Type or member is obsolete + +var mqttServerOptions = new MqttServerOptionsBuilder() + .WithoutDefaultEndpoint() // This call disables the default unencrypted endpoint on port 1883 + .WithEncryptedEndpoint() + .WithEncryptedEndpointPort(8883) + .WithEncryptionCertificate(certificate.Export(X509ContentType.Pfx)) + .WithEncryptionSslProtocol(SslProtocols.Tls12) + .Build(); + Log.Logger.Information("Using SSL certificate for MQTT server"); + +// If you want to use a non-encrypted MQTT server, you can uncomment the following lines instead of the above +// var mqttServerOptions = new MqttServerOptionsBuilder() +// .WithDefaultEndpoint() +// .WithDefaultEndpointPort(1883) +// .Build(); +// Log.Logger.Information("Using unencrypted MQTT server"); +// #endif + +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(new RenderedCompactJsonFormatter()) + // .WriteTo.File(new RenderedCompactJsonFormatter(), "log.json", rollingInterval: RollingInterval.Hour) // File logging can be enabled if needed + .CreateLogger(); + +using var mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions); + +static Data? DecryptMeshPacket(ServiceEnvelope serviceEnvelope) +{ + var nonce = new NonceGenerator(serviceEnvelope.Packet.From, serviceEnvelope.Packet.Id).Create(); + + var decrypted = PacketEncryption.TransformPacket(serviceEnvelope.Packet.Encrypted.ToByteArray(), nonce, Resources.DEFAULT_PSK); + var payload = Data.Parser.ParseFrom(decrypted); + + if (payload.Portnum > PortNum.UnknownApp && payload.Payload.Length > 0) + return payload; + + // Was not able to decrypt the payload + return null; +} + +mqttServer.InterceptingPublishAsync += async (args) => +{ + try + { + if (args.ApplicationMessage.Payload.Length == 0) + { + Log.Logger.Warning("Received empty payload on topic {@Topic} from {@ClientId}", args.ApplicationMessage.Topic, args.ClientId); + args.ProcessPublish = false; // This will block empty packets + return; + } + var serviceEnvelope = ServiceEnvelope.Parser.ParseFrom(args.ApplicationMessage.Payload); + + // Block malformed service envelopes / packets + if ( + String.IsNullOrWhiteSpace(serviceEnvelope.ChannelId) || + String.IsNullOrWhiteSpace(serviceEnvelope.GatewayId) || + serviceEnvelope.Packet == null || + serviceEnvelope.Packet.Id < 1 || + serviceEnvelope.Packet.From < 1 || + serviceEnvelope.Packet.Encrypted == null || + serviceEnvelope.Packet.Encrypted.Length < 1 || + serviceEnvelope.Packet.Decoded != null) + { + Log.Logger.Warning("Service envelope or packet is malformed. Blocking packet on topic {@Topic} from {@ClientId}", args.ApplicationMessage.Topic, args.ClientId); + args.ProcessPublish = false; + return; + } + + var data = DecryptMeshPacket(serviceEnvelope); + // If we were not able to decrypt the packet, it is likely encrypted with an unknown PSK + // Uncomment the following lines if you want to block these packets + // if (data == null) + // { + // Log.Logger.Warning("Service envelope does not contain a valid packet. Blocking packet"); + // args.ProcessPublish = false; // This will block packets that are not valid protobuf packets + // return; + // } + + if (data?.Portnum == PortNum.TextMessageApp) + { + Log.Logger.Information("Received text message on topic {@Topic} from {@ClientId}: {@Message}", + args.ApplicationMessage.Topic, args.ClientId, data.Payload.ToStringUtf8()); + } + else + { + Log.Logger.Information("Received packet on topic {@Topic} from {@ClientId} with port number: {@Portnum}", + args.ApplicationMessage.Topic, args.ClientId, data?.Portnum); + } + + // Any further validation logic to block a packet can be added here + args.ProcessPublish = true; + } + catch (InvalidProtocolBufferException) + { + Log.Logger.Warning("Failed to decode presumed protobuf packet. Blocking"); + args.ProcessPublish = false; // This will block packets encrypted on unknown PSKs + } + catch (Exception ex) + { + Log.Logger.Error("Exception occured while attempting to decode packet on {@Topic} from {@ClientId}: {@Exception}", args.ApplicationMessage.Topic, args.ClientId, ex.Message); + args.ProcessPublish = false; // This will block packets that caused us to encounter an exception + } +}; + +mqttServer.InterceptingSubscriptionAsync += (args) => +{ + args.ProcessSubscription = true; // Subscription filtering logic can be added here to only allow certain topics + + return Task.CompletedTask; +}; + +mqttServer.ValidatingConnectionAsync += (args) => +{ + args.ReasonCode = true ? // Authentication logic can be added here + MqttConnectReasonCode.Success : MqttConnectReasonCode.BadUserNameOrPassword; + + // You can block connections based on client ID, username, ip, etc. + return Task.CompletedTask; +}; + +static IHostBuilder CreateHostBuilder(string[] args) +{ + return Host.CreateDefaultBuilder(args) + .UseConsoleLifetime() + .ConfigureServices((hostContext, services) => + { + services + .AddSingleton(Console.Out); + }); +} + + +using var host = CreateHostBuilder(args).Build(); +await host.StartAsync(); +var lifetime = host.Services.GetRequiredService(); +await mqttServer.StartAsync(); + +var ended = new ManualResetEventSlim(); +var starting = new ManualResetEventSlim(); + +AssemblyLoadContext.Default.Unloading += ctx => +{ + starting.Set(); + Log.Logger.Debug("Waiting for completion"); + ended.Wait(); +}; + +starting.Wait(); + +Log.Logger.Debug("Received signal gracefully shutting down"); +await mqttServer.StopAsync(); +Thread.Sleep(1000); +ended.Set(); + +lifetime.StopApplication(); +await host.WaitForShutdownAsync(); diff --git a/README.md b/README.md index 4d495b9..2d4548e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# mqtt -A meshtastic native packet aware MQTT broker +# Meshtastic MQTT Broker Boilerplate + +This project provides an MQTT broker boilerplate specifically designed for Meshtastic device networks. It handles encrypted mesh packets, validates messages, and can be configured to run with or without SSL. + +## Features + +- MQTT server implementation for Meshtastic devices +- Support for encrypted mesh packet handling and validation +- SSL support for secure MQTT connections +- Configurable logging with Serilog +- Packet filtering and validation logic + +## Docker Setup + +### Prerequisites + +- Docker installed on your system +- Certificate file (if using SSL mode) + +### Docker Installation + +1. Clone the repository: + ```bash + git clone https://github.com/meshtastic/mqtt + cd mqtt + ``` + +2. Build the Docker image: + ```bash + docker build -t meshtastic-mqtt-broker . + ``` + +#### SSL Mode (Port 8883) + +To run with SSL enabled: + +1. Place your certificate file (`certificate.pfx`) in the project directory. (see [MQTTnet Server Wiki](https://github.com/dotnet/MQTTnet/wiki/Server)) +2. Run the container with the SSL environment variable: + +```bash +docker run -p 8883:8883 -v $(pwd)/certificate.pfx:/app/certificate.pfx meshtastic-mqtt-broker +``` + +### Docker Compose Example + +```yaml +version: '3' +services: + mqtt-broker: + build: . + ports: + - "1883:1883" # Standard MQTT port + # - "8883:8883" # SSL port (uncomment if using SSL) + # environment: + # - SSL=true # Uncomment to enable SSL + # volumes: + # - ./certificate.pfx:/app/certificate.pfx # Mount certificate if using SSL + restart: unless-stopped +``` + +## Configuration Options + +- **SSL**: Set environment variable `SSL=true` to enable SSL mode +- **Certificate**: Mount your PFX certificate file to `/app/certificate.pfx` in the container +- **Ports**: The application uses port 1883 for standard MQTT and 8883 for SSL MQTT + +## Troubleshooting + +- Ensure proper network access to the Docker container +- Check that certificates are correctly formatted (for SSL mode) +- Review logs using `docker logs [container-id]` diff --git a/cert.pem b/cert.pem new file mode 100644 index 0000000..d0983bc --- /dev/null +++ b/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUAiOTlcvZA8d3i1YXISMcn+B4+PowDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA0MTYwMTQwNThaFw0yNjA0 +MTYwMTQwNThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQC5qV6+hqXx9g3dEAWZ9ZfBL8K5s2M1JvPakRu4MW9l +My1Ugf6mAB/7FkY168STGU3/8r0a+E743eTaPGWrlhBY1tEfCYwXLi2+xiL3yOB1 ++6uzla7+THOM3G3CN/Hoq0zw6IB2PJQF2ZT8rDLbmYdvv8i4hWSHjxNU/cEK2/aP +rMhKv8uWieEmstDCoF8AKxpFvh9JYhyeDTF5w/2cPEP+LcR36F8rVr9EbiieJ27U +GDvfgDe+Vdjm4YDVPVswf8/goN+TehjAOMQx1BHQLEFEij9KBmYdl6+mQ2mMnlwK +YjZJ36133QolH00x/yYGEak/DS21gECMBbTUw3Y6hs6wdWmV/cHRV7tIrBKGkMxG +0RSdqgukDfXRzi7kPO7TuMu5ju2ifS7JKhhNrh0IbuHFKSNjFYW8tTaXNHBuumKo +GUH51XjlONN43kTDzKMHkPznDImrKKL3PhRqFXUdgiwSoRNHY6/XHPAW6ecJ7Jla +hctnLQFjf4zsRkgTQjlj+fz9JPQTljhFQUkQyotnnp2eC23ceX/dnKG4hz350GkP +zL31vQ62f2BPOHacaLVCJrLBffOJjyAqfvR2J0oGPUo+7iuvjurC2pq9wgKaLLbQ +Y3ncHaBD3bLZk/KzrirX8Rn8cPbCs1Q1Ehe+F2mbarRjSsGuOlEqatns3gYeIZSd +SwIDAQABo1MwUTAdBgNVHQ4EFgQUczeKEnJEd8rhlPzTSDhNUircBvUwHwYDVR0j +BBgwFoAUczeKEnJEd8rhlPzTSDhNUircBvUwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAWTroM00DTyqsxj/SSAM9jx1XZXfEi5ebqCDawROmTdhF +ZnmoQ2wqtRU/B6en2GTah/RKOcAyby/Pgtd3X3/x+J1W1D2uH5wNlhCxlH8X+GbW +2Br4nRKlMxcg1TbHVowPybV5loAcGjiCuIsmk0Iq4M1WUn/Ex6SZENT6MxT3yYVS +neQ5NZhk6luUCpjxgY3XT2tclagM2i5YVVWT8kYQakmtRteBZmK4ZrFfc486/rNj +4hLX8kwhRriz6U2mCtNCLB1kMF5imDHnIBH2winOEmqnN2pEzC15PDFOvRMoJzIl +lXk3Odnv3BXIHosLJQheTsAMFrTDf82itVwoYFMx8QL/0jvYrVkdhzbZMJEcN6Bi +PeOcji3mqcOm1JKsYNPz9V/U0faoixOekG7y7PnW8cEsag9lrGPxr0rGJ9Y431O2 +GIV9D79WY25WWn23+R3VGZWrnOUY33UHl3ZngXnBSvyLMHP6ZSb/SuTM6hMPbqaB +uauhTqdXUc1P90hTToBz1wtbKwoYBYIM7sGZP8IFiFdCSaxX1Kckks6YELFxZWFN +1AWOG5GFT4xOGDgcBnbHcldejKEO6fRa+D0SSucyJ84EK1tvhKAMxPXnp73t4eE/ +gwnGZ/9oLm5RRjAFZeCFCjNgbA2bRTML8MqGYuaBSKMQX4cG2dSeJDH29vvj5tg= +-----END CERTIFICATE----- diff --git a/certificate.pfx b/certificate.pfx new file mode 100644 index 0000000000000000000000000000000000000000..5b9c42c1190d79a5efdf42c3b995579dcee09ab7 GIT binary patch literal 4307 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 literal 0 HcmV?d00001 diff --git a/key.pem b/key.pem new file mode 100644 index 0000000..6cb088a --- /dev/null +++ b/key.pem @@ -0,0 +1,54 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIJpDBWBgkqhkiG9w0BBQ0wSTAxBgkqhkiG9w0BBQwwJAQQTbPqA3Gt99nseU4E +K3aIQgICCAAwDAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIUQ6Xg6l7nOgEgglI +E5x+H+8syfRsEiHc1t7TqEjXt+muVm6LBmzt9B8Jp79wUMxAP1DF5wxsPkKXAErT +Y91lBFP2pt/JICY+mg47d0YtQqIyRsPe36GmN11ew4bG9OmHTlZW3YyAjwNtcdA5 +aCM4PmJGu+J70hWXHUslGGs9ixMFJC8341bWDIKjSRV/p1bY2isrvtB/Kf8Vz+wL +0Myfbmvkp3yXBAQpzVa4qa7I1Vx2TpaAI8/Vov6jVh9M8sLJwuCNqvzXIFBGHcJm +49dz6PAZ038pz8aVWjORZubtICYB7WwzD86XXpPls27CDZOVDgnUb64M22iQEZYd +A0tp14T89eMxm0Xa4I0mv75zn3cxkqfrlNy7pnuRdKMk9pL+xa4wFOeS81nOtY0c +JpC9N65BpV1Y9b7gGZUkxHbMP1MJ5oPHGpHRqGOG1BZ8IdFJ7CrB3gg9aA3Aw2lT +wBnJcO1cvg3qoDFlIbfmE2toTj3kHVA8YSrP0vapoGpYKe/3R56dwbkVXfW+t3TU +B/RCp5yfUR1lnNt5QAqyEX0unV60FnVOB9bfHW7YktXN2V4E7J3I1XDdLyUcfJdN +LGbL+mJ6NNK+fNQWeQdRSW0stCn8yCZ1/UcenhMftSuceZbnxqlqbEo4CEXckVEH +X3uzV4PrNaIiUzCLT6Q5VeRn/nj7L1VBcu0gYMH8JzaZ+RA7tnk80fxS3LrNBrfk +Ww106wn9zog4d/Po4ywgxqg6wxoAieOkK8D6stw7joRZ2EQW7c1W+lWGF5hMOlXU +0pyyfV5Vo7UcHZx2Zig5kCQftIZPaV2jWoV0aM5pC6junNM7Mr3Noga18TIKbsfR +9OYhunrsazIuNoFumj0Ap/GAP+o3rUm+tOTkhGbH6aLMiF+z8TN7zr5JlKbcI8pz +J2PrQYkxyoGiETzvMTtCzfwkL4gKXHShwoZJqvdDAgfBbGFnBMD3MvPBbv3qI3Wl +RaiecINULHsL74EX1JZ31g4FBRoGGk7uDHQP+o6r3/kq6hiJoidJCfnnTiEWaA+v +XOPmGyohqoWFfwaec84ncVtK/5nIB0WWoGLan8UgqcJ/aVVX3Osgxf0bxtEwfR2O +7mvcFCbai0qufwqSJfIavBB2QZYib2f8jzAx2J17WOaQtwD9KNktjnV9m9SpHRhf +imHB6Z88L3fwqprj/rOkrZNRs7RvOtYsj/Y05lpgeoEEoElgI9I6wD2SYb1gzHHB +MG1bwTZIe8Ni8AO/0YrG3afR0I+d2zQ2rhExd0o/5kSpWlQPXbja1h3QvJ32FqKH +0IKxKTIeiJbMJ//yrJhIo8+i2Q/oghl+4NsudyPaWPwzIhlll/1S9PhvoPPC1qFd +j2/DtDRmqEqrQZShWqBr12/6W6H925S3eP3sov0ngcvMVJ3E5fhIdctIHqWH3LiM +2eVedfwY+kwMT2Gc/avWERn4k4pTVD3ZW6wrfqd7gahq7Tt9ZAcORhDR7AMQVShu +XIuv3/n05pKQ9aFZnxTouOIPKTrnBbx0VZ4A9zQN6gYqmgwe1prxl/kOkNnGPAou +K87sL4DNTALsHUwrDnP4qzEnxwRxa3QHNdSP9tLwK8L2iNP1g9T0M2izxNB/Z/+E +UTP6hOFHXminPX9WNp+Xd5NWSGE/oqJnRC9SSztsdNQEHwiI/ER6Z43OXyE1j5Hf +uKACZTw2dEa0Cxu4A3l9GoITe6ITv4CoZS8dd68rUOYRjaSvjD5+alaPveUxR4SP +EiSdhA83KCQD2mKYzP8PyVgimkdOxgoTt2AFQMysrNvUY6lotYKXpz2gGrLz6utF +nU5e3Zv8R5m6x29p82DkwOZLT4Cec+dsHDs9AMmZp9EQ8jmEkRjGhrqdkPxKddiq +1JvWdNhvglvuoHK4hhPz9oAOYjpkFOBw/Z/cMaX36dxDkGMWRI7ivyUKL9NMJIc0 +NJoNOLUB2lUKsjr86GzwXeLJiwBHIVLR1WoueIV3FvAWkq0fMJYRzRgKF8g7iHhG +uMZvbnseyxpTmVpWMf3SFsHIk9cwZ9V9hPjwQCIhqcJt+tMzqycHNOwUnJzWbi77 +rzml05n5ssMFhi52e2dgJUldOT9KSx0gGHij8H8dfNPPtTBuGe9FvarFZqJphyS7 +VyNWe92ILKm+1tXe/xGysqPp+S3c6Bc66jRNjlVCzE+RK2S2M5ifTgPwWTfclgwJ +3iXcMVz6P1Mb+qlkzwkXnCMvsJ8jUVRVlFvlVTQJIzQrC0IHYOLy94TTw/hV8E4q +qGtpk4rnYY7gtAzqV6hglspi827D8o4ns5qnvqKcEhVaAu3JFU2T8/+P7OPhl+1i +wjmhvuc8Hlm0Q9VBo6coujRIhgXV6oVdcxrdIraDGUS8KMiUPTbc4dJcgsGp5sX7 +4fTjUSBrc2bzeDctIKZKJHg6wBmFo5D97QlUbNyey6rmrp1BuZxrbRCKZHnmk81R +G6HgJ9ZdrNbtBGzknNYB98dFrliMWSxMhvK+ieckFpfygpzg/6KGcVqbXEywWpJ8 +D173ZoPTNEb8rmFBBJhNenwW+BiU898F6UxBwT+vhGirNhKMydPS1bJUSgfxWkob +q9BTlakIUCrRaaryMRPZmHp3NEI9TFaMMZ3FTATZrmQ0j5si7H8c2cw3iPvbKOlj +rfCLJZMFKpEXoUpc7Lz35bEKhNV8/Feo30fyFMEE294hjjXEYKE6gWjlNhNfTobh +rCEfj2igU4A9W3S2Db14eK3T1xgJRvsCWWstIa6ELefblek2Y2qP9HXXo2R2Zf8J ++DNj9HBPvvX/FW5NJSU8uEU/ghJIYngwMgTXdAh89rWASqhXXSDJFEmI+fnaZumJ +Aspc1dcOlcrs4vyHZ0AOzRtgPYkhAQ5AF/eubDJzA2uIqMZ3JDNqnj3b5QgW5ws6 +3h1oychjsFgIZBFL4VI086eGWIBIbqXPXQ0U6l944PVQQKETFJ+bGBVDnxjb7KEo +7FF/u+KjrNUkhoLRaiFdo/2vraNar4umtykLS6AsRJn5H66v7dhyDgw88jYFM9Lt +SSqgO5o9WYvf/rwXWusTfuZn32DpTBIJtrwLhhAZs/dv44GVj3H3H85u/h6hr+gw +sw9vs7OUOkC2+PfJg0qeynMT9q7V+sV0CcSqj4SLSaF6ogfSNp0af977gQeFL86X +rH1xuKTR/w3KS06Rjacob21XXWL8FRkS +-----END ENCRYPTED PRIVATE KEY-----