Public Access
Take the release credentials off the command line, where anyone could read them
Three scripts prompted for a secret with Read-Host -AsSecureString or read -s, said in a comment that it therefore never lands in shell history, and then wrote it straight into the arguments of the next process. The comment was true and was answering the wrong question. A command line is not private: on Windows one call to Get-CimInstance Win32_Process hands every argument of every process running as this user to anything that asks, /proc/<pid>/cmdline is world-readable on Linux, and process-creation auditing and EDR agents keep their own copy either way. The values were the Android release signing key's passwords and the forge token — between them the whole capability to ship an update that every installed copy will fetch and run, which ADR 0011 rule 1 and ADR 0013 go to some length to keep off a runner and which was then left in a process listing on a desk. Both replacements are documented features of the tools rather than tricks. The Android passwords use .NET for Android's env: prefix, which puts the name of a variable on the command line and lets MSBuild read the value out of the environment itself. Set immediately before the build and cleared in a finally, so a failed signing does not leave them in the shell for whatever runs next, and -nodeReuse:false so no persistent MSBuild worker outlives the call still holding the inherited copy in its own environment. That prefix is unsupported when AndroidPackageFormat is aab, and the failure is silent — the package would be signed with the literal string "env:DODOSSH_ANDROID_STOREPASS" as its password. So the format is read back and the build stops if it is ever changed, in the same shape as the versionCode guard a few lines above: a wrong release refused here rather than discovered later on somebody's phone. vpk documents VPK_TOKEN as --token's environment fallback, so the flag is simply gone from both desktop scripts and the token never becomes an argument at all. Unset afterwards on the failure path too, which on macOS needs the exit status captured rather than a bare || stop_with. release-macos.sh had the same fault and was missed when the other two were first reported. Its Apple credentials were already right — a notarytool keychain profile rather than --password — and only the forge token was exposed. What this does not claim: an environment block is readable by a process holding rights over this one, the same as its memory is. The comments say so rather than implying the values are now safe. What is bought is real and narrower — off the command line, out of every process listing, out of the audit trail, and out of any log that echoes the invocation. Both PowerShell files parse, all three shell scripts pass bash -n, the AndroidPackageFormat probe evaluates and answers empty so the guard passes, and dotnet build accepts -nodeReuse:false. The signing itself is not exercised here: it needs the release keystore, which by design is not on this machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
483ae1fed9
commit
597e5ae016
3 files changed
+114
-20
No files matched your search
+60
-10
@@ -169,16 +169,66 @@ Bump ApplicationVersion, commit, move the tag, and run this again.
|
||||
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
|
||||
New-Item -ItemType Directory -Path $Staging | Out-Null
|
||||
|
||||
# No RuntimeIdentifier, so every supported ABI is packaged. An arm64-only APK will not install on a
|
||||
# 32-bit handset or an x86_64 emulator, and this is the package strangers are pointed at.
|
||||
& dotnet build $Project --no-restore --configuration Release `
|
||||
-t:SignAndroidPackage `
|
||||
-p:DodoChannel=release `
|
||||
-p:AndroidKeyStore=true `
|
||||
-p:AndroidSigningKeyStore=$KeystorePath `
|
||||
-p:AndroidSigningKeyAlias=$KeyAlias `
|
||||
-p:AndroidSigningStorePass=$storePlain `
|
||||
-p:AndroidSigningKeyPass=$keyPlain
|
||||
# ◆ THE PASSWORDS GO THROUGH THE ENVIRONMENT, BECAUSE -p: PUTS THEM ON A COMMAND LINE.
|
||||
#
|
||||
# They used to be written straight into -p:AndroidSigningStorePass=..., which reads as private and is
|
||||
# not. A process's command line is not a secret on Windows: any process running as this user can read
|
||||
# every argument of every other with one call —
|
||||
#
|
||||
# Get-CimInstance Win32_Process | Select-Object Name, CommandLine
|
||||
#
|
||||
# — and process-creation auditing, EDR agents and crash dumps all record it as a matter of course. The
|
||||
# comment above says the passwords are never stored, and with the arguments written that way it was
|
||||
# true of the disk and false of everything watching the machine.
|
||||
#
|
||||
# env: is .NET for Android's own answer to exactly this, and it names the variable rather than carrying
|
||||
# the value: MSBuild is handed the string "env:DODOSSH_ANDROID_STOREPASS" and reads the password out of
|
||||
# the environment itself. The one restriction is $(AndroidPackageFormat) = aab, where env: is not
|
||||
# supported and file: is the documented alternative; this channel ships an APK, and the guard below
|
||||
# fails loudly rather than silently signing with the literal string if that ever changes.
|
||||
#
|
||||
# Not a perfect channel — an environment block is readable by a process with rights over this one, the
|
||||
# same as memory is — but it is off the command line, out of the audit trail and out of any log that
|
||||
# echoes the build invocation, which is the whole of the difference being bought here.
|
||||
#
|
||||
# -nodeReuse:false so no MSBuild worker outlives this call holding the variables in its environment.
|
||||
# The finally clears them from this process either way, and a persistent node would have kept its own
|
||||
# inherited copy for the next quarter of an hour regardless of what happens here.
|
||||
$packageFormat = (& dotnet msbuild $Project -getProperty:AndroidPackageFormat -nologo `
|
||||
-p:DodoChannel=release | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
||||
|
||||
if ($packageFormat -eq 'aab') {
|
||||
Stop-With @'
|
||||
This channel now builds an aab, and the env: prefix this script signs with is not supported for that
|
||||
format. The signing would silently take "env:DODOSSH_ANDROID_STOREPASS" for the password itself.
|
||||
|
||||
Switch both passwords to the file: prefix, which is supported for aab, and make sure whatever writes
|
||||
that file removes it again.
|
||||
'@
|
||||
}
|
||||
|
||||
try {
|
||||
$env:DODOSSH_ANDROID_STOREPASS = $storePlain
|
||||
$env:DODOSSH_ANDROID_KEYPASS = $keyPlain
|
||||
|
||||
# No RuntimeIdentifier, so every supported ABI is packaged. An arm64-only APK will not install on a
|
||||
# 32-bit handset or an x86_64 emulator, and this is the package strangers are pointed at.
|
||||
& dotnet build $Project --no-restore --configuration Release `
|
||||
-nodeReuse:false `
|
||||
-t:SignAndroidPackage `
|
||||
-p:DodoChannel=release `
|
||||
-p:AndroidKeyStore=true `
|
||||
-p:AndroidSigningKeyStore=$KeystorePath `
|
||||
-p:AndroidSigningKeyAlias=$KeyAlias `
|
||||
-p:AndroidSigningStorePass=env:DODOSSH_ANDROID_STOREPASS `
|
||||
-p:AndroidSigningKeyPass=env:DODOSSH_ANDROID_KEYPASS
|
||||
}
|
||||
finally {
|
||||
# SetEnvironmentVariable rather than Remove-Item, which throws when the variable is not there —
|
||||
# and this runs on the failure path, where it may well not be.
|
||||
[Environment]::SetEnvironmentVariable('DODOSSH_ANDROID_STOREPASS', $null)
|
||||
[Environment]::SetEnvironmentVariable('DODOSSH_ANDROID_KEYPASS', $null)
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { Stop-With 'The build failed.' }
|
||||
|
||||
|
||||
@@ -38,9 +38,11 @@
|
||||
# xcrun notarytool store-credentials DodoSSH \
|
||||
# --apple-id you@example.com --team-id TEAMID --password <app-specific-password>
|
||||
#
|
||||
# The forge token is the one real secret, and it is prompted for rather than read from a file or the
|
||||
# environment, and only in the phase that needs it — for the reason the Windows script gives: the fewer
|
||||
# minutes a credential that can publish an update spends in a shell's memory the better.
|
||||
# The forge token is the one real secret, and it is prompted for rather than read from a file or an
|
||||
# ambient environment variable, and only in the phase that needs it — for the reason the Windows script
|
||||
# gives: the fewer minutes a credential that can publish an update spends in a shell's memory the better.
|
||||
# It reaches vpk through VPK_TOKEN, exported for that one call and unset afterwards, because the
|
||||
# alternative is --token on a command line that is not private. See the comment at the upload itself.
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/release-macos.sh
|
||||
@@ -195,10 +197,18 @@ if [ "$UPLOAD" -eq 1 ]; then
|
||||
# also already hold the Windows package for the same tag, which is the case --merge is really doing
|
||||
# the work for: without it the second platform to publish a given version fails on a release that
|
||||
# exists, and with it the two sit side by side under one tag. --channel keeps the indexes apart.
|
||||
#
|
||||
# No --token in this array, and that is deliberate. It used to be there, and an argument is not a
|
||||
# private place to put a credential: /proc/<pid>/cmdline is world-readable on Linux, `ps` hands out
|
||||
# other processes' arguments freely, and whatever agent is auditing process creation on this machine
|
||||
# keeps its own copy. The comment above is about shell history and was never the whole story.
|
||||
#
|
||||
# vpk documents VPK_TOKEN as --token's environment fallback, so the value never becomes an argument.
|
||||
# Exported for the one call and unset straight after; an environment block is still readable by a
|
||||
# process with rights over this one, but it is out of every process listing and audit log.
|
||||
UPLOAD_ARGS=(
|
||||
upload gitea
|
||||
--repoUrl "$REPO_URL"
|
||||
--token "$TOKEN"
|
||||
--outputDir "$RELEASES_DIR"
|
||||
--channel "$CHANNEL"
|
||||
--releaseName "$TAG"
|
||||
@@ -214,7 +224,15 @@ if [ "$UPLOAD" -eq 1 ]; then
|
||||
esac
|
||||
|
||||
write_step 'Uploading'
|
||||
dotnet vpk "${UPLOAD_ARGS[@]}" || stop_with 'vpk upload failed.'
|
||||
|
||||
# Unset on the way out whether or not the upload worked, so a failed release does not leave the token
|
||||
# sitting in this shell for whatever runs next.
|
||||
export VPK_TOKEN="$TOKEN"
|
||||
vpk_status=0
|
||||
dotnet vpk "${UPLOAD_ARGS[@]}" || vpk_status=$?
|
||||
unset VPK_TOKEN
|
||||
|
||||
[ "$vpk_status" -eq 0 ] || stop_with 'vpk upload failed.'
|
||||
|
||||
write_step "Published $TAG."
|
||||
exit 0
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
2. With -Upload: asks for the forge token and publishes what phase 1 produced. It does not rebuild,
|
||||
so the bytes that reach users are the bytes that were installed and checked.
|
||||
|
||||
The token is prompted for rather than read from a file or an environment variable, and only in the phase
|
||||
that needs it — the build does not, and the fewer minutes a credential that can publish an update spends
|
||||
in a shell's memory the better.
|
||||
The token is prompted for rather than read from a file or an ambient environment variable, and only in
|
||||
the phase that needs it — the build does not, and the fewer minutes a credential that can publish an
|
||||
update spends in a shell's memory the better. It reaches vpk through VPK_TOKEN, set for that one call
|
||||
and cleared afterwards, because the alternative is --token on a command line every process on this
|
||||
machine can read. See the comment at the upload itself.
|
||||
|
||||
.PARAMETER Upload
|
||||
Publish the packages already in Releases/ instead of building.
|
||||
@@ -152,10 +154,24 @@ try {
|
||||
# --merge because Gitea already has a release entry for the pushed tag, and without it the upload
|
||||
# fails on a release that exists. --pre mirrors the rule the docker image job already applies to the
|
||||
# same tag, so a release candidate is a prerelease in both channels or in neither.
|
||||
#
|
||||
# ◆ NO --token HERE. IT WAS AN ARGUMENT, AND AN ARGUMENT IS PUBLIC ON THIS MACHINE.
|
||||
#
|
||||
# The comment above says the token never lands in shell history, which was true and was not the
|
||||
# whole story: it went straight into vpk's command line, and any process running as this user can
|
||||
# read that off a running process with one call —
|
||||
#
|
||||
# Get-CimInstance Win32_Process | Select-Object Name, CommandLine
|
||||
#
|
||||
# — with process-creation auditing and EDR agents keeping their own copy afterwards. A token that
|
||||
# can publish an update every installed copy will fetch and run deserves better than that.
|
||||
#
|
||||
# vpk documents VPK_TOKEN as --token's environment fallback, so the flag simply goes away and the
|
||||
# value never becomes an argument at all. An environment block is still readable by a process with
|
||||
# rights over this one; what it is not is a string sitting in a process listing and an audit log.
|
||||
$uploadArgs = @(
|
||||
'upload', 'gitea',
|
||||
'--repoUrl', $RepoUrl,
|
||||
'--token', $token,
|
||||
'--outputDir', $ReleasesDir,
|
||||
'--channel', $Channel,
|
||||
'--releaseName', $tag,
|
||||
@@ -169,7 +185,17 @@ try {
|
||||
}
|
||||
|
||||
Write-Step 'Uploading'
|
||||
& dotnet vpk @uploadArgs
|
||||
|
||||
try {
|
||||
$env:VPK_TOKEN = $token
|
||||
& dotnet vpk @uploadArgs
|
||||
}
|
||||
finally {
|
||||
# SetEnvironmentVariable rather than Remove-Item, which throws when the variable is not there —
|
||||
# and this runs on the failure path, where it may well not be.
|
||||
[Environment]::SetEnvironmentVariable('VPK_TOKEN', $null)
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) { Stop-With 'vpk upload failed.' }
|
||||
|
||||
Write-Step "Published $tag."
|
||||
|
||||
Reference in new issue
Block a user