Download packages/core/src/config/extensions/integrity.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/extensions/integrity.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/config/extensions/integrity.ts
-
curl -L -o integrity.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/extensions/integrity.ts
10.1 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as fs from 'node:fs'; | |
| import * as path from 'node:path'; | |
| import { | |
| createHash, | |
| createHmac, | |
| randomBytes, | |
| timingSafeEqual, | |
| } from 'node:crypto'; | |
| import { | |
| INTEGRITY_FILENAME, | |
| INTEGRITY_KEY_FILENAME, | |
| KEYCHAIN_SERVICE_NAME, | |
| SECRET_KEY_ACCOUNT, | |
| } from '../constants.js'; | |
| import { type ExtensionInstallMetadata } from '../config.js'; | |
| import { KeychainService } from '../../services/keychainService.js'; | |
| import { isNodeError, getErrorMessage } from '../../utils/errors.js'; | |
| import { debugLogger } from '../../utils/debugLogger.js'; | |
| import { homedir, GEMINI_DIR } from '../../utils/paths.js'; | |
| import stableStringify from 'json-stable-stringify'; | |
| import { | |
| type IExtensionIntegrity, | |
| IntegrityDataStatus, | |
| type ExtensionIntegrityMap, | |
| type IntegrityStore, | |
| IntegrityStoreSchema, | |
| } from './integrityTypes.js'; | |
| export * from './integrityTypes.js'; | |
| /** | |
| * Manages the secret key used for signing integrity data. | |
| * Attempts to use the OS keychain, falling back to a restricted local file. | |
| * @internal | |
| */ | |
| class IntegrityKeyManager { | |
| private readonly fallbackKeyPath: string; | |
| private readonly keychainService: KeychainService; | |
| private cachedSecretKey: string | null = null; | |
| constructor() { | |
| const configDir = path.join(homedir(), GEMINI_DIR); | |
| this.fallbackKeyPath = path.join(configDir, INTEGRITY_KEY_FILENAME); | |
| this.keychainService = new KeychainService(KEYCHAIN_SERVICE_NAME); | |
| } | |
| /** | |
| * Retrieves or generates the master secret key. | |
| */ | |
| async getSecretKey(): Promise<string> { | |
| if (this.cachedSecretKey) { | |
| return this.cachedSecretKey; | |
| } | |
| if (await this.keychainService.isAvailable()) { | |
| try { | |
| this.cachedSecretKey = await this.getSecretKeyFromKeychain(); | |
| return this.cachedSecretKey; | |
| } catch (e) { | |
| debugLogger.warn( | |
| `Keychain access failed, falling back to file-based key: ${getErrorMessage(e)}`, | |
| ); | |
| } | |
| } | |
| this.cachedSecretKey = await this.getSecretKeyFromFile(); | |
| return this.cachedSecretKey; | |
| } | |
| private async getSecretKeyFromKeychain(): Promise<string> { | |
| let key = await this.keychainService.getPassword(SECRET_KEY_ACCOUNT); | |
| if (!key) { | |
| // Generate a fresh 256-bit key if none exists. | |
| key = randomBytes(32).toString('hex'); | |
| await this.keychainService.setPassword(SECRET_KEY_ACCOUNT, key); | |
| } | |
| return key; | |
| } | |
| private async getSecretKeyFromFile(): Promise<string> { | |
| try { | |
| const key = await fs.promises.readFile(this.fallbackKeyPath, 'utf-8'); | |
| return key.trim(); | |
| } catch (e) { | |
| if (isNodeError(e) && e.code === 'ENOENT') { | |
| // Lazily create the config directory if it doesn't exist. | |
| const configDir = path.dirname(this.fallbackKeyPath); | |
| await fs.promises.mkdir(configDir, { recursive: true }); | |
| // Generate a fresh 256-bit key for the local fallback. | |
| const key = randomBytes(32).toString('hex'); | |
| // Store with restricted permissions (read/write for owner only). | |
| await fs.promises.writeFile(this.fallbackKeyPath, key, { mode: 0o600 }); | |
| return key; | |
| } | |
| throw e; | |
| } | |
| } | |
| } | |
| /** | |
| * Handles the persistence and signature verification of the integrity store. | |
| * The entire store is signed to detect manual tampering of the JSON file. | |
| * @internal | |
| */ | |
| class ExtensionIntegrityStore { | |
| private readonly integrityStorePath: string; | |
| constructor(private readonly keyManager: IntegrityKeyManager) { | |
| const configDir = path.join(homedir(), GEMINI_DIR); | |
| this.integrityStorePath = path.join(configDir, INTEGRITY_FILENAME); | |
| } | |
| /** | |
| * Loads the integrity map from disk, verifying the store-wide signature. | |
| */ | |
| async load(): Promise<ExtensionIntegrityMap> { | |
| let content: string; | |
| try { | |
| content = await fs.promises.readFile(this.integrityStorePath, 'utf-8'); | |
| } catch (e) { | |
| if (isNodeError(e) && e.code === 'ENOENT') { | |
| return {}; | |
| } | |
| throw e; | |
| } | |
| const resetInstruction = `Please delete ${this.integrityStorePath} to reset it.`; | |
| // Parse and validate the store structure. | |
| let rawStore: IntegrityStore; | |
| try { | |
| rawStore = IntegrityStoreSchema.parse(JSON.parse(content)); | |
| } catch { | |
| throw new Error( | |
| `Failed to parse extension integrity store. ${resetInstruction}}`, | |
| ); | |
| } | |
| const { store, signature: actualSignature } = rawStore; | |
| // Re-generate the expected signature for the store content. | |
| const storeContent = stableStringify(store) ?? ''; | |
| const expectedSignature = await this.generateSignature(storeContent); | |
| // Verify the store hasn't been tampered with. | |
| if (!this.verifyConstantTime(actualSignature, expectedSignature)) { | |
| throw new Error( | |
| `Extension integrity store cannot be verified. ${resetInstruction}`, | |
| ); | |
| } | |
| return store; | |
| } | |
| /** | |
| * Persists the integrity map to disk with a fresh store-wide signature. | |
| */ | |
| async save(store: ExtensionIntegrityMap): Promise<void> { | |
| // Generate a signature for the entire map to prevent manual tampering. | |
| const storeContent = stableStringify(store) ?? ''; | |
| const storeSignature = await this.generateSignature(storeContent); | |
| const finalData: IntegrityStore = { | |
| store, | |
| signature: storeSignature, | |
| }; | |
| // Ensure parent directory exists before writing. | |
| const configDir = path.dirname(this.integrityStorePath); | |
| await fs.promises.mkdir(configDir, { recursive: true }); | |
| // Use a 'write-then-rename' pattern for an atomic update. | |
| // Restrict file permissions to owner only (0o600). | |
| const tmpPath = `${this.integrityStorePath}.tmp`; | |
| await fs.promises.writeFile(tmpPath, JSON.stringify(finalData, null, 2), { | |
| mode: 0o600, | |
| }); | |
| await fs.promises.rename(tmpPath, this.integrityStorePath); | |
| } | |
| /** | |
| * Generates a deterministic SHA-256 hash of the metadata. | |
| */ | |
| generateHash(metadata: ExtensionInstallMetadata): string { | |
| const content = stableStringify(metadata) ?? ''; | |
| return createHash('sha256').update(content).digest('hex'); | |
| } | |
| /** | |
| * Generates an HMAC-SHA256 signature using the master secret key. | |
| */ | |
| async generateSignature(data: string): Promise<string> { | |
| const secretKey = await this.keyManager.getSecretKey(); | |
| return createHmac('sha256', secretKey).update(data).digest('hex'); | |
| } | |
| /** | |
| * Constant-time comparison to prevent timing attacks. | |
| */ | |
| verifyConstantTime(actual: string, expected: string): boolean { | |
| const actualBuffer = Buffer.from(actual, 'hex'); | |
| const expectedBuffer = Buffer.from(expected, 'hex'); | |
| // timingSafeEqual requires buffers of the same length. | |
| if (actualBuffer.length !== expectedBuffer.length) { | |
| return false; | |
| } | |
| return timingSafeEqual(actualBuffer, expectedBuffer); | |
| } | |
| } | |
| /** | |
| * Implementation of IExtensionIntegrity that persists data to disk. | |
| */ | |
| export class ExtensionIntegrityManager implements IExtensionIntegrity { | |
| private readonly keyManager: IntegrityKeyManager; | |
| private readonly integrityStore: ExtensionIntegrityStore; | |
| private writeLock: Promise<void> = Promise.resolve(); | |
| constructor() { | |
| this.keyManager = new IntegrityKeyManager(); | |
| this.integrityStore = new ExtensionIntegrityStore(this.keyManager); | |
| } | |
| /** | |
| * Verifies the provided metadata against the recorded integrity data. | |
| */ | |
| async verify( | |
| extensionName: string, | |
| metadata: ExtensionInstallMetadata | undefined, | |
| ): Promise<IntegrityDataStatus> { | |
| if (!metadata) { | |
| return IntegrityDataStatus.MISSING; | |
| } | |
| try { | |
| const storeMap = await this.integrityStore.load(); | |
| const extensionRecord = storeMap[extensionName]; | |
| if (!extensionRecord) { | |
| return IntegrityDataStatus.MISSING; | |
| } | |
| // Verify the hash (metadata content) matches the recorded value. | |
| const actualHash = this.integrityStore.generateHash(metadata); | |
| const isHashValid = this.integrityStore.verifyConstantTime( | |
| actualHash, | |
| extensionRecord.hash, | |
| ); | |
| if (!isHashValid) { | |
| debugLogger.warn( | |
| `Integrity mismatch for "${extensionName}": Hash mismatch.`, | |
| ); | |
| return IntegrityDataStatus.INVALID; | |
| } | |
| // Verify the signature (authenticity) using the master secret key. | |
| const actualSignature = | |
| await this.integrityStore.generateSignature(actualHash); | |
| const isSignatureValid = this.integrityStore.verifyConstantTime( | |
| actualSignature, | |
| extensionRecord.signature, | |
| ); | |
| if (!isSignatureValid) { | |
| debugLogger.warn( | |
| `Integrity mismatch for "${extensionName}": Signature mismatch.`, | |
| ); | |
| return IntegrityDataStatus.INVALID; | |
| } | |
| return IntegrityDataStatus.VERIFIED; | |
| } catch (e) { | |
| debugLogger.warn( | |
| `Error verifying integrity for "${extensionName}": ${getErrorMessage(e)}`, | |
| ); | |
| return IntegrityDataStatus.INVALID; | |
| } | |
| } | |
| /** | |
| * Records the integrity data for an extension. | |
| * Uses a promise chain to serialize concurrent store operations. | |
| */ | |
| async store( | |
| extensionName: string, | |
| metadata: ExtensionInstallMetadata, | |
| ): Promise<void> { | |
| const operation = (async () => { | |
| await this.writeLock; | |
| // Generate integrity data for the new metadata. | |
| const hash = this.integrityStore.generateHash(metadata); | |
| const signature = await this.integrityStore.generateSignature(hash); | |
| // Update the store map and persist to disk. | |
| const storeMap = await this.integrityStore.load(); | |
| storeMap[extensionName] = { hash, signature }; | |
| await this.integrityStore.save(storeMap); | |
| })(); | |
| // Update the lock to point to the latest operation, ensuring they are serialized. | |
| this.writeLock = operation.catch(() => {}); | |
| return operation; | |
| } | |
| /** | |
| * Retrieves or generates the master secret key. | |
| * @internal visible for testing | |
| */ | |
| async getSecretKey(): Promise<string> { | |
| return this.keyManager.getSecretKey(); | |
| } | |
| } | |