use isoloom_plan::SecretFile;
use isoloom_spec::model::is_drive_path;
use isoloom_spec::shell::base64;
use crate::powershell;
pub const KEPT_DIR: &str = "C:\\ProgramData\\isoloom\\secrets";
pub const DELIVERED: &str = "C:\\ProgramData\\isoloom\\secrets\\.delivered";
pub const PLACE_SCRIPT: &str = "C:\\ProgramData\\isoloom\\apply-secrets.ps1";
pub const PLACE_TASK: &str = "isoloom-apply-secrets";
pub const BOOTSTRAP: &str = "$s = [Console]::In.ReadLine(); Invoke-Expression ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($s)))";
const PROTECT: &str = "function Protect-IsoloomPath($p) { $dir = Test-Path -PathType Container $p; \
$acl = if ($dir) { New-Object Security.AccessControl.DirectorySecurity } else { New-Object Security.AccessControl.FileSecurity }; \
$acl.SetAccessRuleProtection($true, $false); $inherit = if ($dir) { 'ContainerInherit, ObjectInherit' } else { 'None' }; \
foreach ($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'))) }; \
Set-Acl -Path $p -AclObject $acl }";
#[must_use]
pub fn path(p: &str) -> String {
if is_drive_path(p) {
p.to_owned()
} else {
format!("C:{}", p.replace('/', "\\"))
}
}
#[must_use]
pub fn receive(names: &[String], files: &[SecretFile]) -> String {
let list = names.iter().map(|n| powershell::quote(n)).collect::<Vec<_>>().join(", ");
format!(
"$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {k}\nNew-Item -ItemType Directory -Force $k | Out-Null\nProtect-IsoloomPath $k\n\
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\
Remove-Variable v\nSet-Content -Path {} -Value ''\n{}",
powershell::quote(DELIVERED),
place(files),
k = powershell::quote(KEPT_DIR)
)
}
#[must_use]
pub fn place(files: &[SecretFile]) -> String {
let set: String = files
.iter()
.map(|f| format!("Set-IsoloomSecret {} {}\n", powershell::quote(&f.name), powershell::quote(&path(&f.path))))
.collect::<Vec<_>>()
.concat();
format!(
"$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {}\n\
function Set-IsoloomSecret($name, $path) {{ New-Item -ItemType Directory -Force (Split-Path -Parent $path) | Out-Null; \
if (-not (Test-Path $path)) {{ New-Item -ItemType File $path | Out-Null }}; Protect-IsoloomPath $path; \
[IO.File]::WriteAllBytes($path, [IO.File]::ReadAllBytes((Join-Path $k $name))) }}\n{set}",
powershell::quote(KEPT_DIR)
)
}
#[must_use]
pub fn wait(who: &str) -> String {
let (waiting, missing) = super::messages(who);
let (waiting, missing) = (powershell::quote(&waiting), powershell::quote(&missing));
format!(
"$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",
super::DELIVERY_TIMEOUT,
powershell::quote(DELIVERED)
)
}
#[must_use]
pub fn step(machine: &str, files: &[SecretFile]) -> String {
format!("{}{}", wait(machine), placed(files))
}
fn placed(files: &[SecretFile]) -> String {
format!(
"Set-Content -Path {script} -Value {body}\n\
$a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -ExecutionPolicy Bypass -File {PLACE_SCRIPT}'; \
Register-ScheduledTask -TaskName '{PLACE_TASK}' -Action $a -Trigger (New-ScheduledTaskTrigger -AtStartup) -User 'SYSTEM' -RunLevel Highest -Force | Out-Null\n\
& powershell -NoProfile -ExecutionPolicy Bypass -File {script}; if ($LASTEXITCODE) {{ exit $LASTEXITCODE }}\n",
script = powershell::quote(PLACE_SCRIPT),
body = powershell::quote(&place(files)),
)
}
#[must_use]
pub fn from_environment(values: &[(String, String)], files: &[SecretFile]) -> String {
let pairs = values
.iter()
.map(|(name, var)| format!("{} = {}", powershell::quote(name), powershell::quote(var)))
.collect::<Vec<_>>()
.join("; ");
format!(
"$ErrorActionPreference = 'Stop'\n{PROTECT}\n$k = {k}\nNew-Item -ItemType Directory -Force $k | Out-Null\nProtect-IsoloomPath $k\n\
foreach ($e in @{{ {pairs} }}.GetEnumerator()) {{ $v = [Environment]::GetEnvironmentVariable($e.Value); \
if (-not $v -and (Test-Path \"$k\\$($e.Key)\")) {{ continue }}; \
$b = if ($v) {{ [Convert]::FromBase64String($v) }} else {{ [byte[]]@() }}; \
[IO.File]::WriteAllBytes(\"$k\\$($e.Key).new\", $b); Move-Item -Force \"$k\\$($e.Key).new\" \"$k\\$($e.Key)\" }}\n\
Set-Content -Path {} -Value ''\n{}",
powershell::quote(DELIVERED),
placed(files),
k = powershell::quote(KEPT_DIR),
)
}
#[must_use]
pub fn place_first() -> String {
format!(
"& powershell -NoProfile -ExecutionPolicy Bypass -File {}; if ($LASTEXITCODE) {{ exit $LASTEXITCODE }}",
powershell::quote(PLACE_SCRIPT)
)
}
#[must_use]
pub fn powershell_args() -> Vec<String> {
let bytes: Vec<u8> = BOOTSTRAP.encode_utf16().flat_map(u16::to_le_bytes).collect();
let mut args: Vec<String> = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand"]
.map(String::from)
.to_vec();
args.push(base64(&bytes));
args
}
#[must_use]
pub fn command_line() -> String {
format!("powershell {}", powershell_args().join(" "))
}
#[must_use]
pub fn input(names: &[String], files: &[SecretFile], values: &[u8]) -> Vec<u8> {
let mut out = base64(receive(names, files).as_bytes()).into_bytes();
out.push(b'\n');
out.extend_from_slice(values);
out
}
#[cfg(test)]
mod tests;