[CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9-]{0,62}$')] [string]$ComputerName, [Parameter(Mandatory)] [ValidatePattern('^\d+$')] [string]$RustDeskId, [Parameter(Mandatory)] [ValidateLength(12, 256)] [string]$AccessPassword, [string]$InventoryRoot = "$env:ProgramData\SGU\RustDesk\Devices" ) $ErrorActionPreference = 'Stop' function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Only a local administrator can register a RustDesk device credential.' } } function Initialize-DataProtection { if (-not ('SguRustDeskDataProtection' -as [type])) { Add-Type -TypeDefinition @' using System; using System.ComponentModel; using System.Runtime.InteropServices; public static class SguRustDeskDataProtection { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct DataBlob { public int cbData; public IntPtr pbData; } [DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CryptProtectData(ref DataBlob input, string description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output); [DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool CryptUnprotectData(ref DataBlob input, IntPtr description, IntPtr entropy, IntPtr reserved, IntPtr prompt, int flags, out DataBlob output); [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LocalFree(IntPtr memory); private const int CryptProtectLocalMachine = 0x4; private static DataBlob ToBlob(byte[] value) { var blob = new DataBlob { cbData = value.Length, pbData = IntPtr.Zero }; if (value.Length > 0) { blob.pbData = Marshal.AllocHGlobal(value.Length); Marshal.Copy(value, 0, blob.pbData, value.Length); } return blob; } private static byte[] FromBlob(DataBlob blob) { var value = new byte[blob.cbData]; if (blob.cbData > 0) Marshal.Copy(blob.pbData, value, 0, blob.cbData); return value; } public static byte[] Protect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptProtectData(ref input, null, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, CryptProtectLocalMachine, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } } public static byte[] Unprotect(byte[] value) { var input = ToBlob(value); var output = new DataBlob(); try { if (!CryptUnprotectData(ref input, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, 0, out output)) throw new Win32Exception(Marshal.GetLastWin32Error()); return FromBlob(output); } finally { if (input.pbData != IntPtr.Zero) Marshal.FreeHGlobal(input.pbData); if (output.pbData != IntPtr.Zero) LocalFree(output.pbData); } } } '@ -ErrorAction Stop } } function Set-PrivateDirectoryAcl { param([Parameter(Mandatory)][string]$Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null $acl = New-Object Security.AccessControl.DirectorySecurity $acl.SetAccessRuleProtection($true, $false) $inheritance = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit' $allow = [Security.AccessControl.AccessControlType]::Allow foreach ($sid in @('S-1-5-18', 'S-1-5-32-544')) { $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new( [Security.Principal.SecurityIdentifier]::new($sid), [Security.AccessControl.FileSystemRights]::FullControl, $inheritance, [Security.AccessControl.PropagationFlags]::None, $allow)) } Set-Acl -LiteralPath $Path -AclObject $acl } Assert-Administrator Initialize-DataProtection if (-not $PSCmdlet.ShouldProcess($ComputerName, 'Register the protected RustDesk management credential')) { return } Set-PrivateDirectoryAcl -Path $InventoryRoot $normalizedName = $ComputerName.ToUpperInvariant() $secretPath = Join-Path $InventoryRoot "$normalizedName.secret" $metadataPath = Join-Path $InventoryRoot "$normalizedName.json" $protectedPassword = [SguRustDeskDataProtection]::Protect( [Text.Encoding]::UTF8.GetBytes($AccessPassword)) [IO.File]::WriteAllBytes($secretPath, $protectedPassword) $metadata = [ordered]@{ ComputerName = $normalizedName RustDeskId = $RustDeskId RegisteredAt = (Get-Date).ToString('o') SecretPath = $secretPath } [IO.File]::WriteAllText($metadataPath, ($metadata | ConvertTo-Json), [Text.UTF8Encoding]::new($false)) [pscustomobject]@{ ComputerName = $normalizedName RustDeskId = $RustDeskId Registered = $true MetadataPath = $metadataPath }