Skip to content

Commit c2168bc

Browse files
MariusStorhaugvercelloneVercellone, Jason
authored
🌟 [Major]: Add Sodium encryption and decryption functions (#12)
## Description This pull request introduces the `Sodium` PowerShell module, providing cryptographic functionality using `Sodium.Core` and `libsodium`. It enables encryption and decryption of secrets, ensuring compatibility with GitHub's security standards and PowerShell across different platforms. - Fixes #9 ## Changes Overview This PR initiates the `Sodium` project, starting with the following features: - Exported functions, the main deliverable of the PR: - `New-SodiumKeyPair`: Generates a new public/private key pair. - `ConvertTo-SodiumEncryptedString`: Encrypts a message with a public key. - `ConvertFrom-SodiumEncryptedString`: Decrypts a sealed message with a private key. - Dev note: - These were created so we can reuse the PSModule framework as it does not yet support binary modules. With this we get management of the module build process as well as auto-generation of docs, published to GitHub Pages - A nested binary PowerShell module `PSModule.Sodium`: - `New-PublicKeyBoxKeyPair`: Generates a new public/private key pair. - `New-SealedPublicKeyBox`: Encrypts a message with a public key. - `Open-SealedPublicKeyBox`: Decrypts a sealed message with a private key. - `PSModule.Sodium.Isolated` class to create an [Isolated Assembly Load Context](https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts?view=powershell-7.4#loading-through-net-core-assembly-load-contexts) that targets `.NET 8.0`, and handle references to `Sodium.Core`. - `PublicKeyBoxHelper` class to expose the `GenerateKeyPair` method. - `SealedPublicKeyBoxHelper` classes to expose the `SealedPublicKeyBox.Create` and `SealedPublicKeyBox.Open` methods. - Added `build.ps1` script to automate building and publishing the module. - Introduced `clean.ps1` script to clean up build artifacts. ### Special thanks Thank you @vercellone for the contribution and collaboration on this! ## Type of Change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [x] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas --------- Co-authored-by: Jason Vercellone <vercellone@users.noreply.github.com> Co-authored-by: Vercellone, Jason <Jason.Vercellone@mkcorp.com>
1 parent 0660116 commit c2168bc

34 files changed

Lines changed: 1368 additions & 55 deletions

.github/workflows/Linter.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ jobs:
2929
GITHUB_TOKEN: ${{ github.token }}
3030
VALIDATE_MARKDOWN_PRETTIER: false
3131
VALIDATE_YAML_PRETTIER: false
32+
VALIDATE_JSON_PRETTIER: false

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@
1111

1212
# PSModule framework outputs folder
1313
outputs/*
14+
15+
# .Net build output
16+
bin/
17+
obj/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Sodium.Core" Version="1.4.0-preview.2" />
9+
</ItemGroup>
10+
11+
</Project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Sodium;
2+
3+
namespace PSModule.Sodium.Isolated
4+
{
5+
public static class PublicKeyBoxHelper
6+
{
7+
public static (byte[] PublicKey, byte[] PrivateKey) GenerateKeyPair()
8+
{
9+
var keyPair = PublicKeyBox.GenerateKeyPair();
10+
return (keyPair.PublicKey, keyPair.PrivateKey);
11+
}
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using PSModule.Sodium.Isolated;
2+
using Sodium;
3+
4+
namespace PSModule.Sodium.Isolated
5+
{
6+
public class SealedPublicKeyBoxHelper
7+
{
8+
public static byte[] Create(byte[] secret, byte[] publicKey)
9+
{
10+
return SealedPublicKeyBox.Create(secret, publicKey);
11+
}
12+
13+
public static byte[] Open(byte[] secret, byte[] privateKey, byte[] publicKey)
14+
{
15+
return SealedPublicKeyBox.Open(secret, privateKey, publicKey);
16+
}
17+
}
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using System.Text;
3+
using System.Management.Automation;
4+
using System.Security;
5+
using PSModule.Sodium.Isolated;
6+
7+
namespace PSModule.Sodium
8+
{
9+
[Cmdlet(VerbsCommon.New, "PublicKeyBoxKeyPair")]
10+
[OutputType(typeof(string))]
11+
public class NewPublicKeyBoxKeyPairCommand : PSCmdlet
12+
{
13+
protected override void ProcessRecord()
14+
{
15+
(byte[] publicKey, byte[] privateKey) = PublicKeyBoxHelper.GenerateKeyPair();
16+
var publicKeyString = Convert.ToBase64String(publicKey);
17+
var privateKeyString = Convert.ToBase64String(privateKey);
18+
WriteObject(new { PublicKey = publicKeyString, PrivateKey = privateKeyString });
19+
}
20+
}
21+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Text;
3+
using System.Management.Automation;
4+
using System.Security;
5+
using PSModule.Sodium.Isolated;
6+
7+
namespace PSModule.Sodium
8+
{
9+
[Cmdlet(VerbsCommon.New, "SealedPublicKeyBox")]
10+
[OutputType(typeof(string))]
11+
public class NewSealedPublicKeyBoxCommand : PSCmdlet
12+
{
13+
[Parameter(Mandatory = true)]
14+
public string Secret { get; set; }
15+
16+
[Parameter(Mandatory = true)]
17+
public string PublicKey { get; set; }
18+
19+
protected override void ProcessRecord()
20+
{
21+
var encryptedString = Convert.ToBase64String(
22+
SealedPublicKeyBoxHelper.Create(
23+
Encoding.UTF8.GetBytes(Secret),
24+
Convert.FromBase64String(PublicKey)
25+
)
26+
);
27+
WriteObject(encryptedString);
28+
}
29+
}
30+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Text;
3+
using System.Management.Automation;
4+
using System.Security;
5+
using PSModule.Sodium.Isolated;
6+
7+
namespace PSModule.Sodium
8+
{
9+
[Cmdlet(VerbsCommon.Open, "SealedPublicKeyBox")]
10+
[OutputType(typeof(string))]
11+
public class OpenSealedPublicKeyBoxCommand : PSCmdlet
12+
{
13+
[Parameter(Mandatory = true)]
14+
public string EncryptedSecret { get; set; }
15+
16+
[Parameter(Mandatory = true)]
17+
public string PublicKey { get; set; }
18+
19+
[Parameter(Mandatory = true)]
20+
public string PrivateKey { get; set; }
21+
22+
protected override void ProcessRecord()
23+
{
24+
var decryptedString = Encoding.UTF8.GetString(
25+
SealedPublicKeyBoxHelper.Open(
26+
Convert.FromBase64String(EncryptedSecret),
27+
Convert.FromBase64String(PrivateKey),
28+
Convert.FromBase64String(PublicKey)
29+
)
30+
);
31+
WriteObject(decryptedString);
32+
}
33+
}
34+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Reflection;
6+
using System.Runtime.InteropServices;
7+
using System.Runtime.Loader;
8+
9+
namespace PSModule.Sodium
10+
{
11+
public class IsolatedAssemblyLoadContext : AssemblyLoadContext
12+
{
13+
private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
14+
15+
private static readonly ConcurrentDictionary<string, IsolatedAssemblyLoadContext> s_isolatedLoadContexts = new ConcurrentDictionary<string, IsolatedAssemblyLoadContext>();
16+
17+
internal static IsolatedAssemblyLoadContext GetForDirectory(string directoryPath)
18+
{
19+
return s_isolatedLoadContexts.GetOrAdd(directoryPath, (path) => new IsolatedAssemblyLoadContext(path));
20+
}
21+
22+
private readonly string _isolatedDirPath;
23+
24+
public IsolatedAssemblyLoadContext(string isolatedDirPath) : base(nameof(IsolatedAssemblyLoadContext))
25+
{
26+
_isolatedDirPath = isolatedDirPath;
27+
}
28+
29+
protected override Assembly Load(AssemblyName assemblyName)
30+
{
31+
string assemblyFileName = $"{assemblyName.Name}.dll";
32+
33+
// Make sure we allow other common PowerShell dependencies to be loaded by PowerShell
34+
// But specifically exclude Sodium.Core since we want to use our isolated version here
35+
if (!assemblyName.Name.Equals("Sodium.Core", StringComparison.OrdinalIgnoreCase))
36+
{
37+
string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName);
38+
if (File.Exists(psHomeAsmPath))
39+
{
40+
// With this API, returning null means nothing is loaded
41+
return null;
42+
}
43+
}
44+
45+
// Now try to load the assembly from the isolated directory
46+
string isolatedAsmPath = Path.Join(_isolatedDirPath, assemblyFileName);
47+
if (File.Exists(isolatedAsmPath))
48+
{
49+
WriteDebugMessage($"PSModule.Sodium.IsolatedAssemblyLoadContext is attempting to load Mmanaged DLL from path: {isolatedAsmPath}");
50+
return LoadFromAssemblyPath(isolatedAsmPath);
51+
}
52+
53+
// else return null so that the default handler will resolve it
54+
return null;
55+
}
56+
57+
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
58+
{
59+
string platformFolder = GetPlatformFolder();
60+
string extension = GetPlatformExtension();
61+
string dllPath = Path.Combine(_isolatedDirPath, "runtimes", platformFolder, "native", unmanagedDllName + extension);
62+
63+
if (dllPath != null)
64+
{
65+
if (File.Exists(dllPath))
66+
{
67+
WriteDebugMessage($"PSModule.Sodium.IsolatedAssemblyLoadContext is attempting to load UNManaged DLL from path: {dllPath}");
68+
return LoadUnmanagedDllFromPath(dllPath);
69+
}
70+
}
71+
72+
return IntPtr.Zero;
73+
}
74+
75+
private static string GetPlatformFolder()
76+
{
77+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
78+
return System.Environment.Is64BitProcess ? "win-x64" : "win-x86";
79+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
80+
return $"linux-{RuntimeInformation.OSArchitecture}".ToLower();
81+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
82+
return $"osx-{RuntimeInformation.OSArchitecture}".ToLower();
83+
throw new PlatformNotSupportedException("Unsupported platform");
84+
}
85+
86+
private static string GetPlatformExtension()
87+
{
88+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
89+
return ".dll";
90+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
91+
return ".so";
92+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
93+
return ".dylib";
94+
throw new PlatformNotSupportedException("Unsupported platform");
95+
}
96+
97+
/// <summary>
98+
/// Write debug message to console if SYSTEM_DEBUG or ACTIONS_STEP_DEBUG
99+
/// </summary>
100+
/// <param name="message"></param>
101+
private static void WriteDebugMessage(string message)
102+
{
103+
// SYSTEM_DEBUG is applicable to Azure DevOps pipelines
104+
// ACTIONS_STEP_DEBUG is applicable to GitHub step debug logging
105+
if (System.Environment.GetEnvironmentVariable("SYSTEM_DEBUG") == "True" || System.Environment.GetEnvironmentVariable("ACTIONS_STEP_DEBUG") == "true")
106+
Console.WriteLine(message);
107+
}
108+
}
109+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using System;
2+
using System.IO;
3+
using System.Management.Automation;
4+
using System.Reflection;
5+
using System.Runtime.Loader;
6+
7+
namespace PSModule.Sodium
8+
{
9+
public class ModuleInitializer : IModuleAssemblyInitializer
10+
{
11+
private static string s_binBasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
12+
private static string s_binIsolatedPath = Path.Combine(s_binBasePath, "isolated");
13+
14+
public void OnImport()
15+
{
16+
WriteDebugMessage("PSModule.Sodium.ModuleInitializer OnImport called.");
17+
WriteDebugMessage($"s_binBasePath: {s_binBasePath}");
18+
WriteDebugMessage($"s_binIsolatedPath: {s_binIsolatedPath}");
19+
AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore;
20+
}
21+
22+
private static Assembly ResolveAssembly_NetCore(
23+
AssemblyLoadContext assemblyLoadContext,
24+
AssemblyName assemblyName)
25+
{
26+
try
27+
{
28+
WriteDebugMessage($"PSModule.Sodium.ModuleInitializer ResolveAssembly_NetCore called for: {assemblyName}");
29+
30+
// In .NET Core, PowerShell deals with assembly probing so our logic is much simpler
31+
// We only care about Sodium.Core and our .Isolated assembly
32+
if (!assemblyName.Name.Equals("Sodium.Core") && !assemblyName.Name.EndsWith(".Isolated"))
33+
{
34+
return null;
35+
}
36+
37+
// Load Isolated assemblies through the isolated ALC, and let it resolve further dependencies automatically
38+
WriteDebugMessage($"PSModule.Sodium.ModuleInitializer is attempting to load Managed DLL: {assemblyName}");
39+
var isolatedAssemblyLoadContext = IsolatedAssemblyLoadContext.GetForDirectory(s_binIsolatedPath);
40+
WriteDebugMessage($"PSModule.Sodium.IsolatedAssemblyLoadContext created for directory: {s_binIsolatedPath}");
41+
var assembly = isolatedAssemblyLoadContext.LoadFromAssemblyName(assemblyName);
42+
WriteDebugMessage($"Assembly loaded: {assembly?.FullName ?? "null"}");
43+
return assembly;
44+
}
45+
catch (Exception ex)
46+
{
47+
WriteDebugMessage($"Exception in ResolveAssembly_NetCore: {ex.Message}");
48+
WriteDebugMessage(ex.StackTrace);
49+
throw;
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Write debug message to console if SYSTEM_DEBUG is set to true
55+
/// </summary>
56+
/// <param name="message"></param>
57+
private static void WriteDebugMessage(string message)
58+
{
59+
if (System.Environment.GetEnvironmentVariable("SYSTEM_DEBUG") == "True")
60+
Console.WriteLine(message);
61+
}
62+
}
63+
}

0 commit comments

Comments
 (0)