isoloom_render/secrets/
windows.rs1use isoloom_plan::SecretFile;
15use isoloom_spec::model::is_drive_path;
16use isoloom_spec::shell::base64;
17
18use crate::powershell;
19
20pub const KEPT_DIR: &str = "C:\\ProgramData\\isoloom\\secrets";
22pub const DELIVERED: &str = "C:\\ProgramData\\isoloom\\secrets\\.delivered";
24pub const PLACE_SCRIPT: &str = "C:\\ProgramData\\isoloom\\apply-secrets.ps1";
26pub const PLACE_TASK: &str = "isoloom-apply-secrets";
28
29pub const BOOTSTRAP: &str = "$s = [Console]::In.ReadLine(); Invoke-Expression ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)))";
33
34const PROTECT: &str = "function Protect-IsoloomPath($p) { $dir = Test-Path -PathType Container $p; \
37$acl = if ($dir) { New-Object Security.AccessControl.DirectorySecurity } else { New-Object Security.AccessControl.FileSecurity }; \
38$acl.SetAccessRuleProtection($true, $false); $inherit = if ($dir) { 'ContainerInherit, ObjectInherit' } else { 'None' }; \
39foreach ($sid in 'S-1-5-18', 'S-1-5-32-544') { $acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier $sid), 'FullControl', $inherit, 'None', 'Allow'))) }; \
40Set-Acl -Path $p -AclObject $acl }";
41
42#[must_use]
44pub fn path(p: &str) -> String {
45 if is_drive_path(p) {
46 p.to_owned()
47 } else {
48 format!("C:{}", p.replace('/', "\\"))
49 }
50}
51
52#[must_use]
56pub fn receive(names: &[String], files: &[SecretFile]) -> String {
57 let list = names.iter().map(|n| powershell::quote(n)).collect::<Vec<_>>().join(", ");
58 format!(
59 "$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {k}\nNew-Item -ItemType Directory -Force $k | Out-Null\nProtect-IsoloomPath $k\n\
60 foreach ($n in @({list})) {{ $v = [Console]::In.ReadLine(); if ($null -eq $v) {{ throw \"no value came for $n\" }}; [IO.File]::WriteAllBytes(\"$k\\$n.new\", [Convert]::FromBase64String($v.Trim())); Move-Item -Force \"$k\\$n.new\" \"$k\\$n\" }}\n\
61 Remove-Variable v\nSet-Content -Path {} -Value ''\n{}",
62 powershell::quote(DELIVERED),
63 place(files),
64 k = powershell::quote(KEPT_DIR)
65 )
66}
67
68#[must_use]
72pub fn place(files: &[SecretFile]) -> String {
73 let set: String = files
74 .iter()
75 .map(|f| format!("Set-IsoloomSecret {} {}\n", powershell::quote(&f.name), powershell::quote(&path(&f.path))))
76 .collect::<Vec<_>>()
77 .concat();
78 format!(
79 "$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {}\n\
80 function Set-IsoloomSecret($name, $path) {{ New-Item -ItemType Directory -Force (Split-Path -Parent $path) | Out-Null; \
81 if (-not (Test-Path $path)) {{ New-Item -ItemType File $path | Out-Null }}; Protect-IsoloomPath $path; \
82 [IO.File]::WriteAllBytes($path, [IO.File]::ReadAllBytes((Join-Path $k $name))) }}\n{set}",
83 powershell::quote(KEPT_DIR)
84 )
85}
86
87#[must_use]
90pub fn wait(who: &str) -> String {
91 let (waiting, missing) = super::messages(who);
92 let (waiting, missing) = (powershell::quote(&waiting), powershell::quote(&missing));
93 format!(
94 "$t = (Get-Date).AddSeconds({}); $said = $false; while (-not (Test-Path {})) {{ if (-not $said) {{ Write-Output {waiting}; $said = $true }}; if ((Get-Date) -gt $t) {{ throw {missing} }}; Start-Sleep 5 }}\n",
95 super::DELIVERY_TIMEOUT,
96 powershell::quote(DELIVERED)
97 )
98}
99
100#[must_use]
103pub fn step(machine: &str, files: &[SecretFile]) -> String {
104 format!("{}{}", wait(machine), placed(files))
105}
106
107fn placed(files: &[SecretFile]) -> String {
109 format!(
110 "Set-Content -Path {script} -Value {body}\n\
111 $a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File {PLACE_SCRIPT}'; \
112 Register-ScheduledTask -TaskName '{PLACE_TASK}' -Action $a -Trigger (New-ScheduledTaskTrigger -AtStartup) -User 'SYSTEM' -RunLevel Highest -Force | Out-Null\n\
113 & powershell -NoProfile -ExecutionPolicy Bypass -File {script}; if ($LASTEXITCODE) {{ exit $LASTEXITCODE }}\n",
114 script = powershell::quote(PLACE_SCRIPT),
115 body = powershell::quote(&place(files)),
116 )
117}
118
119#[must_use]
126pub fn from_environment(values: &[(String, String)], files: &[SecretFile]) -> String {
127 let pairs = values
128 .iter()
129 .map(|(name, var)| format!("{} = {}", powershell::quote(name), powershell::quote(var)))
130 .collect::<Vec<_>>()
131 .join("; ");
132 format!(
133 "$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {k}\nNew-Item -ItemType Directory -Force $k | Out-Null\nProtect-IsoloomPath $k\n\
134 foreach ($e in @{{ {pairs} }}.GetEnumerator()) {{ $v = [Environment]::GetEnvironmentVariable($e.Value); \
135 if (-not $v -and (Test-Path \"$k\\$($e.Key)\")) {{ continue }}; \
136 $b = if ($v) {{ [Convert]::FromBase64String($v) }} else {{ [byte[]]@() }}; \
137 [IO.File]::WriteAllBytes(\"$k\\$($e.Key).new\", $b); Move-Item -Force \"$k\\$($e.Key).new\" \"$k\\$($e.Key)\" }}\n\
138 Set-Content -Path {} -Value ''\n{}",
139 powershell::quote(DELIVERED),
140 placed(files),
141 k = powershell::quote(KEPT_DIR),
142 )
143}
144
145#[must_use]
148pub fn place_first() -> String {
149 format!(
150 "& powershell -NoProfile -ExecutionPolicy Bypass -File {}; if ($LASTEXITCODE) {{ exit $LASTEXITCODE }}",
151 powershell::quote(PLACE_SCRIPT)
152 )
153}
154
155#[must_use]
158pub fn powershell_args() -> Vec<String> {
159 let bytes: Vec<u8> = BOOTSTRAP.encode_utf16().flat_map(u16::to_le_bytes).collect();
160 let mut args: Vec<String> = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand"]
161 .map(String::from)
162 .to_vec();
163 args.push(base64(&bytes));
164 args
165}
166
167#[must_use]
170pub fn command_line() -> String {
171 format!("powershell {}", powershell_args().join(" "))
172}
173
174#[must_use]
177pub fn input(names: &[String], files: &[SecretFile], values: &[u8]) -> Vec<u8> {
178 let mut out = base64(receive(names, files).as_bytes()).into_bytes();
179 out.push(b'\n');
180 out.extend_from_slice(values);
181 out
182}
183
184#[cfg(test)]
185mod tests;