Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
filters: |
nvm:
- 'src/**'
- '.github/**'
github:
- '.github/**'
- 'LICENSE'
Expand Down Expand Up @@ -127,7 +128,7 @@ jobs:
- name: Checkout common modules
shell: pwsh
run: |
$dest = Join-Path (Split-Path (Split-Path $env:GITHUB_WORKSPACE -Parent) -Parent) "common"
$dest = Join-Path (Split-Path $env:GITHUB_WORKSPACE -Parent) "common"
if (-not (Test-Path (Join-Path $dest "settings\go.mod"))) {
git clone --depth 1 https://github.com/nvm-windows/common.git $dest
}
Expand Down
264 changes: 264 additions & 0 deletions src/bootstrap/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
//go:build windows

package bootstrap

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"

prefs "common/preferences"
"common/registry"
)

const (
CommunityAppxName = "AuthorSoftware.NVMWindows"
CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified"
identityMsixName = "NVMWindows.msix"
retiredValueName = "CommunityIdentityRetired"
registeredValueName = "CertifiedIdentityRegistered"
certifiedRetiredValueName = "CertifiedIdentityRetired"
)

var runAppxCommand = func(command string) error {
ps := filepath.Join(os.Getenv("SystemRoot"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
cmd := exec.Command(ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
}
return nil
}

// RegisterSparseIdentity registers the per-user identity .msix against programRoot.
// Failure is non-fatal for callers; toasts fall back to AUMID-only.
func RegisterSparseIdentity(programRoot string) error {
programRoot = filepath.Clean(strings.TrimSpace(programRoot))
if programRoot == "" {
return fmt.Errorf("program root is empty")
}
msix := filepath.Join(programRoot, "identity", identityMsixName)
if _, err := os.Stat(msix); err != nil {
return err
}
cmd := fmt.Sprintf(
"Add-AppxPackage -Path %s -ExternalLocation %s -ErrorAction Stop",
psSingleQuote(msix),
psSingleQuote(programRoot),
)
return runAppxCommand(cmd)
}

// ProvisionSparseIdentity registers the identity for all users (elevated MSI).
func ProvisionSparseIdentity(programRoot string) error {
programRoot = filepath.Clean(strings.TrimSpace(programRoot))
if programRoot == "" {
return fmt.Errorf("program root is empty")
}
msix := filepath.Join(programRoot, "identity", identityMsixName)
if _, err := os.Stat(msix); err != nil {
return err
}
cmd := fmt.Sprintf(
"$msix = %s; $root = %s; Add-AppxPackage -Path $msix -ExternalLocation $root -AllUsers -ErrorAction SilentlyContinue; if (-not $?) { Add-AppxPackage -Path $msix -ExternalLocation $root -ErrorAction SilentlyContinue }; try { Add-AppxProvisionedPackage -Online -PackagePath $msix -SkipLicense -ErrorAction SilentlyContinue | Out-Null } catch {}",
psSingleQuote(msix),
psSingleQuote(programRoot),
)
return runAppxCommand(cmd)
}

// DeprovisionSparseIdentity drops per-user, all-user, and provisioned packages.
func DeprovisionSparseIdentity(name string) error {
name = strings.TrimSpace(name)
if name == "" {
name = CertifiedAppxName
}
cmd := fmt.Sprintf(
"$n = %s; Get-AppxPackage -AllUsers -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue; Get-AppxPackage -Name $n -ErrorAction SilentlyContinue | Remove-AppxPackage -ErrorAction SilentlyContinue; Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -eq $n } | ForEach-Object { Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction SilentlyContinue }",
psSingleQuote(name),
)
err := runAppxCommand(cmd)
if strings.EqualFold(name, CertifiedAppxName) {
_ = clearCertifiedIdentityRegistered()
}
return err
}

// UnregisterSparseIdentity removes a sparse identity package. Missing package is success.
func UnregisterSparseIdentity(name string) error {
name = strings.TrimSpace(name)
if name == "" {
name = CommunityAppxName
}
cmd := fmt.Sprintf(
"Get-AppxPackage -Name %s | Remove-AppxPackage -ErrorAction SilentlyContinue",
psSingleQuote(name),
)
err := runAppxCommand(cmd)
if strings.EqualFold(name, CertifiedAppxName) {
_ = clearCertifiedIdentityRegistered()
}
return err
}

// MaybeRemoveStaleCommunityIdentity drops a leftover community Appx after
// community→certified when the MSI impersonated CA ran as SYSTEM.
func MaybeRemoveStaleCommunityIdentity() {
root, err := ProgramRoot()
if err != nil || !isMachineProgramRoot(root) {
return
}
if communityIdentityRetired() {
return
}
_ = UnregisterSparseIdentity(CommunityAppxName)
_ = markCommunityIdentityRetired()
}

// MaybeRegisterCertifiedIdentity registers the Program Files identity .msix once
// per user when the MSI impersonated CA ran as SYSTEM.
func MaybeRegisterCertifiedIdentity() {
root, err := ProgramRoot()
if err != nil || !isMachineProgramRoot(root) {
return
}
if certifiedIdentityRegistered() {
return
}
if err := RegisterSparseIdentity(root); err != nil {
return
}
_ = markCertifiedIdentityRegistered()
_ = clearCertifiedIdentityRetired()
}

// MaybeRemoveStaleCertifiedIdentity drops leftover Certified Appx after
// certified→community when MSI deprovision ran as SYSTEM. Leaves Certified
// identity alone if Program Files nvm.exe is still present.
func MaybeRemoveStaleCertifiedIdentity() {
root, err := ProgramRoot()
if err != nil || !shouldRemoveStaleCertifiedIdentity(root, certifiedProgramInstalled()) {
return
}
if certifiedIdentityRetired() {
return
}
_ = UnregisterSparseIdentity(CertifiedAppxName)
_ = markCertifiedIdentityRetired()
}

func shouldRemoveStaleCertifiedIdentity(programRoot string, certifiedStillInstalled bool) bool {
if isMachineProgramRoot(programRoot) {
return false
}
return !certifiedStillInstalled
}

func certifiedProgramInstalled() bool {
for _, p := range certifiedNvmExePaths() {
if _, err := os.Stat(p); err == nil {
return true
}
}
return false
}

func certifiedNvmExePaths() []string {
var out []string
if pf := strings.TrimSpace(os.Getenv("ProgramFiles")); pf != "" {
out = append(out, filepath.Join(pf, "Author Software", "nvm", "nvm.exe"))
}
if pf := strings.TrimSpace(os.Getenv("ProgramFiles(x86)")); pf != "" {
out = append(out, filepath.Join(pf, "Author Software", "nvm", "nvm.exe"))
}
return out
}

func isMachineProgramRoot(root string) bool {
low := strings.ToLower(filepath.Clean(root))
return strings.Contains(low, `\program files\`) || strings.Contains(low, `\program files (x86)\`)
}

func communityIdentityRetired() bool {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return false
}
v, exists, err := registry.GetBool(root + "/" + retiredValueName)
if err != nil || !exists {
return false
}
return v
}

func markCommunityIdentityRetired() error {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return nil
}
return registry.PutBool(true, root+"/"+retiredValueName)
}

func certifiedIdentityRegistered() bool {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return false
}
v, exists, err := registry.GetBool(root + "/" + registeredValueName)
if err != nil || !exists {
return false
}
return v
}

func markCertifiedIdentityRegistered() error {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return nil
}
return registry.PutBool(true, root+"/"+registeredValueName)
}

func clearCertifiedIdentityRegistered() error {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return nil
}
return registry.Del(root + "/" + registeredValueName)
}

func certifiedIdentityRetired() bool {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return false
}
v, exists, err := registry.GetBool(root + "/" + certifiedRetiredValueName)
if err != nil || !exists {
return false
}
return v
}

func markCertifiedIdentityRetired() error {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return nil
}
return registry.PutBool(true, root+"/"+certifiedRetiredValueName)
}

func clearCertifiedIdentityRetired() error {
root := strings.TrimSpace(prefs.ROOT)
if root == "" {
return nil
}
return registry.Del(root + "/" + certifiedRetiredValueName)
}

func psSingleQuote(path string) string {
return "'" + strings.ReplaceAll(path, "'", "''") + "'"
}
22 changes: 22 additions & 0 deletions src/bootstrap/identity_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build !windows

package bootstrap

const (
CommunityAppxName = "AuthorSoftware.NVMWindows"
CertifiedAppxName = "AuthorSoftware.NVMWindows.Certified"
)

func RegisterSparseIdentity(string) error { return nil }

func ProvisionSparseIdentity(string) error { return nil }

func DeprovisionSparseIdentity(string) error { return nil }

func UnregisterSparseIdentity(string) error { return nil }

func MaybeRemoveStaleCommunityIdentity() {}

func MaybeRegisterCertifiedIdentity() {}

func MaybeRemoveStaleCertifiedIdentity() {}
Loading
Loading