$ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $credentialTarget = 'D3ROVoice-Android-Upload-20260821' $keystorePath = 'C:\Users\encep\.d3ro\release\d3ro-upload-key-20260821.p12' $expectedAlias = 'd3ro-upload-20260821' $releaseIdentity = Get-Content -Raw -LiteralPath 'release\android-release-identity.json' | ConvertFrom-Json $expectedSha256 = $releaseIdentity.uploadCertificateSha256 if (-not (Test-Path -LiteralPath $keystorePath -PathType Leaf)) { throw "Android upload keystore is missing: $keystorePath" } Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public static class D3ROCredentialReader { [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct Credential { public UInt32 Flags; public UInt32 Type; public string TargetName; public string Comment; public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; public UInt32 CredentialBlobSize; public IntPtr CredentialBlob; public UInt32 Persist; public UInt32 AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName; } [DllImport("advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)] public static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential); [DllImport("advapi32.dll", SetLastError = true)] public static extern void CredFree(IntPtr credential); } '@ $credentialPointer = [IntPtr]::Zero $password = $null try { if (-not [D3ROCredentialReader]::CredRead($credentialTarget, 1, 0, [ref]$credentialPointer)) { $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() throw "Windows credential is unavailable: $credentialTarget (Win32 $errorCode)" } $credential = [Runtime.InteropServices.Marshal]::PtrToStructure( $credentialPointer, [type][D3ROCredentialReader+Credential] ) if ($credential.CredentialBlobSize -eq 0) { throw "Windows credential has an empty secret: $credentialTarget" } $blob = [byte[]]::new($credential.CredentialBlobSize) [Runtime.InteropServices.Marshal]::Copy($credential.CredentialBlob, $blob, 0, $blob.Length) $password = [Text.Encoding]::Unicode.GetString($blob).TrimEnd([char]0) if ($password.Length -lt 20) { throw 'Stored Android upload key password fails the minimum length policy.' } $env:D3RO_UPLOAD_KEY_PASSWORD_CHECK = $password $keytoolOutput = & keytool.exe -list -v ` -storetype PKCS12 ` -keystore $keystorePath ` -storepass:env D3RO_UPLOAD_KEY_PASSWORD_CHECK 2>&1 if ($LASTEXITCODE -ne 0) { throw "keytool could not read the Android upload keystore: $keytoolOutput" } $rendered = $keytoolOutput -join "`n" if ($rendered -notmatch [regex]::Escape($expectedAlias)) { throw "Android upload key alias mismatch. Expected $expectedAlias." } if ($rendered -notmatch 'PrivateKeyEntry') { throw 'Android upload keystore does not contain a private key entry.' } if ($rendered -notmatch [regex]::Escape($expectedSha256)) { throw "Android upload certificate mismatch. Expected $expectedSha256." } [pscustomobject]@{ ok = $true alias = $expectedAlias certificateSha256 = $expectedSha256 keystoreBytes = (Get-Item -LiteralPath $keystorePath).Length privateKeyReadable = $true credentialTargetPresent = $true } | ConvertTo-Json } finally { Remove-Item Env:D3RO_UPLOAD_KEY_PASSWORD_CHECK -ErrorAction SilentlyContinue $password = $null if ($credentialPointer -ne [IntPtr]::Zero) { [D3ROCredentialReader]::CredFree($credentialPointer) } }