using System.Security.Cryptography;
using System.Text;
namespace LlamaApp.Common;
/// Why an install.ps1 response may be refused.
public enum InstallScriptVerdict
{
/// The response is acceptable.
Ok,
/// A non-200 response (the server rejected the request).
HttpError,
/// A 3xx response — redirects are not followed, so the script never runs.
Redirected,
/// The response did not come from the pinned HTTPS URL/host/path.
WrongUri,
/// The advertised (or actual) body exceeds .
TooLarge,
}
///
/// Provenance policy for the downloaded install.ps1. The app executes
/// that script straight from memory (PowerShell -EncodedCommand — it is
/// never written to disk), so a compromised endpoint, DNS answer, or
/// system-trusted TLS-inspection proxy would otherwise be arbitrary code
/// execution. This type centralizes the checks in one pure, unit-testable
/// place:
///
///
/// - the response must be a direct 200 from exactly
/// https://llama.app/install.ps1 — redirects are surfaced, not
/// followed, so a 30x to another host is refused;
/// - the body is bounded to and its SHA-256 is
/// recorded for audit.
///
///
/// There is deliberately no hash pin: install.ps1 is revised
/// independently of the app, so a build-time pin goes stale and blocks
/// runtime upgrades (attended and unattended alike). The exact-URL,
/// no-redirect, bounded-size policy above is the standing gate; the logged
/// hash keeps each install auditable after the fact.
///
public static class InstallScriptIntegrity
{
/// The one URL the installer may be fetched from.
public const string ExpectedScriptUrl = "https://llama.app/install.ps1";
/// Expected host (case-insensitive).
public const string ExpectedHost = "llama.app";
/// Expected absolute path.
public const string ExpectedPath = "/install.ps1";
///
/// Hard cap on the downloaded script. The real file is a few KB; anything
/// larger is either an attack or an endpoint error.
///
public const int MaxBytes = 1 * 1024 * 1024;
///
/// Maximum base64 payload characters that may ride on the PowerShell
/// command line. Windows caps a process command line (exe + arguments) at
/// 32,767 characters (CreateProcess), and the payload expands to
/// ~2.67 chars per script character (UTF-16 → base64), so this bounds the
/// script to roughly 11,000 characters — comfortably above the real
/// install.ps1 (a few KB). A larger script is refused rather than
/// spilled to disk: the app never persists the script anywhere.
///
public const int MaxEncodedCommandChars = 30_000;
/// True when the verdict allows the script to run.
public static bool IsOk(InstallScriptVerdict verdict) => verdict == InstallScriptVerdict.Ok;
///
/// Validates a response line for the installer fetch.
/// is the URI the response actually came from (equal to the request URI when
/// redirects are disabled).
///
public static InstallScriptVerdict ValidateResponse(int statusCode, Uri? finalUri, long? contentLength)
{
if (statusCode is >= 300 and < 400) return InstallScriptVerdict.Redirected;
if (statusCode != 200) return InstallScriptVerdict.HttpError;
if (finalUri is null || !IsExpectedUri(finalUri)) return InstallScriptVerdict.WrongUri;
if (contentLength is { } len && (len <= 0 || len > MaxBytes)) return InstallScriptVerdict.TooLarge;
return InstallScriptVerdict.Ok;
}
///
/// True only for HTTPS on (default port) with
/// exactly .
///
public static bool IsExpectedUri(Uri uri)
{
if (!uri.IsAbsoluteUri) return false;
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) return false;
if (!string.Equals(uri.Host, ExpectedHost, StringComparison.OrdinalIgnoreCase)) return false;
if (!uri.IsDefaultPort) return false;
if (uri.Query.Length != 0 || uri.Fragment.Length != 0) return false;
return string.Equals(uri.AbsolutePath, ExpectedPath, StringComparison.Ordinal);
}
/// Lower-case hex SHA-256 of .
public static string Sha256Hex(byte[] data)
=> Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant();
///
/// Prepares the downloaded script bytes for fileless execution: decodes
/// them to text (BOM-aware UTF-8 — the encoding the script is published
/// in) and returns the base64-encoded UTF-16LE form that PowerShell's
/// -EncodedCommand expects. The script therefore never touches the
/// disk — it travels from the validated response through memory into the
/// interpreter — and every install fetches a fresh copy (nothing is
/// cached between runs). Throws when
/// the encoded payload would exceed
/// (the Windows command-line limit) rather than falling back to a temp
/// file: refusing oversized input is safer than materializing it.
///
public static string EncodeCommand(byte[] scriptBytes)
{
string script;
using (var reader = new StreamReader(
new MemoryStream(scriptBytes), Encoding.UTF8,
detectEncodingFromByteOrderMarks: true))
{
script = reader.ReadToEnd();
}
var encoded = Convert.ToBase64String(Encoding.Unicode.GetBytes(script));
if (encoded.Length > MaxEncodedCommandChars)
throw new InvalidDataException(
$"install.ps1 is too large to execute in memory ({encoded.Length} > {MaxEncodedCommandChars} encoded characters). " +
"Refusing to write it to disk.");
return encoded;
}
///
/// Reads into memory, refusing to buffer more
/// than . Keeps a hostile/oversized response
/// from exhausting memory before it is ever executed.
///
public static async Task ReadBoundedAsync(
Stream stream, int maxBytes = MaxBytes, CancellationToken cancel = default)
{
using var buffer = new MemoryStream();
var chunk = new byte[16 * 1024];
int read;
while ((read = await stream.ReadAsync(chunk, cancel)) > 0)
{
if (buffer.Length + read > maxBytes)
throw new InvalidDataException($"install.ps1 exceeds the {maxBytes}-byte limit.");
buffer.Write(chunk, 0, read);
}
return buffer.ToArray();
}
/// Human-readable reason for refusing a script.
public static string Describe(InstallScriptVerdict verdict) => verdict switch
{
InstallScriptVerdict.HttpError => "the server returned a non-200 response",
InstallScriptVerdict.Redirected => "the server redirected the download (redirects are refused)",
InstallScriptVerdict.WrongUri => $"the response did not come from {ExpectedScriptUrl}",
InstallScriptVerdict.TooLarge => $"the script exceeds the {MaxBytes}-byte limit",
_ => "the response was accepted",
};
}