Powershell/Private/SystemContext/Invoke-SystemContextAPI.ps1
Function Invoke-SystemContextAPI { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] [validateSet('GET', 'POST', 'PUT', 'DELETE')] $method, [Parameter(Mandatory = $true)] [string] [validateSet('systems/memberof', 'systems', 'systems/associations', 'systems/users', 'systemgroups/members')] $endpoint, [Parameter(Mandatory = $false)] [Parameter(ParameterSetName = "association")] [string] [validateSet('add', 'remove', 'update')] $op, [Parameter(Mandatory = $false)] [Parameter(ParameterSetName = "association")] [string] [validateSet('user', 'systemgroup')] $type, [Parameter(Mandatory = $false)] [Parameter(ParameterSetName = "association")] [bool] $admin, [Parameter(Mandatory = $false)] [Parameter(ParameterSetName = "association")] [string] $id ) begin { try { $config = get-content 'C:\Program Files\JumpCloud\Plugins\Contrib\jcagent.conf' $regex = 'systemKey\":\"(\w+)\"' $systemKey = [regex]::Match($config, $regex).Groups[1].Value } catch { throw "Could not get systemKey from jcagent.conf" } # TODO: for pwsh 5.1 we need to load the library for PWSH 7+ we can use the native RSA # Referenced Library for RSA Switch ($PSVersionTable.PSVersion.Major) { '5' { # https://github.com/wing328/PSPetstore/blob/87a2c455a7c62edcfc927ff5bf4955b287ef483b/src/PSOpenAPITools/Private/RSAEncryptionProvider.cs Add-Type -typedef @" using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Text; namespace RSAEncryption { public class RSAEncryptionProvider { public static RSACryptoServiceProvider GetRSAProviderFromPemFile(String pemfile, SecureString keyPassPharse = null) { const String pempubheader = "-----BEGIN PUBLIC KEY-----"; const String pempubfooter = "-----END PUBLIC KEY-----"; bool isPrivateKeyFile = true; byte[] pemkey = null; if (!File.Exists(pemfile)) { throw new Exception("private key file does not exist."); } string pemstr = File.ReadAllText(pemfile).Trim(); if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter)) { isPrivateKeyFile = false; } if (isPrivateKeyFile) { pemkey = ConvertPrivateKeyToBytes(pemstr, keyPassPharse); if (pemkey == null) { return null; } return DecodeRSAPrivateKey(pemkey); } return null ; } static byte[] ConvertPrivateKeyToBytes(String instr, SecureString keyPassPharse = null) { const String pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; const String pemprivfooter = "-----END RSA PRIVATE KEY-----"; String pemstr = instr.Trim(); byte[] binkey; if (!pemstr.StartsWith(pemprivheader) || !pemstr.EndsWith(pemprivfooter)) { return null; } StringBuilder sb = new StringBuilder(pemstr); sb.Replace(pemprivheader, ""); sb.Replace(pemprivfooter, ""); String pvkstr = sb.ToString().Trim(); try { // if there are no PEM encryption info lines, this is an UNencrypted PEM private key binkey = Convert.FromBase64String(pvkstr); return binkey; } catch (System.FormatException) { StringReader str = new StringReader(pvkstr); //-------- read PEM encryption info. lines and extract salt ----- if (!str.ReadLine().StartsWith("Proc-Type: 4,ENCRYPTED")) return null; String saltline = str.ReadLine(); if (!saltline.StartsWith("DEK-Info: DES-EDE3-CBC,")) return null; String saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim(); byte[] salt = new byte[saltstr.Length / 2]; for (int i = 0; i < salt.Length; i++) salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16); if (!(str.ReadLine() == "")) return null; //------ remaining b64 data is encrypted RSA key ---- String encryptedstr = str.ReadToEnd(); try { //should have b64 encrypted RSA key now binkey = Convert.FromBase64String(encryptedstr); } catch (System.FormatException) { //data is not in base64 fromat return null; } byte[] deskey = GetEncryptedKey(salt, keyPassPharse, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes if (deskey == null) return null; //------ Decrypt the encrypted 3des-encrypted RSA private key ------ byte[] rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV return rsakey; } } public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey) { byte[] MODULUS, E, D, P, Q, DP, DQ, IQ; // --------- Set up stream to decode the asn.1 encoded RSA private key ------ MemoryStream mem = new MemoryStream(privkey); BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading byte bt = 0; ushort twobytes = 0; int elems = 0; try { twobytes = binr.ReadUInt16(); if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) binr.ReadByte(); //advance 1 byte else if (twobytes == 0x8230) binr.ReadInt16(); //advance 2 bytes else return null; twobytes = binr.ReadUInt16(); if (twobytes != 0x0102) //version number return null; bt = binr.ReadByte(); if (bt != 0x00) return null; //------ all private key components are Integer sequences ---- elems = GetIntegerSize(binr); MODULUS = binr.ReadBytes(elems); elems = GetIntegerSize(binr); E = binr.ReadBytes(elems); elems = GetIntegerSize(binr); D = binr.ReadBytes(elems); elems = GetIntegerSize(binr); P = binr.ReadBytes(elems); elems = GetIntegerSize(binr); Q = binr.ReadBytes(elems); elems = GetIntegerSize(binr); DP = binr.ReadBytes(elems); elems = GetIntegerSize(binr); DQ = binr.ReadBytes(elems); elems = GetIntegerSize(binr); IQ = binr.ReadBytes(elems); // ------- create RSACryptoServiceProvider instance and initialize with public key ----- RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); RSAParameters RSAparams = new RSAParameters(); RSAparams.Modulus = MODULUS; RSAparams.Exponent = E; RSAparams.D = D; RSAparams.P = P; RSAparams.Q = Q; RSAparams.DP = DP; RSAparams.DQ = DQ; RSAparams.InverseQ = IQ; RSA.ImportParameters(RSAparams); return RSA; } catch (Exception) { return null; } finally { binr.Close(); } } private static int GetIntegerSize(BinaryReader binr) { byte bt = 0; byte lowbyte = 0x00; byte highbyte = 0x00; int count = 0; bt = binr.ReadByte(); if (bt != 0x02) //expect integer return 0; bt = binr.ReadByte(); if (bt == 0x81) count = binr.ReadByte(); // data size in next byte else if (bt == 0x82) { highbyte = binr.ReadByte(); // data size in next 2 bytes lowbyte = binr.ReadByte(); byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; count = BitConverter.ToInt32(modint, 0); } else { count = bt; // we already have the data size } while (binr.ReadByte() == 0x00) { //remove high order zeros in data count -= 1; } binr.BaseStream.Seek(-1, SeekOrigin.Current); //last ReadByte wasn't a removed zero, so back up a byte return count; } static byte[] GetEncryptedKey(byte[] salt, SecureString secpswd, int count, int miter) { IntPtr unmanagedPswd = IntPtr.Zero; int HASHLENGTH = 16; //MD5 bytes byte[] keymaterial = new byte[HASHLENGTH * miter]; //to store contatenated Mi hashed results byte[] psbytes = new byte[secpswd.Length]; unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd); Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length); Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd); // --- contatenate salt and pswd bytes into fixed data array --- byte[] data00 = new byte[psbytes.Length + salt.Length]; Array.Copy(psbytes, data00, psbytes.Length); //copy the pswd bytes Array.Copy(salt, 0, data00, psbytes.Length, salt.Length); //concatenate the salt bytes // ---- do multi-hashing and contatenate results D1, D2 ... into keymaterial bytes ---- MD5 md5 = new MD5CryptoServiceProvider(); byte[] result = null; byte[] hashtarget = new byte[HASHLENGTH + data00.Length]; //fixed length initial hashtarget for (int j = 0; j < miter; j++) { // ---- Now hash consecutively for count times ------ if (j == 0) result = data00; //initialize else { Array.Copy(result, hashtarget, result.Length); Array.Copy(data00, 0, hashtarget, result.Length, data00.Length); result = hashtarget; } for (int i = 0; i < count; i++) result = md5.ComputeHash(result); Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //contatenate to keymaterial } byte[] deskey = new byte[24]; Array.Copy(keymaterial, deskey, deskey.Length); Array.Clear(psbytes, 0, psbytes.Length); Array.Clear(data00, 0, data00.Length); Array.Clear(result, 0, result.Length); Array.Clear(hashtarget, 0, hashtarget.Length); Array.Clear(keymaterial, 0, keymaterial.Length); return deskey; } static byte[] DecryptKey(byte[] cipherData, byte[] desKey, byte[] IV) { MemoryStream memst = new MemoryStream(); TripleDES alg = TripleDES.Create(); alg.Key = desKey; alg.IV = IV; try { CryptoStream cs = new CryptoStream(memst, alg.CreateDecryptor(), CryptoStreamMode.Write); cs.Write(cipherData, 0, cipherData.Length); cs.Close(); } catch (Exception){ return null; } byte[] decryptedData = memst.ToArray(); return decryptedData; } } } "@ } Default { Write-Verbose "PowerShell version: $($PSVersionTable.PSVersion)" } } # Validate the method and endpoint combination switch ($endpoint) { "systems" { if ($method -notin @("GET", "PUT", "DELETE")) { throw "Invalid method '$method' for endpoint '$endpoint'. Valid methods are: GET, PUT, DELETE." } else { $requestURL = "/api/systems/$systemKey" } } "systems/memberof" { if ($method -ne "GET") { throw "Invalid method '$method' for endpoint '$endpoint'. The only valid method is: GET." } else { $requestURL = "/api/v2/systems/$systemKey/memberof" } } "systems/associations" { if ($method -notin "GET", "POST") { throw "Invalid method '$method' for endpoint '$endpoint'. The only valid method is: GET." } else { $requestURL = "/api/v2/systems/$systemKey/associations?targets=user" } } "systems/users" { if ($method -ne "GET") { throw "Invalid method '$method' for endpoint '$endpoint'. The only valid method is: GET." } else { $requestURL = "/api/v2/systems/$systemKey/users" } } "systemgroups/members" { if ($method -ne "POST") { throw "Invalid method '$method' for endpoint '$endpoint'. The only valid method is: POST." } else { $requestURL = "/api/v2/systemgroups/$systemKey/members" } } default { throw "Invalid endpoint '$endpoint'." } } if ($PSCmdlet.ParameterSetName -eq 'association') { switch ($endpoint) { "systems/associations" { If ($method -eq 'POST') { $form = @{ "id" = "$id" "type" = "$type" "op" = "$op" "attributes" = @{ "sudo" = @{ "enabled" = $admin "withoutPassword" = $false } } } | ConvertTo-Json -Depth 10 } else { if ($id -or $admin -or $type -or $op) { throw "The parameters 'id,', 'admin', 'type', and 'op' can only be used with the endpoint 'systems/associations' and method 'POST'." } } } "systemgroups/members" { If ($method -eq 'POST') { $form = @{ "id" = "$id" "type" = "$type" "op" = "$op" } | ConvertTo-Json -Depth 10 } else { if ($id -or $type -or $op) { throw "The parameters 'id', 'type', and 'op' can only be used with the endpoint 'systemgroups/members' and method 'POST'." } } } Default {} } } } process { # Format and create the signature request $now = (Get-Date -Date ((Get-Date).ToUniversalTime()) -UFormat "+%a, %d %h %Y %H:%M:%S GMT") # create the string to sign from the request-line and the date $signstr = "$method $requestURL HTTP/1.1`ndate: $now" $enc = [system.Text.Encoding]::UTF8 $data = $enc.GetBytes($signstr) # Create a New SHA256 Crypto Provider $sha = New-Object System.Security.Cryptography.SHA256CryptoServiceProvider # Now hash and display results $result = $sha.ComputeHash($data) # Private Key Path $PrivateKeyFilePath = 'C:\Program Files\JumpCloud\Plugins\Contrib\client.key' # set the Hash Algorithm $hashAlgo = [System.Security.Cryptography.HashAlgorithmName]::SHA256 # depending on the powershell version 5 or 7 we need to load the RSA provider switch ($PSVersionTable.PSVersion.Major) { '5' { # Load the RSA Encryption Provider [System.Security.Cryptography.RSA]$rsa = [RSAEncryption.RSAEncryptionProvider]::GetRSAProviderFromPemFile($PrivateKeyFilePath) } Default { # For PowerShell 7+ we can use the native RSA $pem = Get-Content -Path $PrivateKeyFilePath -Raw $rsa = [System.Security.Cryptography.RSA]::Create() $rsa.ImportFromPem($pem) } } # Format the Signature $signedBytes = $rsa.SignHash($result, $hashAlgo, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) $signature = [Convert]::ToBase64String($signedBytes) # Invoke the WebRequest $headers = @{ Accept = "application/json" Date = "$now" Authorization = "Signature keyId=`"system/$($systemKey)`",headers=`"request-line date`",algorithm=`"rsa-sha256`",signature=`"$($signature)`"" } switch ($method) { 'GET' { $request = Invoke-RestMethod -Method $method -Uri "https://console.jumpcloud.com$requestURL" -ContentType 'application/json' -Headers $headers } 'PUT' { $request = Invoke-RestMethod -Method $method -Uri "https://console.jumpcloud.com$requestURL" -ContentType 'application/json' -Headers $headers -Body $form } 'POST' { $request = Invoke-RestMethod -Method $method -Uri "https://console.jumpcloud.com$requestURL" -ContentType 'application/json' -Headers $headers -Body $form } 'DELETE' { $request = Invoke-RestMethod -Method DELETE -Uri "https://console.jumpcloud.com/$requestURL" -ContentType 'application/json' -Headers $headers } Default { 'Invalid method specified. Valid methods are: GET, PUT, POST, DELETE.' } } } end { return $request } } |