(data: SecureStorageData)
| 95 | return promise |
| 96 | }, |
| 97 | update(data: SecureStorageData): { success: boolean; warning?: string } { |
| 98 | // Invalidate cache before update |
| 99 | clearKeychainCache() |
| 100 | |
| 101 | try { |
| 102 | const storageServiceName = getMacOsKeychainStorageServiceName( |
| 103 | CREDENTIALS_SERVICE_SUFFIX, |
| 104 | ) |
| 105 | const username = getUsername() |
| 106 | const jsonString = jsonStringify(data) |
| 107 | |
| 108 | // Convert to hexadecimal to avoid any escaping issues |
| 109 | const hexValue = Buffer.from(jsonString, 'utf-8').toString('hex') |
| 110 | |
| 111 | // Prefer stdin (`security -i`) so process monitors (CrowdStrike et al.) |
| 112 | // see only "security -i", not the payload (INC-3028). |
| 113 | // When the payload would overflow the stdin line buffer, fall back to |
| 114 | // argv. Hex in argv is recoverable by a determined observer but defeats |
| 115 | // naive plaintext-grep rules, and the alternative — silent credential |
| 116 | // corruption — is strictly worse. ARG_MAX on darwin is 1MB so argv has |
| 117 | // effectively no size limit for our purposes. |
| 118 | const command = `add-generic-password -U -a "${username}" -s "${storageServiceName}" -X "${hexValue}"\n` |
| 119 | |
| 120 | let result |
| 121 | if (command.length <= SECURITY_STDIN_LINE_LIMIT) { |
| 122 | result = execaSync('security', ['-i'], { |
| 123 | input: command, |
| 124 | stdio: ['pipe', 'pipe', 'pipe'], |
| 125 | reject: false, |
| 126 | }) |
| 127 | } else { |
| 128 | logForDebugging( |
| 129 | `Keychain payload (${jsonString.length}B JSON) exceeds security -i stdin limit; using argv`, |
| 130 | { level: 'warn' }, |
| 131 | ) |
| 132 | result = execaSync( |
| 133 | 'security', |
| 134 | [ |
| 135 | 'add-generic-password', |
| 136 | '-U', |
| 137 | '-a', |
| 138 | username, |
| 139 | '-s', |
| 140 | storageServiceName, |
| 141 | '-X', |
| 142 | hexValue, |
| 143 | ], |
| 144 | { stdio: ['ignore', 'pipe', 'pipe'], reject: false }, |
| 145 | ) |
| 146 | } |
| 147 | |
| 148 | if (result.exitCode !== 0) { |
| 149 | return { success: false } |
| 150 | } |
| 151 | |
| 152 | // Update cache with new data on success |
| 153 | keychainCacheState.cache = { data, cachedAt: Date.now() } |
| 154 | return { success: true } |
nothing calls this directly
no test coverage detected