Class: Gem::CredentialStore::WindowsBackend
| Relationships & Source Files | |
| Inherits: | Object |
| Defined in: | lib/rubygems/credential_store/native/windows.rb |
Overview
Stores credentials in the Windows Credential Manager via the
Windows.Security.Credentials.PasswordVault WinRT API, driven from
PowerShell. Account/service/secret values are passed as environment
variables rather than interpolated into the script text, so no quoting
scheme is needed and values cannot break out of the script.
Windows PowerShell is used rather than PowerShell 7 (pwsh) because the
WinRT projection used here is not reliably available under pwsh. It is
spawned as powershell rather than powershell.exe, the way this codebase
spawns git, so PATHEXT resolves it. That also lets the tests put a shim
ahead of it on Windows, where a file with a shebang is not executable.
Constant Summary
-
LOAD_VAULT_TYPE =
private
# File 'lib/rubygems/credential_store/native/windows.rb', line 21<<~POWERSHELL $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] POWERSHELL
Class Method Summary
- .delete(service, account)
- .delete_all(service)
- .get(service, account)
-
.list(service)
FindAllByResource raises when the resource has no entries, which is an empty list rather than an error.
- .set(service, account, secret)
- .missing_credential?(message) ⇒ Boolean private
- .run(script, service, account, secret = nil) private
Class Method Details
.delete(service, account)
[ GitHub ]# File 'lib/rubygems/credential_store/native/windows.rb', line 69
def self.delete(service, account) script = <<~POWERSHELL #{LOAD_VAULT_TYPE} $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT) $vault.Remove($credential) POWERSHELL _out, err, status = run(script, service, account) status.success? || missing_credential?(err) end
.delete_all(service)
[ GitHub ]# File 'lib/rubygems/credential_store/native/windows.rb', line 102
def self.delete_all(service) script = <<~POWERSHELL #{LOAD_VAULT_TYPE} $vault = New-Object Windows.Security.Credentials.PasswordVault try { $vault.FindAllByResource($env:RUBYGEMS_CRED_SERVICE) | ForEach-Object { $vault.Remove($_) } } catch { if (-not ($_.Exception.Message -match 'not found|0x80070490')) { throw } } POWERSHELL _out, err, status = run(script, service, nil) status.success? || missing_credential?(err) end
.get(service, account)
[ GitHub ]# File 'lib/rubygems/credential_store/native/windows.rb', line 27
def self.get(service, account) script = <<~POWERSHELL #{LOAD_VAULT_TYPE} $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT) # Emitting into the pipeline would send the string through PowerShell's # formatter, which wraps at the host width and would corrupt a secret. [Console]::Out.Write($credential.Password) POWERSHELL out, err, status = run(script, service, account) # An absent credential is ordinary. Any other failure is not, so raise and # let the wrapper report why rather than authenticating without one. unless status.success? return nil if missing_credential?(err) raise "powershell exited with #{status.exitstatus}: #{err.strip}" end secret = out.chomp secret.empty? ? nil : secret end
.list(service)
FindAllByResource raises when the resource has no entries, which is an empty list rather than an error.
# File 'lib/rubygems/credential_store/native/windows.rb', line 83
def self.list(service) script = <<~POWERSHELL #{LOAD_VAULT_TYPE} $vault = New-Object Windows.Security.Credentials.PasswordVault try { $vault.FindAllByResource($env:RUBYGEMS_CRED_SERVICE) | ForEach-Object { [Console]::Out.WriteLine($_.UserName) } } catch { if (-not ($_.Exception.Message -match 'not found|0x80070490')) { throw } } POWERSHELL out, _err, status = run(script, service, nil) return [] unless status.success? out.split("\n").map(&:chomp).reject(&:empty?).uniq end
.missing_credential?(message) ⇒ Boolean (private)
# File 'lib/rubygems/credential_store/native/windows.rb', line 125
def self.missing_credential?() text = .to_s.downcase text.include?("element not found") || text.include?("0x80070490") || text.include?("could not be found") end
.run(script, service, account, secret = nil) (private)
[ GitHub ]# File 'lib/rubygems/credential_store/native/windows.rb', line 117
def self.run(script, service, account, secret = nil) env = { "RUBYGEMS_CRED_SERVICE" => service, "RUBYGEMS_CRED_ACCOUNT" => account } env["RUBYGEMS_CRED_SECRET"] = secret if secret Open3.capture3(env, "powershell", "-NoProfile", "-NonInteractive", "-Command", "-", stdin_data: script) end
.set(service, account, secret)
[ GitHub ]# File 'lib/rubygems/credential_store/native/windows.rb', line 50
def self.set(service, account, secret) script = <<~POWERSHELL #{LOAD_VAULT_TYPE} $vault = New-Object Windows.Security.Credentials.PasswordVault try { $existing = $vault.Retrieve($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT) $vault.Remove($existing) } catch {} $credential = New-Object Windows.Security.Credentials.PasswordCredential($env:RUBYGEMS_CRED_SERVICE, $env:RUBYGEMS_CRED_ACCOUNT, $env:RUBYGEMS_CRED_SECRET) $vault.Add($credential) POWERSHELL _out, err, status = run(script, service, account, secret) return true if status.success? # Raise so the reason reaches the user; see MacOSBackend.set. raise "powershell exited with #{status.exitstatus}: #{err.strip}" end