@@ -6,10 +6,12 @@ import { join } from "node:path";
66
77// Chromium OSCrypt: "v10" = fixed password (macOS: the Keychain password; Linux: "peanuts"),
88// "v11" = Linux Secret Service password. macOS stretches with 1003 PBKDF2 rounds, Linux with 1.
9+ // Windows v10 uses AES-256-GCM with a DPAPI-wrapped key from Local State (not PBKDF2).
910const SAFE_STORAGE_PREFIX_V10 = "v10" ;
1011const SAFE_STORAGE_PREFIX_V11 = "v11" ;
12+ const SAFE_STORAGE_PREFIX_V10_BUF = Buffer . from ( SAFE_STORAGE_PREFIX_V10 ) ;
1113const LINUX_BASIC_TEXT_PASSWORD = "peanuts" ;
12- const SUPPORTED_PLATFORMS = new Set ( [ "darwin" , "linux" ] ) ;
14+ const SUPPORTED_PLATFORMS = new Set ( [ "darwin" , "linux" , "win32" ] ) ;
1315
1416export class GrokBotGatewaySessionError extends Error {
1517 constructor ( code , message ) {
@@ -80,23 +82,57 @@ export function decryptSafeStorageString(encryptedBase64, password, platform = "
8082 ] ) . toString ( "utf8" ) ;
8183}
8284
83- export function grokBotGatewayDescriptorPath ( home = homedir ( ) , platform = process . platform , env = process . env ) {
85+ // Windows Chromium Safe Storage: "v10" + 12-byte nonce + AES-256-GCM ciphertext + 16-byte tag.
86+ export function decryptWindowsSafeStorageString ( encryptedBase64 , key ) {
87+ const encrypted = Buffer . from ( encryptedBase64 , "base64" ) ;
88+ if ( ! encrypted . subarray ( 0 , 3 ) . equals ( SAFE_STORAGE_PREFIX_V10_BUF ) ) {
89+ throw new Error ( "Unsupported Grok Bot Safe Storage format." ) ;
90+ }
91+
92+ const decipher = crypto . createDecipheriv (
93+ "aes-256-gcm" ,
94+ key ,
95+ encrypted . subarray ( 3 , 15 ) ,
96+ ) ;
97+ decipher . setAuthTag ( encrypted . subarray ( - 16 ) ) ;
98+ return Buffer . concat ( [
99+ decipher . update ( encrypted . subarray ( 15 , - 16 ) ) ,
100+ decipher . final ( ) ,
101+ ] ) . toString ( "utf8" ) ;
102+ }
103+
104+ function grokBotAppDataPath ( home , platform , env = { } ) {
105+ if ( platform === "win32" ) {
106+ const appData = env . APPDATA || join ( home , "AppData/Roaming" ) ;
107+ return join ( appData , "Grok Bot" ) ;
108+ }
84109 if ( platform === "linux" ) {
85110 const configHome = env . XDG_CONFIG_HOME || join ( home , ".config" ) ;
86- return join ( configHome , "Grok Bot/gateway-descriptor.json " ) ;
111+ return join ( configHome , "Grok Bot" ) ;
87112 }
88- return join (
89- home ,
90- "Library/Application Support/Grok Bot/gateway-descriptor.json" ,
91- ) ;
113+ return join ( home , "Library/Application Support/Grok Bot" ) ;
114+ }
115+
116+ export function grokBotGatewayDescriptorPath ( home = homedir ( ) , platform = process . platform , env = process . env ) {
117+ return join ( grokBotAppDataPath ( home , platform , env ) , "gateway-descriptor.json" ) ;
118+ }
119+
120+ function sessionEnv ( { env = process . env , appData } = { } ) {
121+ // Windows tests pass appData directly; empty string clears APPDATA to exercise the home fallback.
122+ if ( appData !== undefined ) return { ...env , APPDATA : appData } ;
123+ return env ;
92124}
93125
94126export function hasGrokBotGatewaySession ( {
95127 platform = process . platform ,
96128 home = homedir ( ) ,
97129 env = process . env ,
130+ appData,
98131} = { } ) {
99- return SUPPORTED_PLATFORMS . has ( platform ) && existsSync ( grokBotGatewayDescriptorPath ( home , platform , env ) ) ;
132+ return (
133+ SUPPORTED_PLATFORMS . has ( platform ) &&
134+ existsSync ( grokBotGatewayDescriptorPath ( home , platform , sessionEnv ( { env, appData } ) ) )
135+ ) ;
100136}
101137
102138function readKeychainPassword ( platform = process . platform ) {
@@ -114,27 +150,72 @@ function readKeychainPassword(platform = process.platform) {
114150 ) . trimEnd ( ) ;
115151}
116152
153+ function unprotectWithDpapi ( blob ) {
154+ const script =
155+ "Add-Type -AssemblyName System.Security; " +
156+ "$blob = [Convert]::FromBase64String([Console]::In.ReadToEnd().Trim()); " +
157+ "[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Unprotect($blob, $null, 'CurrentUser'))" ;
158+ const out = execFileSync (
159+ join (
160+ process . env . SystemRoot ?? "C:\\Windows" ,
161+ "System32/WindowsPowerShell/v1.0/powershell.exe" ,
162+ ) ,
163+ [ "-NoProfile" , "-NonInteractive" , "-Command" , script ] ,
164+ { input : blob . toString ( "base64" ) , encoding : "utf8" } ,
165+ ) ;
166+ return Buffer . from ( out . trim ( ) , "base64" ) ;
167+ }
168+
169+ function readWindowsSafeStorageKey ( home , env , unprotectData ) {
170+ const path = join ( grokBotAppDataPath ( home , "win32" , env ) , "Local State" ) ;
171+ const encryptedKey = existsSync ( path )
172+ ? JSON . parse ( readFileSync ( path , "utf8" ) ) . os_crypt ?. encrypted_key
173+ : null ;
174+ const blob = Buffer . from (
175+ typeof encryptedKey === "string" ? encryptedKey : "" ,
176+ "base64" ,
177+ ) ;
178+ if ( blob . subarray ( 0 , 5 ) . toString ( "latin1" ) !== "DPAPI" ) {
179+ throw new GrokBotGatewaySessionError (
180+ "MISSING_SAFE_STORAGE_KEY" ,
181+ "Grok Bot Local State has no Safe Storage key." ,
182+ ) ;
183+ }
184+ return unprotectData ( blob . subarray ( 5 ) ) ;
185+ }
186+
117187export function loadGrokBotGatewaySession ( {
118188 platform = process . platform ,
119189 home = homedir ( ) ,
120190 env = process . env ,
191+ appData,
121192 getKeychainPassword = readKeychainPassword ,
193+ unprotectData = unprotectWithDpapi ,
122194} = { } ) {
123195 if ( ! SUPPORTED_PLATFORMS . has ( platform ) ) return null ;
124196
125- const path = grokBotGatewayDescriptorPath ( home , platform , env ) ;
197+ const effectiveEnv = sessionEnv ( { env, appData } ) ;
198+ const path = grokBotGatewayDescriptorPath ( home , platform , effectiveEnv ) ;
126199 if ( ! existsSync ( path ) ) return null ;
127200
128201 const wrapped = JSON . parse ( readFileSync ( path , "utf8" ) ) ;
129202 const encrypted = encryptedPayload ( wrapped ) ;
130- const prefix = Buffer . from ( encrypted , "base64" ) . subarray ( 0 , 3 ) . toString ( "latin1" ) ;
131- // Linux v10 is the keyring-less basic_text backend; no secret store to ask.
132- const needsKeychain = ! ( platform === "linux" && prefix === SAFE_STORAGE_PREFIX_V10 ) ;
133- const clear = decryptSafeStorageString (
134- encrypted ,
135- needsKeychain ? getKeychainPassword ( platform ) : LINUX_BASIC_TEXT_PASSWORD ,
136- platform ,
137- ) ;
203+ let clear ;
204+ if ( platform === "win32" ) {
205+ clear = decryptWindowsSafeStorageString (
206+ encrypted ,
207+ readWindowsSafeStorageKey ( home , effectiveEnv , unprotectData ) ,
208+ ) ;
209+ } else {
210+ const prefix = Buffer . from ( encrypted , "base64" ) . subarray ( 0 , 3 ) . toString ( "latin1" ) ;
211+ // Linux v10 is the keyring-less basic_text backend; no secret store to ask.
212+ const needsKeychain = ! ( platform === "linux" && prefix === SAFE_STORAGE_PREFIX_V10 ) ;
213+ clear = decryptSafeStorageString (
214+ encrypted ,
215+ needsKeychain ? getKeychainPassword ( platform ) : LINUX_BASIC_TEXT_PASSWORD ,
216+ platform ,
217+ ) ;
218+ }
138219 const descriptor = JSON . parse ( clear ) ;
139220 if ( ! descriptor . baseUrl || ! descriptor . token ) {
140221 throw new GrokBotGatewaySessionError (
0 commit comments