@hellolynk-ai/codex-plugin 0.0.0-stage → 0.2.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +86 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +160 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[ValidateSet(
|
|
4
|
+
"PrepareRoot",
|
|
5
|
+
"SecureReleaseTree",
|
|
6
|
+
"VerifyReleaseTree",
|
|
7
|
+
"StoreProfile",
|
|
8
|
+
"StoreStatus",
|
|
9
|
+
"ProbeCredential",
|
|
10
|
+
"ValidateManagedFile",
|
|
11
|
+
"SecureManagedFile",
|
|
12
|
+
"Store",
|
|
13
|
+
"Load",
|
|
14
|
+
"Remove",
|
|
15
|
+
"StoreRuntimeSession",
|
|
16
|
+
"LoadRuntimeSession",
|
|
17
|
+
"RemoveRuntimeSession",
|
|
18
|
+
"StoreE2eePrivateKeys",
|
|
19
|
+
"LoadE2eePrivateKeys",
|
|
20
|
+
"LoadE2eePrivateKeysBatch",
|
|
21
|
+
"RemoveE2eePrivateKeys"
|
|
22
|
+
)]
|
|
23
|
+
[string]$Command,
|
|
24
|
+
|
|
25
|
+
[Parameter(Mandatory = $true)]
|
|
26
|
+
[ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$')]
|
|
27
|
+
[string]$ProfileId,
|
|
28
|
+
|
|
29
|
+
[string]$ClientRoot = "",
|
|
30
|
+
|
|
31
|
+
[string]$ManagedRelativePath = "",
|
|
32
|
+
|
|
33
|
+
[string]$StoreRoot = "",
|
|
34
|
+
|
|
35
|
+
[string]$E2eeStoreRoot = ""
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
$ErrorActionPreference = "Stop"
|
|
39
|
+
$ProgressPreference = "SilentlyContinue"
|
|
40
|
+
|
|
41
|
+
Add-Type -TypeDefinition @'
|
|
42
|
+
using System;
|
|
43
|
+
using System.ComponentModel;
|
|
44
|
+
using System.Runtime.InteropServices;
|
|
45
|
+
using Microsoft.Win32.SafeHandles;
|
|
46
|
+
|
|
47
|
+
public static class HelloLynkWindowsFileNative {
|
|
48
|
+
public const uint GENERIC_READ = 0x80000000;
|
|
49
|
+
public const uint FILE_SHARE_READ = 0x00000001;
|
|
50
|
+
public const uint FILE_SHARE_WRITE = 0x00000002;
|
|
51
|
+
public const uint FILE_SHARE_DELETE = 0x00000004;
|
|
52
|
+
public const uint OPEN_EXISTING = 3;
|
|
53
|
+
public const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
|
54
|
+
public const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
|
55
|
+
public const uint MOVEFILE_REPLACE_EXISTING = 0x00000001;
|
|
56
|
+
public const uint MOVEFILE_WRITE_THROUGH = 0x00000008;
|
|
57
|
+
|
|
58
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
59
|
+
public struct BY_HANDLE_FILE_INFORMATION {
|
|
60
|
+
public uint FileAttributes;
|
|
61
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
|
|
62
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
|
|
63
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
|
|
64
|
+
public uint VolumeSerialNumber;
|
|
65
|
+
public uint FileSizeHigh;
|
|
66
|
+
public uint FileSizeLow;
|
|
67
|
+
public uint NumberOfLinks;
|
|
68
|
+
public uint FileIndexHigh;
|
|
69
|
+
public uint FileIndexLow;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
73
|
+
public static extern SafeFileHandle CreateFileW(
|
|
74
|
+
string fileName, uint desiredAccess, uint shareMode, IntPtr securityAttributes,
|
|
75
|
+
uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile);
|
|
76
|
+
|
|
77
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
78
|
+
public static extern uint GetFinalPathNameByHandleW(
|
|
79
|
+
SafeFileHandle file, System.Text.StringBuilder path, uint length, uint flags);
|
|
80
|
+
|
|
81
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
82
|
+
public static extern bool GetFileInformationByHandle(
|
|
83
|
+
SafeFileHandle file, out BY_HANDLE_FILE_INFORMATION information);
|
|
84
|
+
|
|
85
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
86
|
+
public static extern bool MoveFileExW(string existingName, string newName, uint flags);
|
|
87
|
+
|
|
88
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
89
|
+
public static extern bool CreateDirectoryW(string path, IntPtr securityAttributes);
|
|
90
|
+
}
|
|
91
|
+
'@
|
|
92
|
+
|
|
93
|
+
$script:CurrentSid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
94
|
+
$script:SystemSid = [Security.Principal.SecurityIdentifier]::new("S-1-5-18")
|
|
95
|
+
$script:AdministratorsSid = [Security.Principal.SecurityIdentifier]::new("S-1-5-32-544")
|
|
96
|
+
$script:TrustedInstallerSid = try {
|
|
97
|
+
([Security.Principal.NTAccount]::new("NT SERVICE", "TrustedInstaller")).Translate(
|
|
98
|
+
[Security.Principal.SecurityIdentifier]
|
|
99
|
+
)
|
|
100
|
+
} catch { $null }
|
|
101
|
+
$script:AllowedSids = @(
|
|
102
|
+
$script:CurrentSid.Value,
|
|
103
|
+
$script:SystemSid.Value,
|
|
104
|
+
$script:AdministratorsSid.Value
|
|
105
|
+
)
|
|
106
|
+
$script:TrustedAncestorOwnerSids = @($script:AllowedSids)
|
|
107
|
+
if ($null -ne $script:TrustedInstallerSid) {
|
|
108
|
+
$script:TrustedAncestorOwnerSids += $script:TrustedInstallerSid.Value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Fail([string]$Code) {
|
|
112
|
+
[Console]::Error.WriteLine($Code)
|
|
113
|
+
exit 1
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Normalize-HelloLynkAbsoluteFixedPath([string]$PathValue) {
|
|
117
|
+
if (
|
|
118
|
+
[string]::IsNullOrWhiteSpace($PathValue) -or
|
|
119
|
+
-not [IO.Path]::IsPathFullyQualified($PathValue) -or
|
|
120
|
+
$PathValue.StartsWith("\\", [StringComparison]::Ordinal) -or
|
|
121
|
+
$PathValue.StartsWith("//", [StringComparison]::Ordinal)
|
|
122
|
+
) { Fail "client_root_path_invalid" }
|
|
123
|
+
try { $full = [IO.Path]::GetFullPath($PathValue) } catch { Fail "client_root_path_invalid" }
|
|
124
|
+
$trimmedRequested = $PathValue.TrimEnd([char[]]@('\', '/'))
|
|
125
|
+
$trimmedFull = $full.TrimEnd([char[]]@('\', '/'))
|
|
126
|
+
if (-not $trimmedRequested.Equals($trimmedFull, [StringComparison]::OrdinalIgnoreCase)) {
|
|
127
|
+
Fail "client_root_alias_rejected"
|
|
128
|
+
}
|
|
129
|
+
try { $drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($full)) } catch { Fail "client_root_volume_invalid" }
|
|
130
|
+
if ($drive.DriveType -ne [IO.DriveType]::Fixed) { Fail "client_root_fixed_local_required" }
|
|
131
|
+
return $full
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function Assert-HelloLynkNoReparseAncestors([string]$PathValue) {
|
|
135
|
+
$current = $PathValue
|
|
136
|
+
while (-not [string]::IsNullOrWhiteSpace($current)) {
|
|
137
|
+
if ([IO.Directory]::Exists($current) -or [IO.File]::Exists($current)) {
|
|
138
|
+
try { $item = Get-Item -LiteralPath $current -Force } catch { Fail "client_root_path_untrusted" }
|
|
139
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
140
|
+
Fail "client_root_reparse_rejected"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
$parent = [IO.Path]::GetDirectoryName($current.TrimEnd([char[]]@('\', '/')))
|
|
144
|
+
if ([string]::IsNullOrWhiteSpace($parent) -or $parent.Equals($current, [StringComparison]::OrdinalIgnoreCase)) { break }
|
|
145
|
+
$current = $parent
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function Get-HelloLynkExistingAncestorDirectories([string]$RootPath) {
|
|
150
|
+
$directories = [Collections.Generic.List[string]]::new()
|
|
151
|
+
$current = [IO.Path]::GetDirectoryName($RootPath.TrimEnd([char[]]@('\', '/')))
|
|
152
|
+
while (-not [string]::IsNullOrWhiteSpace($current)) {
|
|
153
|
+
if ([IO.Directory]::Exists($current)) { $directories.Add($current) }
|
|
154
|
+
$parent = [IO.Path]::GetDirectoryName($current.TrimEnd([char[]]@('\', '/')))
|
|
155
|
+
if ([string]::IsNullOrWhiteSpace($parent) -or $parent.Equals($current, [StringComparison]::OrdinalIgnoreCase)) {
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
$current = $parent
|
|
159
|
+
}
|
|
160
|
+
$ordered = @($directories)
|
|
161
|
+
[Array]::Reverse($ordered)
|
|
162
|
+
return $ordered
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function Assert-HelloLynkTrustedAncestorAcl([string]$RootPath) {
|
|
166
|
+
$rootParent = [IO.Path]::GetDirectoryName($RootPath.TrimEnd([char[]]@('\', '/')))
|
|
167
|
+
$directories = @(Get-HelloLynkExistingAncestorDirectories $RootPath)
|
|
168
|
+
$dangerousRights = (
|
|
169
|
+
[Security.AccessControl.FileSystemRights]::CreateFiles -bor
|
|
170
|
+
[Security.AccessControl.FileSystemRights]::CreateDirectories -bor
|
|
171
|
+
[Security.AccessControl.FileSystemRights]::WriteAttributes -bor
|
|
172
|
+
[Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
|
|
173
|
+
[Security.AccessControl.FileSystemRights]::Delete -bor
|
|
174
|
+
[Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
175
|
+
[Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
176
|
+
[Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
177
|
+
)
|
|
178
|
+
foreach ($directory in $directories) {
|
|
179
|
+
Assert-HelloLynkNoReparseAncestors $directory
|
|
180
|
+
$identity = Open-HelloLynkIdentity $directory $true
|
|
181
|
+
$identity.Handle.Dispose()
|
|
182
|
+
try { $acl = Get-Acl -LiteralPath $directory } catch { Fail "client_root_ancestor_acl_untrusted" }
|
|
183
|
+
try {
|
|
184
|
+
$ownerSid = ([Security.Principal.NTAccount]$acl.Owner).Translate(
|
|
185
|
+
[Security.Principal.SecurityIdentifier]
|
|
186
|
+
).Value
|
|
187
|
+
} catch {
|
|
188
|
+
try { $ownerSid = ([Security.Principal.SecurityIdentifier]$acl.Owner).Value } catch {
|
|
189
|
+
Fail "client_root_ancestor_owner_untrusted"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if ($script:TrustedAncestorOwnerSids -notcontains $ownerSid) {
|
|
193
|
+
Fail "client_root_ancestor_owner_untrusted"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Package/capability grants constrain an AppContainer in addition to its
|
|
197
|
+
# user's rights; they are not grants to another Windows user. Only accept
|
|
198
|
+
# specific app identities on this user's own profile ancestors. The actual
|
|
199
|
+
# credential root still requires the closed DACL checked below.
|
|
200
|
+
$userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile).TrimEnd('\', '/')
|
|
201
|
+
$isOwnProfileAncestor = $ownerSid -eq $script:CurrentSid.Value -and (
|
|
202
|
+
$directory.Equals($userProfile, [StringComparison]::OrdinalIgnoreCase) -or
|
|
203
|
+
$directory.StartsWith($userProfile + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)
|
|
204
|
+
)
|
|
205
|
+
$isRootParent = $directory.Equals($rootParent, [StringComparison]::OrdinalIgnoreCase)
|
|
206
|
+
$directoryIndex = [Array]::IndexOf($directories, $directory)
|
|
207
|
+
$nextDirectoryProtected = $false
|
|
208
|
+
if ($directoryIndex -ge 0 -and $directoryIndex + 1 -lt $directories.Count) {
|
|
209
|
+
try {
|
|
210
|
+
$nextDirectoryProtected = (Get-Acl -LiteralPath $directories[$directoryIndex + 1]).AreAccessRulesProtected
|
|
211
|
+
} catch { Fail "client_root_ancestor_acl_untrusted" }
|
|
212
|
+
}
|
|
213
|
+
$rules = $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])
|
|
214
|
+
foreach ($rule in $rules) {
|
|
215
|
+
if (
|
|
216
|
+
$rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow -or
|
|
217
|
+
$script:TrustedAncestorOwnerSids -contains $rule.IdentityReference.Value -or
|
|
218
|
+
($isOwnProfileAncestor -and $rule.IdentityReference.Value -match '^S-1-15-[23]-(?:\d+-){6,7}\d+$') -or
|
|
219
|
+
(($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0)
|
|
220
|
+
) { continue }
|
|
221
|
+
$rights = $rule.FileSystemRights
|
|
222
|
+
$hasDangerousRights = (($rights -band $dangerousRights) -ne 0)
|
|
223
|
+
$hasDeleteChild = (
|
|
224
|
+
($rights -band [Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles) -ne 0
|
|
225
|
+
)
|
|
226
|
+
if (
|
|
227
|
+
$hasDeleteChild -or
|
|
228
|
+
($hasDangerousRights -and ($isRootParent -or -not $nextDirectoryProtected))
|
|
229
|
+
) { Fail "client_root_ancestor_dacl_untrusted" }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function New-HelloLynkAcl([bool]$Directory) {
|
|
235
|
+
if ($Directory) {
|
|
236
|
+
$acl = [Security.AccessControl.DirectorySecurity]::new()
|
|
237
|
+
$inheritance = [Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
238
|
+
} else {
|
|
239
|
+
$acl = [Security.AccessControl.FileSecurity]::new()
|
|
240
|
+
$inheritance = [Security.AccessControl.InheritanceFlags]::None
|
|
241
|
+
}
|
|
242
|
+
$acl.SetOwner($script:CurrentSid)
|
|
243
|
+
$acl.SetAccessRuleProtection($true, $false)
|
|
244
|
+
foreach ($sid in @($script:CurrentSid, $script:SystemSid, $script:AdministratorsSid)) {
|
|
245
|
+
$rule = if ($Directory) {
|
|
246
|
+
[Security.AccessControl.FileSystemAccessRule]::new(
|
|
247
|
+
$sid,
|
|
248
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
249
|
+
$inheritance,
|
|
250
|
+
[Security.AccessControl.PropagationFlags]::None,
|
|
251
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
252
|
+
)
|
|
253
|
+
} else {
|
|
254
|
+
[Security.AccessControl.FileSystemAccessRule]::new(
|
|
255
|
+
$sid,
|
|
256
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
257
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
$null = $acl.AddAccessRule($rule)
|
|
261
|
+
}
|
|
262
|
+
return $acl
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function Set-HelloLynkSecureAcl([string]$PathValue, [bool]$Directory) {
|
|
266
|
+
try { Set-Acl -LiteralPath $PathValue -AclObject (New-HelloLynkAcl $Directory) } catch { Fail "client_root_acl_apply_failed" }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function Assert-HelloLynkSecureAcl([string]$PathValue, [bool]$Directory) {
|
|
270
|
+
try { $acl = Get-Acl -LiteralPath $PathValue } catch { Fail "client_root_acl_untrusted" }
|
|
271
|
+
try { $ownerSid = ([Security.Principal.NTAccount]$acl.Owner).Translate([Security.Principal.SecurityIdentifier]).Value } catch {
|
|
272
|
+
try { $ownerSid = ([Security.Principal.SecurityIdentifier]$acl.Owner).Value } catch { Fail "client_root_owner_untrusted" }
|
|
273
|
+
}
|
|
274
|
+
if ($ownerSid -ne $script:CurrentSid.Value) { Fail "client_root_owner_untrusted" }
|
|
275
|
+
if (-not $acl.AreAccessRulesProtected) { Fail "client_root_dacl_unprotected" }
|
|
276
|
+
$rules = $acl.GetAccessRules($true, $false, [Security.Principal.SecurityIdentifier])
|
|
277
|
+
$currentFullControl = $false
|
|
278
|
+
$systemFullControl = $false
|
|
279
|
+
$administratorsFullControl = $false
|
|
280
|
+
foreach ($rule in $rules) {
|
|
281
|
+
$sid = $rule.IdentityReference.Value
|
|
282
|
+
if ($script:AllowedSids -notcontains $sid) { Fail "client_root_dacl_untrusted" }
|
|
283
|
+
if ($rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) {
|
|
284
|
+
Fail "client_root_dacl_untrusted"
|
|
285
|
+
}
|
|
286
|
+
$hasFullControl = (
|
|
287
|
+
($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq
|
|
288
|
+
[Security.AccessControl.FileSystemRights]::FullControl
|
|
289
|
+
)
|
|
290
|
+
if ($hasFullControl) {
|
|
291
|
+
if ($sid -eq $script:CurrentSid.Value) { $currentFullControl = $true }
|
|
292
|
+
if ($sid -eq $script:SystemSid.Value) { $systemFullControl = $true }
|
|
293
|
+
if ($sid -eq $script:AdministratorsSid.Value) { $administratorsFullControl = $true }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (-not ($currentFullControl -and $systemFullControl -and $administratorsFullControl)) {
|
|
297
|
+
Fail "client_root_dacl_untrusted"
|
|
298
|
+
}
|
|
299
|
+
if ($Directory -and -not [IO.Directory]::Exists($PathValue)) { Fail "client_root_not_directory" }
|
|
300
|
+
if (-not $Directory -and -not [IO.File]::Exists($PathValue)) { Fail "client_file_not_regular" }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function Open-HelloLynkIdentity([string]$PathValue, [bool]$Directory) {
|
|
304
|
+
$flags = [HelloLynkWindowsFileNative]::FILE_FLAG_OPEN_REPARSE_POINT
|
|
305
|
+
if ($Directory) { $flags = $flags -bor [HelloLynkWindowsFileNative]::FILE_FLAG_BACKUP_SEMANTICS }
|
|
306
|
+
$handle = [HelloLynkWindowsFileNative]::CreateFileW(
|
|
307
|
+
$PathValue,
|
|
308
|
+
[HelloLynkWindowsFileNative]::GENERIC_READ,
|
|
309
|
+
[HelloLynkWindowsFileNative]::FILE_SHARE_READ -bor
|
|
310
|
+
[HelloLynkWindowsFileNative]::FILE_SHARE_WRITE -bor
|
|
311
|
+
[HelloLynkWindowsFileNative]::FILE_SHARE_DELETE,
|
|
312
|
+
[IntPtr]::Zero,
|
|
313
|
+
[HelloLynkWindowsFileNative]::OPEN_EXISTING,
|
|
314
|
+
$flags,
|
|
315
|
+
[IntPtr]::Zero
|
|
316
|
+
)
|
|
317
|
+
if ($handle.IsInvalid) { Fail "client_file_open_failed" }
|
|
318
|
+
$builder = [Text.StringBuilder]::new(32768)
|
|
319
|
+
$length = [HelloLynkWindowsFileNative]::GetFinalPathNameByHandleW($handle, $builder, $builder.Capacity, 0)
|
|
320
|
+
if ($length -eq 0 -or $length -ge $builder.Capacity) { $handle.Dispose(); Fail "client_file_final_path_failed" }
|
|
321
|
+
$final = $builder.ToString()
|
|
322
|
+
if ($final.StartsWith("\\?\UNC\", [StringComparison]::OrdinalIgnoreCase)) {
|
|
323
|
+
$handle.Dispose(); Fail "client_root_fixed_local_required"
|
|
324
|
+
}
|
|
325
|
+
if ($final.StartsWith("\\?\", [StringComparison]::Ordinal)) { $final = $final.Substring(4) }
|
|
326
|
+
if (-not $final.Equals($PathValue, [StringComparison]::OrdinalIgnoreCase)) {
|
|
327
|
+
$handle.Dispose(); Fail "client_root_alias_rejected"
|
|
328
|
+
}
|
|
329
|
+
$information = [HelloLynkWindowsFileNative+BY_HANDLE_FILE_INFORMATION]::new()
|
|
330
|
+
if (-not [HelloLynkWindowsFileNative]::GetFileInformationByHandle($handle, [ref]$information)) {
|
|
331
|
+
$handle.Dispose(); Fail "client_file_identity_failed"
|
|
332
|
+
}
|
|
333
|
+
return [pscustomobject]@{
|
|
334
|
+
Handle = $handle
|
|
335
|
+
Volume = $information.VolumeSerialNumber
|
|
336
|
+
IndexHigh = $information.FileIndexHigh
|
|
337
|
+
IndexLow = $information.FileIndexLow
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function Test-HelloLynkSameIdentity($Left, $Right) {
|
|
342
|
+
return (
|
|
343
|
+
$Left.Volume -eq $Right.Volume -and
|
|
344
|
+
$Left.IndexHigh -eq $Right.IndexHigh -and
|
|
345
|
+
$Left.IndexLow -eq $Right.IndexLow
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function Enter-HelloLynkRootMutex([string]$CanonicalRoot) {
|
|
350
|
+
$material = [Text.Encoding]::UTF8.GetBytes($script:CurrentSid.Value + "`0" + $CanonicalRoot.ToLowerInvariant())
|
|
351
|
+
$sha = [Security.Cryptography.SHA256]::Create()
|
|
352
|
+
try { $hash = [Convert]::ToHexString($sha.ComputeHash($material)).ToLowerInvariant() } finally { $sha.Dispose() }
|
|
353
|
+
$mutex = [Threading.Mutex]::new($false, ("Local\HelloLynk.ClientRoot.{0}" -f $hash))
|
|
354
|
+
try { $acquired = $mutex.WaitOne([TimeSpan]::FromSeconds(15)) } catch [Threading.AbandonedMutexException] { $acquired = $true }
|
|
355
|
+
if (-not $acquired) { $mutex.Dispose(); Fail "client_root_mutex_timeout" }
|
|
356
|
+
return $mutex
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function Exit-HelloLynkRootMutex($Mutex) {
|
|
360
|
+
if ($null -eq $Mutex) { return }
|
|
361
|
+
try { $Mutex.ReleaseMutex() } finally { $Mutex.Dispose() }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function Ensure-HelloLynkSecureDirectory([string]$PathValue) {
|
|
365
|
+
if ([IO.Directory]::Exists($PathValue)) {
|
|
366
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
367
|
+
Assert-HelloLynkSecureAcl $PathValue $true
|
|
368
|
+
$identity = Open-HelloLynkIdentity $PathValue $true
|
|
369
|
+
$identity.Handle.Dispose()
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
$parent = [IO.Path]::GetDirectoryName($PathValue)
|
|
373
|
+
if (-not [IO.Directory]::Exists($parent)) { Ensure-HelloLynkSecureDirectory $parent }
|
|
374
|
+
if (-not [HelloLynkWindowsFileNative]::CreateDirectoryW($PathValue, [IntPtr]::Zero)) {
|
|
375
|
+
Fail "client_root_create_race"
|
|
376
|
+
}
|
|
377
|
+
Set-HelloLynkSecureAcl $PathValue $true
|
|
378
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
379
|
+
Assert-HelloLynkSecureAcl $PathValue $true
|
|
380
|
+
$identity = Open-HelloLynkIdentity $PathValue $true
|
|
381
|
+
$identity.Handle.Dispose()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function Resolve-HelloLynkClientRoot([string]$RootValue, [bool]$Create) {
|
|
385
|
+
$root = Normalize-HelloLynkAbsoluteFixedPath $RootValue
|
|
386
|
+
Assert-HelloLynkNoReparseAncestors $root
|
|
387
|
+
$mutex = Enter-HelloLynkRootMutex $root
|
|
388
|
+
try {
|
|
389
|
+
Assert-HelloLynkTrustedAncestorAcl $root
|
|
390
|
+
if ($Create) { Ensure-HelloLynkSecureDirectory $root }
|
|
391
|
+
if (-not [IO.Directory]::Exists($root)) { Fail "client_root_missing" }
|
|
392
|
+
Assert-HelloLynkNoReparseAncestors $root
|
|
393
|
+
Assert-HelloLynkSecureAcl $root $true
|
|
394
|
+
$identity = Open-HelloLynkIdentity $root $true
|
|
395
|
+
try {
|
|
396
|
+
Assert-HelloLynkTrustedAncestorAcl $root
|
|
397
|
+
$canonical = $root
|
|
398
|
+
} finally { $identity.Handle.Dispose() }
|
|
399
|
+
return [pscustomobject]@{ Root = $canonical; Mutex = $mutex }
|
|
400
|
+
} catch {
|
|
401
|
+
Exit-HelloLynkRootMutex $mutex
|
|
402
|
+
throw
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function Assert-HelloLynkPathInsideRoot([string]$Root, [string]$Candidate, [string]$Expected) {
|
|
407
|
+
$candidateFull = Normalize-HelloLynkAbsoluteFixedPath $Candidate
|
|
408
|
+
if (-not $candidateFull.Equals($Expected, [StringComparison]::OrdinalIgnoreCase)) {
|
|
409
|
+
Fail "client_root_path_invalid"
|
|
410
|
+
}
|
|
411
|
+
$prefix = $Root.TrimEnd([char[]]@('\')) + '\'
|
|
412
|
+
if (-not $candidateFull.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
413
|
+
Fail "client_root_path_invalid"
|
|
414
|
+
}
|
|
415
|
+
return $candidateFull
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function Read-HelloLynkSecureText([string]$PathValue, [int]$MaxBytes) {
|
|
419
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
420
|
+
Assert-HelloLynkSecureAcl ([IO.Path]::GetDirectoryName($PathValue)) $true
|
|
421
|
+
Assert-HelloLynkSecureAcl $PathValue $false
|
|
422
|
+
$first = Open-HelloLynkIdentity $PathValue $false
|
|
423
|
+
try {
|
|
424
|
+
$stream = [IO.FileStream]::new($first.Handle, [IO.FileAccess]::Read)
|
|
425
|
+
try {
|
|
426
|
+
if ($stream.Length -gt $MaxBytes) { Fail "client_file_too_large" }
|
|
427
|
+
$reader = [IO.StreamReader]::new($stream, [Text.UTF8Encoding]::new($false, $true), $true, 4096, $true)
|
|
428
|
+
try { $text = $reader.ReadToEnd() } finally { $reader.Dispose() }
|
|
429
|
+
} finally { $stream.Dispose() }
|
|
430
|
+
$first.Handle = $null
|
|
431
|
+
} finally {
|
|
432
|
+
if ($null -ne $first.Handle) { $first.Handle.Dispose() }
|
|
433
|
+
}
|
|
434
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
435
|
+
Assert-HelloLynkSecureAcl $PathValue $false
|
|
436
|
+
$second = Open-HelloLynkIdentity $PathValue $false
|
|
437
|
+
try {
|
|
438
|
+
if (-not (Test-HelloLynkSameIdentity $first $second)) { Fail "client_file_identity_changed" }
|
|
439
|
+
} finally { $second.Handle.Dispose() }
|
|
440
|
+
return $text
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function Remove-HelloLynkVerifiedTemp([string]$TempPath) {
|
|
444
|
+
if (-not [IO.File]::Exists($TempPath)) { return }
|
|
445
|
+
Assert-HelloLynkNoReparseAncestors $TempPath
|
|
446
|
+
Assert-HelloLynkSecureAcl $TempPath $false
|
|
447
|
+
[IO.File]::Delete($TempPath)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function Write-HelloLynkSecureText([string]$PathValue, [string]$TextValue) {
|
|
451
|
+
$parent = [IO.Path]::GetDirectoryName($PathValue)
|
|
452
|
+
Ensure-HelloLynkSecureDirectory $parent
|
|
453
|
+
if ([IO.File]::Exists($PathValue)) {
|
|
454
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
455
|
+
Assert-HelloLynkSecureAcl $PathValue $false
|
|
456
|
+
}
|
|
457
|
+
$tempPath = Join-Path $parent (".{0}.{1}.tmp" -f [IO.Path]::GetFileName($PathValue), [Guid]::NewGuid().ToString("N"))
|
|
458
|
+
try {
|
|
459
|
+
$bytes = [Text.UTF8Encoding]::new($false).GetBytes($TextValue)
|
|
460
|
+
$stream = [IO.FileStream]::new($tempPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
|
|
461
|
+
try {
|
|
462
|
+
$stream.Write($bytes, 0, $bytes.Length)
|
|
463
|
+
$stream.Flush($true)
|
|
464
|
+
} finally { $stream.Dispose() }
|
|
465
|
+
Set-HelloLynkSecureAcl $tempPath $false
|
|
466
|
+
Assert-HelloLynkNoReparseAncestors $tempPath
|
|
467
|
+
Assert-HelloLynkSecureAcl $tempPath $false
|
|
468
|
+
$tempIdentity = Open-HelloLynkIdentity $tempPath $false
|
|
469
|
+
$tempIdentity.Handle.Dispose()
|
|
470
|
+
if (-not [HelloLynkWindowsFileNative]::MoveFileExW(
|
|
471
|
+
$tempPath,
|
|
472
|
+
$PathValue,
|
|
473
|
+
[HelloLynkWindowsFileNative]::MOVEFILE_REPLACE_EXISTING -bor [HelloLynkWindowsFileNative]::MOVEFILE_WRITE_THROUGH
|
|
474
|
+
)) { Fail "client_file_replace_failed" }
|
|
475
|
+
Assert-HelloLynkNoReparseAncestors $PathValue
|
|
476
|
+
Assert-HelloLynkSecureAcl $PathValue $false
|
|
477
|
+
$finalIdentity = Open-HelloLynkIdentity $PathValue $false
|
|
478
|
+
try {
|
|
479
|
+
if (-not (Test-HelloLynkSameIdentity $tempIdentity $finalIdentity)) { Fail "client_file_identity_changed" }
|
|
480
|
+
} finally { $finalIdentity.Handle.Dispose() }
|
|
481
|
+
} finally {
|
|
482
|
+
Remove-HelloLynkVerifiedTemp $tempPath
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
# Returns $null when the file is absent. Anything present but unreadable fails:
|
|
487
|
+
# a damaged key file is never reported as missing.
|
|
488
|
+
function Read-HelloLynkE2eePrivateKeys([string]$KeysPath, [string]$KeysProfileId) {
|
|
489
|
+
Assert-HelloLynkNoReparseAncestors $KeysPath
|
|
490
|
+
if (-not [IO.File]::Exists($KeysPath)) { return $null }
|
|
491
|
+
$serialized = Read-HelloLynkSecureText $KeysPath 524288
|
|
492
|
+
try { $credential = [Management.Automation.PSSerializer]::Deserialize($serialized) } catch { Fail "e2ee_private_key_material_invalid" }
|
|
493
|
+
if (
|
|
494
|
+
$credential -isnot [Management.Automation.PSCredential] -or
|
|
495
|
+
$credential.UserName -ne ("hellolynk-direct-text-e2ee-private-keys:v2:{0}" -f $KeysProfileId)
|
|
496
|
+
) { Fail "e2ee_private_key_material_binding_invalid" }
|
|
497
|
+
$plain = $credential.GetNetworkCredential().Password
|
|
498
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 262144) { Fail "e2ee_private_key_material_too_large" }
|
|
499
|
+
try { $wrapper = $plain | ConvertFrom-Json } catch { Fail "e2ee_private_key_material_invalid" }
|
|
500
|
+
$propertyNames = @($wrapper.PSObject.Properties.Name)
|
|
501
|
+
if (
|
|
502
|
+
$propertyNames.Count -ne 3 -or
|
|
503
|
+
$propertyNames -notcontains "schemaVersion" -or
|
|
504
|
+
$propertyNames -notcontains "format" -or
|
|
505
|
+
$propertyNames -notcontains "privateKeyMaterial" -or
|
|
506
|
+
[int]$wrapper.schemaVersion -ne 2 -or
|
|
507
|
+
[string]$wrapper.format -ne "hellolynk_direct_text_e2ee_private_keys_v2" -or
|
|
508
|
+
[string]::IsNullOrWhiteSpace([string]$wrapper.privateKeyMaterial)
|
|
509
|
+
) { Fail "e2ee_private_key_material_invalid" }
|
|
510
|
+
try { $null = [string]$wrapper.privateKeyMaterial | ConvertFrom-Json } catch { Fail "e2ee_private_key_material_invalid" }
|
|
511
|
+
return $plain
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
if (-not $ClientRoot) {
|
|
516
|
+
if ($StoreRoot -and [IO.Path]::GetFileName($StoreRoot.TrimEnd([char[]]@('\', '/'))) -ieq "credentials") {
|
|
517
|
+
$ClientRoot = [IO.Path]::GetDirectoryName($StoreRoot.TrimEnd([char[]]@('\', '/')))
|
|
518
|
+
} else {
|
|
519
|
+
$ClientRoot = if ($StoreRoot) { $StoreRoot } elseif ($env:LOCALAPPDATA) { Join-Path $env:LOCALAPPDATA "HelloLynk" } else { "" }
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (-not $ClientRoot) { Fail "client_root_unavailable" }
|
|
523
|
+
$createRoot = $Command -in @(
|
|
524
|
+
"PrepareRoot", "StoreProfile", "StoreStatus", "Store", "StoreRuntimeSession", "StoreE2eePrivateKeys"
|
|
525
|
+
)
|
|
526
|
+
$rootSession = Resolve-HelloLynkClientRoot $ClientRoot $createRoot
|
|
527
|
+
$clientRootFull = $rootSession.Root
|
|
528
|
+
|
|
529
|
+
$expectedCredentialRoot = Join-Path $clientRootFull "credentials"
|
|
530
|
+
$expectedE2eeRoot = Join-Path $clientRootFull "e2ee-private-keys"
|
|
531
|
+
$storeRootFull = if ($StoreRoot) { Normalize-HelloLynkAbsoluteFixedPath $StoreRoot } else { $expectedCredentialRoot }
|
|
532
|
+
$e2eeStoreRootFull = if ($E2eeStoreRoot) { Normalize-HelloLynkAbsoluteFixedPath $E2eeStoreRoot } else { $expectedE2eeRoot }
|
|
533
|
+
if (
|
|
534
|
+
-not $storeRootFull.Equals($clientRootFull, [StringComparison]::OrdinalIgnoreCase) -and
|
|
535
|
+
-not $storeRootFull.Equals($expectedCredentialRoot, [StringComparison]::OrdinalIgnoreCase)
|
|
536
|
+
) { Fail "credential_store_root_invalid" }
|
|
537
|
+
if (-not $e2eeStoreRootFull.Equals($expectedE2eeRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
|
538
|
+
Fail "e2ee_private_key_store_root_invalid"
|
|
539
|
+
}
|
|
540
|
+
if ($storeRootFull.Equals($e2eeStoreRootFull, [StringComparison]::OrdinalIgnoreCase)) {
|
|
541
|
+
Fail "e2ee_private_key_store_not_separated"
|
|
542
|
+
}
|
|
543
|
+
if ($Command -eq "StoreE2eePrivateKeys") { Ensure-HelloLynkSecureDirectory $e2eeStoreRootFull }
|
|
544
|
+
if (
|
|
545
|
+
$Command -in @(
|
|
546
|
+
"LoadE2eePrivateKeys", "LoadE2eePrivateKeysBatch", "RemoveE2eePrivateKeys"
|
|
547
|
+
) -and
|
|
548
|
+
[IO.Directory]::Exists($e2eeStoreRootFull)
|
|
549
|
+
) { Ensure-HelloLynkSecureDirectory $e2eeStoreRootFull }
|
|
550
|
+
if ($Command -in @("Store", "StoreRuntimeSession")) {
|
|
551
|
+
Ensure-HelloLynkSecureDirectory $storeRootFull
|
|
552
|
+
}
|
|
553
|
+
if (
|
|
554
|
+
$Command -in @("Load", "Remove", "ProbeCredential", "LoadRuntimeSession", "RemoveRuntimeSession") -and
|
|
555
|
+
[IO.Directory]::Exists($storeRootFull)
|
|
556
|
+
) { Ensure-HelloLynkSecureDirectory $storeRootFull }
|
|
557
|
+
$credentialPath = Join-Path $storeRootFull ("credential-{0}.dpapi.clixml" -f $ProfileId)
|
|
558
|
+
$runtimeSessionPath = Join-Path $storeRootFull ("runtime-session-{0}.dpapi.clixml" -f $ProfileId)
|
|
559
|
+
$e2eePrivateKeysPath = Join-Path $e2eeStoreRootFull ("direct-text-private-keys-{0}.dpapi.clixml" -f $ProfileId)
|
|
560
|
+
|
|
561
|
+
switch ($Command) {
|
|
562
|
+
"PrepareRoot" {
|
|
563
|
+
foreach ($child in @("profiles", "credentials", "e2ee-private-keys", "status", "core", "platform", "bin", "host", "host\runtime", "releases")) {
|
|
564
|
+
Ensure-HelloLynkSecureDirectory (Join-Path $clientRootFull $child)
|
|
565
|
+
}
|
|
566
|
+
Ensure-HelloLynkSecureDirectory (Join-Path $clientRootFull "platform\windows")
|
|
567
|
+
Ensure-HelloLynkSecureDirectory (Join-Path $clientRootFull "core\vendor")
|
|
568
|
+
[Console]::Out.Write("prepared")
|
|
569
|
+
}
|
|
570
|
+
{ $_ -in @("SecureReleaseTree", "VerifyReleaseTree") } {
|
|
571
|
+
if ($ManagedRelativePath -notmatch '^releases\\(?!\.{1,2}$)[A-Za-z0-9.][A-Za-z0-9_.-]{2,191}$') {
|
|
572
|
+
Fail "client_release_path_invalid"
|
|
573
|
+
}
|
|
574
|
+
$releasePath = Assert-HelloLynkPathInsideRoot `
|
|
575
|
+
$clientRootFull `
|
|
576
|
+
(Join-Path $clientRootFull $ManagedRelativePath) `
|
|
577
|
+
(Join-Path $clientRootFull $ManagedRelativePath)
|
|
578
|
+
if (-not [IO.Directory]::Exists($releasePath)) { Fail "client_release_missing" }
|
|
579
|
+
$pending = [Collections.Generic.Stack[string]]::new()
|
|
580
|
+
$pending.Push($releasePath)
|
|
581
|
+
while ($pending.Count -gt 0) {
|
|
582
|
+
$directory = $pending.Pop()
|
|
583
|
+
Assert-HelloLynkNoReparseAncestors $directory
|
|
584
|
+
if ($Command -eq "SecureReleaseTree") { Set-HelloLynkSecureAcl $directory $true }
|
|
585
|
+
Assert-HelloLynkSecureAcl $directory $true
|
|
586
|
+
foreach ($item in @(Get-ChildItem -LiteralPath $directory -Force)) {
|
|
587
|
+
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
588
|
+
Fail "client_release_reparse_rejected"
|
|
589
|
+
}
|
|
590
|
+
if ($item.PSIsContainer) {
|
|
591
|
+
$pending.Push($item.FullName)
|
|
592
|
+
} elseif ([IO.File]::Exists($item.FullName)) {
|
|
593
|
+
if ($Command -eq "SecureReleaseTree") { Set-HelloLynkSecureAcl $item.FullName $false }
|
|
594
|
+
Assert-HelloLynkSecureAcl $item.FullName $false
|
|
595
|
+
} else {
|
|
596
|
+
Fail "client_release_entry_invalid"
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
[Console]::Out.Write($(if ($Command -eq "SecureReleaseTree") { "secured" } else { "verified" }))
|
|
601
|
+
}
|
|
602
|
+
"StoreProfile" {
|
|
603
|
+
$path = Join-Path $clientRootFull ("profiles\{0}.json" -f $ProfileId)
|
|
604
|
+
$plain = [Console]::In.ReadToEnd()
|
|
605
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 65536) { Fail "client_profile_too_large" }
|
|
606
|
+
Write-HelloLynkSecureText $path $plain
|
|
607
|
+
$plain = $null
|
|
608
|
+
[Console]::Out.Write("stored")
|
|
609
|
+
}
|
|
610
|
+
"StoreStatus" {
|
|
611
|
+
$path = Join-Path $clientRootFull ("status\{0}.json" -f $ProfileId)
|
|
612
|
+
$plain = [Console]::In.ReadToEnd()
|
|
613
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 65536) { Fail "host_status_too_large" }
|
|
614
|
+
Write-HelloLynkSecureText $path $plain
|
|
615
|
+
$plain = $null
|
|
616
|
+
[Console]::Out.Write("stored")
|
|
617
|
+
}
|
|
618
|
+
"ProbeCredential" {
|
|
619
|
+
Assert-HelloLynkNoReparseAncestors $credentialPath
|
|
620
|
+
if (-not [IO.File]::Exists($credentialPath)) { Fail "device_credential_missing" }
|
|
621
|
+
Assert-HelloLynkSecureAcl $credentialPath $false
|
|
622
|
+
$probe = Open-HelloLynkIdentity $credentialPath $false
|
|
623
|
+
$probe.Handle.Dispose()
|
|
624
|
+
[Console]::Out.Write("present")
|
|
625
|
+
}
|
|
626
|
+
"ValidateManagedFile" {
|
|
627
|
+
if (
|
|
628
|
+
[string]::IsNullOrWhiteSpace($ManagedRelativePath) -or
|
|
629
|
+
$ManagedRelativePath -notmatch '^(?:core(?:\\vendor)?|platform\\windows|bin)\\[A-Za-z0-9_.-]+$'
|
|
630
|
+
) { Fail "client_managed_path_invalid" }
|
|
631
|
+
$path = Assert-HelloLynkPathInsideRoot `
|
|
632
|
+
$clientRootFull `
|
|
633
|
+
(Join-Path $clientRootFull $ManagedRelativePath) `
|
|
634
|
+
(Join-Path $clientRootFull $ManagedRelativePath)
|
|
635
|
+
Assert-HelloLynkNoReparseAncestors $path
|
|
636
|
+
if (-not [IO.File]::Exists($path)) { Fail "client_managed_file_missing" }
|
|
637
|
+
Assert-HelloLynkSecureAcl $path $false
|
|
638
|
+
$managedIdentity = Open-HelloLynkIdentity $path $false
|
|
639
|
+
$managedIdentity.Handle.Dispose()
|
|
640
|
+
[Console]::Out.Write("trusted")
|
|
641
|
+
}
|
|
642
|
+
"SecureManagedFile" {
|
|
643
|
+
if (
|
|
644
|
+
[string]::IsNullOrWhiteSpace($ManagedRelativePath) -or
|
|
645
|
+
$ManagedRelativePath -notmatch '^(?:core(?:\\vendor)?|platform\\windows|bin)\\[A-Za-z0-9_.-]+$'
|
|
646
|
+
) { Fail "client_managed_path_invalid" }
|
|
647
|
+
$path = Assert-HelloLynkPathInsideRoot `
|
|
648
|
+
$clientRootFull `
|
|
649
|
+
(Join-Path $clientRootFull $ManagedRelativePath) `
|
|
650
|
+
(Join-Path $clientRootFull $ManagedRelativePath)
|
|
651
|
+
Assert-HelloLynkNoReparseAncestors $path
|
|
652
|
+
if (-not [IO.File]::Exists($path)) { Fail "client_managed_file_missing" }
|
|
653
|
+
Set-HelloLynkSecureAcl $path $false
|
|
654
|
+
Assert-HelloLynkSecureAcl $path $false
|
|
655
|
+
$managedIdentity = Open-HelloLynkIdentity $path $false
|
|
656
|
+
$managedIdentity.Handle.Dispose()
|
|
657
|
+
[Console]::Out.Write("secured")
|
|
658
|
+
}
|
|
659
|
+
"Store" {
|
|
660
|
+
$plain = [Console]::In.ReadToEnd()
|
|
661
|
+
if ($plain -notmatch '^hellolynk_device_v1_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{32,}$') {
|
|
662
|
+
Fail "device_credential_invalid"
|
|
663
|
+
}
|
|
664
|
+
$secure = ConvertTo-SecureString -String $plain -AsPlainText -Force
|
|
665
|
+
$credential = [Management.Automation.PSCredential]::new($ProfileId, $secure)
|
|
666
|
+
try {
|
|
667
|
+
$serialized = [Management.Automation.PSSerializer]::Serialize($credential)
|
|
668
|
+
Write-HelloLynkSecureText $credentialPath $serialized
|
|
669
|
+
} finally {
|
|
670
|
+
$plain = $null
|
|
671
|
+
$secure = $null
|
|
672
|
+
$credential = $null
|
|
673
|
+
$serialized = $null
|
|
674
|
+
}
|
|
675
|
+
[Console]::Out.Write("stored")
|
|
676
|
+
}
|
|
677
|
+
"Load" {
|
|
678
|
+
Assert-HelloLynkNoReparseAncestors $credentialPath
|
|
679
|
+
if (-not [IO.File]::Exists($credentialPath)) { Fail "device_credential_missing" }
|
|
680
|
+
$serialized = Read-HelloLynkSecureText $credentialPath 262144
|
|
681
|
+
try { $credential = [Management.Automation.PSSerializer]::Deserialize($serialized) } catch { Fail "device_credential_invalid" }
|
|
682
|
+
if ($credential -isnot [Management.Automation.PSCredential] -or $credential.UserName -ne $ProfileId) {
|
|
683
|
+
Fail "device_credential_binding_invalid"
|
|
684
|
+
}
|
|
685
|
+
$plain = $credential.GetNetworkCredential().Password
|
|
686
|
+
if ($plain -notmatch '^hellolynk_device_v1_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{32,}$') {
|
|
687
|
+
Fail "device_credential_invalid"
|
|
688
|
+
}
|
|
689
|
+
[Console]::Out.Write($plain)
|
|
690
|
+
$plain = $null
|
|
691
|
+
$credential = $null
|
|
692
|
+
$serialized = $null
|
|
693
|
+
}
|
|
694
|
+
"Remove" {
|
|
695
|
+
if ([IO.File]::Exists($credentialPath)) {
|
|
696
|
+
Assert-HelloLynkNoReparseAncestors $credentialPath
|
|
697
|
+
Assert-HelloLynkSecureAcl $credentialPath $false
|
|
698
|
+
[IO.File]::Delete($credentialPath)
|
|
699
|
+
}
|
|
700
|
+
[Console]::Out.Write("removed")
|
|
701
|
+
}
|
|
702
|
+
"StoreRuntimeSession" {
|
|
703
|
+
$plain = [Console]::In.ReadToEnd()
|
|
704
|
+
if ([Text.Encoding]::UTF8.GetByteCount($plain) -gt 65536) { Fail "runtime_session_too_large" }
|
|
705
|
+
try { $session = $plain | ConvertFrom-Json } catch { Fail "runtime_session_invalid" }
|
|
706
|
+
if (
|
|
707
|
+
[int]$session.schemaVersion -ne 1 -or
|
|
708
|
+
[string]$session.sessionId -notmatch '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$' -or
|
|
709
|
+
[string]$session.runtimeId -notmatch '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$' -or
|
|
710
|
+
[string]$session.principalId -notmatch '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$' -or
|
|
711
|
+
[string]$session.agentId -notmatch '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$' -or
|
|
712
|
+
[string]$session.deviceId -notmatch '^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$' -or
|
|
713
|
+
[string]$session.sessionSecret -notmatch '^runtime_[a-f0-9]{48}$' -or
|
|
714
|
+
[double]$session.expiresAtMs -le 0
|
|
715
|
+
) { Fail "runtime_session_invalid" }
|
|
716
|
+
$secure = ConvertTo-SecureString -String $plain -AsPlainText -Force
|
|
717
|
+
$credential = [Management.Automation.PSCredential]::new(("runtime-session:{0}" -f $ProfileId), $secure)
|
|
718
|
+
try {
|
|
719
|
+
$serialized = [Management.Automation.PSSerializer]::Serialize($credential)
|
|
720
|
+
Write-HelloLynkSecureText $runtimeSessionPath $serialized
|
|
721
|
+
} finally {
|
|
722
|
+
$plain = $null
|
|
723
|
+
$session = $null
|
|
724
|
+
$secure = $null
|
|
725
|
+
$credential = $null
|
|
726
|
+
$serialized = $null
|
|
727
|
+
}
|
|
728
|
+
[Console]::Out.Write("stored")
|
|
729
|
+
}
|
|
730
|
+
"LoadRuntimeSession" {
|
|
731
|
+
Assert-HelloLynkNoReparseAncestors $runtimeSessionPath
|
|
732
|
+
if (-not [IO.File]::Exists($runtimeSessionPath)) {
|
|
733
|
+
[Console]::Out.Write("missing")
|
|
734
|
+
break
|
|
735
|
+
}
|
|
736
|
+
$serialized = Read-HelloLynkSecureText $runtimeSessionPath 262144
|
|
737
|
+
try { $credential = [Management.Automation.PSSerializer]::Deserialize($serialized) } catch { Fail "runtime_session_invalid" }
|
|
738
|
+
if (
|
|
739
|
+
$credential -isnot [Management.Automation.PSCredential] -or
|
|
740
|
+
$credential.UserName -ne ("runtime-session:{0}" -f $ProfileId)
|
|
741
|
+
) { Fail "runtime_session_binding_invalid" }
|
|
742
|
+
$plain = $credential.GetNetworkCredential().Password
|
|
743
|
+
try { $session = $plain | ConvertFrom-Json } catch { Fail "runtime_session_invalid" }
|
|
744
|
+
if (
|
|
745
|
+
[int]$session.schemaVersion -ne 1 -or
|
|
746
|
+
[string]$session.sessionSecret -notmatch '^runtime_[a-f0-9]{48}$' -or
|
|
747
|
+
[double]$session.expiresAtMs -le 0
|
|
748
|
+
) { Fail "runtime_session_invalid" }
|
|
749
|
+
[Console]::Out.Write($plain)
|
|
750
|
+
$plain = $null
|
|
751
|
+
$session = $null
|
|
752
|
+
$credential = $null
|
|
753
|
+
$serialized = $null
|
|
754
|
+
}
|
|
755
|
+
"RemoveRuntimeSession" {
|
|
756
|
+
if ([IO.File]::Exists($runtimeSessionPath)) {
|
|
757
|
+
Assert-HelloLynkNoReparseAncestors $runtimeSessionPath
|
|
758
|
+
Assert-HelloLynkSecureAcl $runtimeSessionPath $false
|
|
759
|
+
[IO.File]::Delete($runtimeSessionPath)
|
|
760
|
+
}
|
|
761
|
+
[Console]::Out.Write("removed")
|
|
762
|
+
}
|
|
763
|
+
"StoreE2eePrivateKeys" {
|
|
764
|
+
$plain = [Console]::In.ReadToEnd()
|
|
765
|
+
# A SecureString holds at most 65536 characters. Larger keys fail here,
|
|
766
|
+
# before the existing file is touched.
|
|
767
|
+
if ($plain.Length -gt 65536) { Fail "e2ee_private_key_material_too_large" }
|
|
768
|
+
try { $wrapper = $plain | ConvertFrom-Json } catch { Fail "e2ee_private_key_material_invalid" }
|
|
769
|
+
$propertyNames = @($wrapper.PSObject.Properties.Name)
|
|
770
|
+
if (
|
|
771
|
+
$propertyNames.Count -ne 3 -or
|
|
772
|
+
$propertyNames -notcontains "schemaVersion" -or
|
|
773
|
+
$propertyNames -notcontains "format" -or
|
|
774
|
+
$propertyNames -notcontains "privateKeyMaterial" -or
|
|
775
|
+
[int]$wrapper.schemaVersion -ne 2 -or
|
|
776
|
+
[string]$wrapper.format -ne "hellolynk_direct_text_e2ee_private_keys_v2" -or
|
|
777
|
+
[string]::IsNullOrWhiteSpace([string]$wrapper.privateKeyMaterial) -or
|
|
778
|
+
[Text.Encoding]::UTF8.GetByteCount([string]$wrapper.privateKeyMaterial) -gt 98304
|
|
779
|
+
) { Fail "e2ee_private_key_material_invalid" }
|
|
780
|
+
try {
|
|
781
|
+
$material = [string]$wrapper.privateKeyMaterial | ConvertFrom-Json
|
|
782
|
+
} catch { Fail "e2ee_private_key_material_invalid" }
|
|
783
|
+
if ($null -eq $material -or $material -is [Array]) { Fail "e2ee_private_key_material_invalid" }
|
|
784
|
+
$secure = ConvertTo-SecureString -String $plain -AsPlainText -Force
|
|
785
|
+
$credential = [Management.Automation.PSCredential]::new(
|
|
786
|
+
("hellolynk-direct-text-e2ee-private-keys:v2:{0}" -f $ProfileId),
|
|
787
|
+
$secure
|
|
788
|
+
)
|
|
789
|
+
try {
|
|
790
|
+
$serialized = [Management.Automation.PSSerializer]::Serialize($credential)
|
|
791
|
+
Write-HelloLynkSecureText $e2eePrivateKeysPath $serialized
|
|
792
|
+
} finally {
|
|
793
|
+
$plain = $null
|
|
794
|
+
$wrapper = $null
|
|
795
|
+
$material = $null
|
|
796
|
+
$secure = $null
|
|
797
|
+
$credential = $null
|
|
798
|
+
$serialized = $null
|
|
799
|
+
}
|
|
800
|
+
[Console]::Out.Write("stored")
|
|
801
|
+
}
|
|
802
|
+
"LoadE2eePrivateKeys" {
|
|
803
|
+
$plain = Read-HelloLynkE2eePrivateKeys $e2eePrivateKeysPath $ProfileId
|
|
804
|
+
[Console]::Out.Write($(if ($null -eq $plain) { "missing" } else { $plain }))
|
|
805
|
+
$plain = $null
|
|
806
|
+
}
|
|
807
|
+
"LoadE2eePrivateKeysBatch" {
|
|
808
|
+
# stdin: a JSON array of distinct profile IDs. One root check and one
|
|
809
|
+
# process serve every profile; any damaged file fails the whole batch.
|
|
810
|
+
try { $requested = [Console]::In.ReadToEnd() | ConvertFrom-Json -NoEnumerate } catch { Fail "e2ee_private_key_batch_invalid" }
|
|
811
|
+
if ($requested -isnot [Array] -or $requested.Count -lt 1 -or $requested.Count -gt 64) { Fail "e2ee_private_key_batch_invalid" }
|
|
812
|
+
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
813
|
+
foreach ($id in $requested) {
|
|
814
|
+
if ($id -isnot [string] -or $id -notmatch '^[A-Za-z0-9][A-Za-z0-9_.-]{2,127}$' -or -not $seen.Add($id)) {
|
|
815
|
+
Fail "e2ee_private_key_batch_invalid"
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
$entries = [Collections.Generic.List[string]]::new()
|
|
819
|
+
foreach ($id in $requested) {
|
|
820
|
+
$plain = Read-HelloLynkE2eePrivateKeys (Join-Path $e2eeStoreRootFull ("direct-text-private-keys-{0}.dpapi.clixml" -f $id)) $id
|
|
821
|
+
$entries.Add($(if ($null -eq $plain) { '{{"profileId":"{0}","state":"missing"}}' -f $id } else {
|
|
822
|
+
'{{"profileId":"{0}","state":"present","value":"{1}"}}' -f $id, [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($plain))
|
|
823
|
+
}))
|
|
824
|
+
$plain = $null
|
|
825
|
+
}
|
|
826
|
+
[Console]::Out.Write("[" + ($entries -join ",") + "]")
|
|
827
|
+
$entries = $null
|
|
828
|
+
}
|
|
829
|
+
"RemoveE2eePrivateKeys" {
|
|
830
|
+
if ([IO.File]::Exists($e2eePrivateKeysPath)) {
|
|
831
|
+
Assert-HelloLynkNoReparseAncestors $e2eePrivateKeysPath
|
|
832
|
+
Assert-HelloLynkSecureAcl $e2eePrivateKeysPath $false
|
|
833
|
+
[IO.File]::Delete($e2eePrivateKeysPath)
|
|
834
|
+
}
|
|
835
|
+
[Console]::Out.Write("removed")
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
} catch {
|
|
839
|
+
$message = [string]$_.Exception.Message
|
|
840
|
+
if ($message -match '^[a-z][a-z0-9_]{0,127}$') { Fail $message }
|
|
841
|
+
Fail "credential_store_operation_failed"
|
|
842
|
+
} finally {
|
|
843
|
+
if ($null -ne $rootSession) { Exit-HelloLynkRootMutex $rootSession.Mutex }
|
|
844
|
+
}
|