UNPKG

@heroku-cli/command

Version:
160 lines (159 loc) 6.87 kB
import { Scrubber } from '@heroku/js-blanket'; import childProcess from 'node:child_process'; /** * Handles credential storage and retrieval using the Windows Credential Manager. * Uses PowerShell commands to interact with the Windows.Security.Credentials.PasswordVault API. */ export class WindowsHandler { scrubber = new Scrubber({ patterns: [ /Retrieve\("([^"]+)",\s*"([^"]+)"\)/g, // Scrub account in Retrieve("service", "account") /PasswordCredential\("([^"]+)",\s*"([^"]+)",\s*"([^"]+)"\)/g, // Scrub account and token in PasswordCredential ], }); /** * Retrieves the authentication token from Windows Credential Manager. * @param account - The account login to use (e.g. 'test@example.com') * @param service - The service name to use * @returns The stored authentication token. * @throws Error if the token is not found or retrieval fails. */ getAuth(account, service) { try { const psCommand = ` $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = $vault.Retrieve("${service}", "${account}") $credential.Password `; const output = childProcess.execSync(psCommand, { encoding: 'utf8', shell: 'powershell', stdio: ['pipe', 'pipe', 'ignore'] }); const token = output.trim(); if (!token) { throw new Error('Token not found'); } return token; } catch (error) { const { message } = error; throw new Error(`Failed to retrieve token from Windows Credential Manager: ${this.scrubError(message)}`); } } /** * Lists all accounts stored in Windows Credential Manager for a given service. * @param service - The service name to search for * @returns Array of account names found for the service * @throws Error if the search operation fails */ listAccounts(service) { try { const psCommand = ` $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] $vault = New-Object Windows.Security.Credentials.PasswordVault try { $creds = $vault.FindAllByResource("${service}") $creds | ForEach-Object { $_.UserName } } catch { # No credentials found for this resource exit 0 } `; const output = childProcess.execSync(psCommand, { encoding: 'utf8', shell: 'powershell', stdio: ['pipe', 'pipe', 'ignore'] }); // Expected output format: // user1@example.com // user2@example.com // ... const accounts = output .split('\n') .map(line => line.trim()) .filter(line => line.length > 0); return accounts; } catch (error) { const { message } = error; throw new Error(`Failed to list accounts in Windows Credential Manager: ${this.scrubError(message)}`); } } /** * Removes the authentication token from Windows Credential Manager. * @param account - The account login to use (e.g. 'test@example.com') * @param service - The service name to use * @returns void * @throws Error if the removal operation fails. */ removeAuth(account, service) { try { const psCommand = ` $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = $vault.Retrieve("${service}", "${account}") $vault.Remove($credential) `; childProcess.execSync(psCommand, { encoding: 'utf8', shell: 'powershell', stdio: ['pipe', 'pipe', 'ignore'] }); } catch (error) { const { message } = error; if (this.isMissingVaultCredential(message)) { return; } throw new Error(`Failed to remove token from Windows Credential Manager: ${this.scrubError(message)}`); } } /** * Saves an authentication entry to Windows Credential Manager. * If a credential with the same name already exists, it is removed before saving the new one. * @param auth - The authentication entry containing account and token information to store. * @returns void * @throws Error if the save operation fails. */ saveAuth(auth) { try { try { const removeCommand = ` $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = $vault.Retrieve("${auth.service}", "${auth.account}") $vault.Remove($credential) `; childProcess.execSync(removeCommand, { encoding: 'utf8', shell: 'powershell', stdio: ['pipe', 'pipe', 'ignore'] }); } catch { // noop - item does not exist } const addCommand = ` $ErrorActionPreference = 'Stop' [void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] $vault = New-Object Windows.Security.Credentials.PasswordVault $credential = New-Object Windows.Security.Credentials.PasswordCredential("${auth.service}", "${auth.account}", "${auth.token}") $vault.Add($credential) `; childProcess.execSync(addCommand, { encoding: 'utf8', shell: 'powershell', stdio: ['pipe', 'pipe', 'ignore'] }); } catch (error) { const { message } = error; throw new Error(`Failed to store token in Windows Credential Manager: ${this.scrubError(message)}`); } } /** * PasswordVault.Retrieve throws when the credential is absent (e.g. netrc-only login). */ isMissingVaultCredential(message) { const lower = message.toLowerCase(); return lower.includes('element not found') || lower.includes('specified credential could not be found') || lower.includes('0x80070490'); } /** * Scrubs account names and passwords/tokens from error messages. * * @param message - The error message to scrub * @returns The scrubbed error message with sensitive data replaced by "[SCRUBBED]" */ scrubError(message) { const result = this.scrubber.scrub({ message }); return result.data.message; } }